diff --git a/.doctor-rst.yaml b/.doctor-rst.yaml new file mode 100644 index 00000000000..c4d6b09029a --- /dev/null +++ b/.doctor-rst.yaml @@ -0,0 +1,82 @@ +rules: + no_inheritdoc: ~ + avoid_repetetive_words: ~ + blank_line_after_directive: ~ + short_array_syntax: ~ + no_app_console: ~ + typo: ~ + replacement: ~ + composer_dev_option_not_at_the_end: ~ + yarn_dev_option_at_the_end: ~ + versionadded_directive_should_have_version: ~ + deprecated_directive_should_have_version: ~ + no_composer_req: ~ + no_php_open_tag_in_code_block_php_directive: ~ + no_blank_line_after_filepath_in_php_code_block: ~ + no_blank_line_after_filepath_in_yaml_code_block: ~ + no_blank_line_after_filepath_in_xml_code_block: ~ + no_blank_line_after_filepath_in_twig_code_block: ~ + php_prefix_before_bin_console: ~ + use_deprecated_directive_instead_of_versionadded: ~ + no_space_before_self_xml_closing_tag: ~ + no_explicit_use_of_code_block_php: ~ + ensure_order_of_code_blocks_in_configuration_block: ~ + american_english: ~ + valid_use_statements: ~ + lowercase_as_in_use_statements: ~ + ordered_use_statements: ~ + no_namespace_after_use_statements: ~ + correct_code_block_directive_based_on_the_content: ~ + max_blank_lines: + max: 2 + replace_code_block_types: ~ + use_https_xsd_urls: ~ + blank_line_before_directive: ~ + extension_xlf_instead_of_xliff: ~ + valid_inline_highlighted_namespaces: ~ + indention: ~ + unused_links: ~ + yaml_instead_of_yml_suffix: ~ + extend_abstract_controller: ~ +# no_app_bundle: ~ + + # 4.x + versionadded_directive_major_version: + major_version: 4 + + versionadded_directive_min_version: + min_version: '4.0' + + deprecated_directive_major_version: + major_version: 4 + + deprecated_directive_min_version: + min_version: '4.0' + +# do not report as violation +whitelist: + regex: + - '/FOSUserBundle(.*)\.yml/' + - '/``.yml``/' + - '/(.*)\.orm\.yml/' # currently DoctrineBundle only supports .yml + lines: + - 'in config files, so the old ``app/config/config_dev.yml`` goes to' + - '#. The most important config file is ``app/config/services.yml``, which now is' + - 'code in production without a proxy, it becomes trivially easy to abuse your' + - '.. _`EasyDeployBundle`: https://github.com/EasyCorp/easy-deploy-bundle' + - 'The bin/console Command' + - '# username is your full Gmail or Google Apps email address' + - '.. _`LDAP injection`: http://projects.webappsec.org/w/page/13246947/LDAP%20Injection' + - '.. versionadded:: 0.21.0' # Encore + - '.. versionadded:: 2.4.0' # SwiftMailer + - '.. versionadded:: 1.26' # Twig + - '.. versionadded:: 1.30' # Twig + - '.. versionadded:: 1.2' # MakerBundle + - '.. versionadded:: 1.11' # MakerBundle + - '.. versionadded:: 1.3' # MakerBundle + - '.. versionadded:: 1.8' # MakerBundle + - '0 => 123' # assertion for var_dumper - components/var_dumper.rst + - '1 => "foo"' # assertion for var_dumper - components/var_dumper.rst + - '$var .= "Because of this `\xE9` octet (\\xE9),\n";' + - "`Deploying Symfony 4 Apps on Heroku`_." + - ".. _`Deploying Symfony 4 Apps on Heroku`: https://devcenter.heroku.com/articles/deploying-symfony4" diff --git a/.editorconfig b/.editorconfig index 6f5286431d4..f9366facfb0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,8 @@ root = true -[*.{rst,rst.inc}] +[*] indent_style = space -indent_size = 2 +indent_size = 4 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 00000000000..ada570e342c --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,12 @@ +on: [push, pull_request] +name: Lint +jobs: + doctor-rst: + name: DOCtor-RST + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: DOCtor-RST + uses: docker://oskarstark/doctor-rst + with: + args: --short diff --git a/.travis.yml b/.travis.yml index cccb01eef30..8ed2088e6ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,12 +4,12 @@ python: 2.7 sudo: false cache: - directories: [$HOME/.cache/pip] + directories: [$HOME/.cache/pip] install: pip install -r _build/.requirements.txt script: make -C _build SPHINXOPTS=-nW html branches: - except: - - github-comments + except: + - github-comments diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..d211dd419d0 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,83 @@ +Code of Conduct +=============== + +Our Pledge +---------- + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnic origin, gender identity and expression, level of +experience, education, socio-economic status, nationality, personal appearance, +religion, or sexual identity and orientation. + +Our Standards +------------- + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +Our Responsibilities +-------------------- + +[CoC Active Response Ensurers, or CARE][1], are responsible for clarifying the +standards of acceptable behavior and are expected to take appropriate and fair +corrective action in response to any instances of unacceptable behavior. + +CARE team members have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +Scope +----- + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by CARE team members. + +Enforcement +----------- + +Instances of abusive, harassing, or otherwise unacceptable behavior +[may be reported][2] by contacting the [CARE team members][1]. +All complaints will be reviewed and investigated and will result in a response +that is deemed necessary and appropriate to the circumstances. The CARE team is +obligated to maintain confidentiality with regard to the reporter of an +incident. Further details of specific enforcement policies may be posted +separately. + +CARE team members who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by the +[core team][3]. + +Attribution +----------- + +This Code of Conduct is adapted from the [Contributor Covenant version 1.4][4]. + +[1]: https://symfony.com/doc/current/contributing/code_of_conduct/care_team.html +[2]: https://symfony.com/doc/current/contributing/code_of_conduct/reporting_guidelines.html +[3]: https://symfony.com/doc/current/contributing/code/core_team.html +[4]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000000..01524e6ec84 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,340 @@ +LICENSE +======= + +**Creative Commons Attribution-ShareAlike 3.0 Unported** +https://creativecommons.org/licenses/by-sa/3.0/ + +----- + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS +PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR +OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS +LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE +BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED +TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN +CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. + +1. Definitions +-------------- + +a. **"Adaptation"** means a work based upon the Work, or upon the Work and other +pre-existing works, such as a translation, adaptation, derivative work, +arrangement of music or other alterations of a literary or artistic work, or +phonogram or performance and includes cinematographic adaptations or any other +form in which the Work may be recast, transformed, or adapted including in any +form recognizably derived from the original, except that a work that constitutes +a Collection will not be considered an Adaptation for the purpose of this +License. For the avoidance of doubt, where the Work is a musical work, +performance or phonogram, the synchronization of the Work in timed-relation with +a moving image ("synching") will be considered an Adaptation for the purpose of +this License. + +b. **"Collection"** means a collection of literary or artistic works, such as +encyclopedias and anthologies, or performances, phonograms or broadcasts, or +other works or subject matter other than works listed in Section 1(f) below, +which, by reason of the selection and arrangement of their contents, constitute +intellectual creations, in which the Work is included in its entirety in +unmodified form along with one or more other contributions, each constituting +separate and independent works in themselves, which together are assembled into +a collective whole. A work that constitutes a Collection will not be considered +an Adaptation (as defined below) for the purposes of this License. + +c. **"Creative Commons Compatible License"** means a license that is listed at +https://creativecommons.org/compatiblelicenses that has been approved by +Creative Commons as being essentially equivalent to this License, including, at +a minimum, because that license: (i) contains terms that have the same purpose, +meaning and effect as the License Elements of this License; and, (ii) explicitly +permits the relicensing of adaptations of works made available under that +license under this License or a Creative Commons jurisdiction license with the +same License Elements as this License. + +d. **"Distribute"** means to make available to the public the original and +copies of the Work or Adaptation, as appropriate, through sale or other transfer +of ownership. + +e. **"License Elements"** means the following high-level license attributes as +selected by Licensor and indicated in the title of this License: Attribution, +ShareAlike. + +f. **"Licensor"** means the individual, individuals, entity or entities that +offer(s) the Work under the terms of this License. + +g. **"Original Author""** means, in the case of a literary or artistic work, the +individual, individuals, entity or entities who created the Work or if no +individual or entity can be identified, the publisher; and in addition (i) in +the case of a performance the actors, singers, musicians, dancers, and other +persons who act, sing, deliver, declaim, play in, interpret or otherwise perform +literary or artistic works or expressions of folklore; (ii) in the case of a +phonogram the producer being the person or legal entity who first fixes the +sounds of a performance or other sounds; and, (iii) in the case of broadcasts, +the organization that transmits the broadcast. + +h. **"Work"** means the literary and/or artistic work offered under the terms of +this License including without limitation any production in the literary, +scientific and artistic domain, whatever may be the mode or form of its +expression including digital form, such as a book, pamphlet and other writing; a +lecture, address, sermon or other work of the same nature; a dramatic or +dramatico-musical work; a choreographic work or entertainment in dumb show; a +musical composition with or without words; a cinematographic work to which are +assimilated works expressed by a process analogous to cinematography; a work of +drawing, painting, architecture, sculpture, engraving or lithography; a +photographic work to which are assimilated works expressed by a process +analogous to photography; a work of applied art; an illustration, map, plan, +sketch or three-dimensional work relative to geography, topography, architecture +or science; a performance; a broadcast; a phonogram; a compilation of data to +the extent it is protected as a copyrightable work; or a work performed by a +variety or circus performer to the extent it is not otherwise considered a +literary or artistic work. + +i. **"You"** means an individual or entity exercising rights under this License +who has not previously violated the terms of this License with respect to the +Work, or who has received express permission from the Licensor to exercise +rights under this License despite a previous violation. + +j. **"Publicly Perform"** means to perform public recitations of the Work and to +communicate to the public those public recitations, by any means or process, +including by wire or wireless means or public digital performances; to make +available to the public Works in such a way that members of the public may +access these Works from a place and at a place individually chosen by them; to +perform the Work to the public by any means or process and the communication to +the public of the performances of the Work, including by public digital +performance; to broadcast and rebroadcast the Work by any means including signs, +sounds or images. + +k. **"Reproduce"** means to make copies of the Work by any means including +without limitation by sound or visual recordings and the right of fixation and +reproducing fixations of the Work, including storage of a protected performance +or phonogram in digital form or other electronic medium. + +2. Fair Dealing Rights +---------------------- + +Nothing in this License is intended to reduce, limit, or restrict any uses free +from copyright or rights arising from limitations or exceptions that are +provided for in connection with the copyright protection under copyright law or +other applicable laws. + +3. License Grant +---------------- + +Subject to the terms and conditions of this License, Licensor hereby grants You +a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the +applicable copyright) license to exercise the rights in the Work as stated +below: + +a. to Reproduce the Work, to incorporate the Work into one or more Collections, +and to Reproduce the Work as incorporated in the Collections; + +b. to create and Reproduce Adaptations provided that any such Adaptation, +including any translation in any medium, takes reasonable steps to clearly +label, demarcate or otherwise identify that changes were made to the original +Work. For example, a translation could be marked "The original work was +translated from English to Spanish," or a modification could indicate "The +original work has been modified."; + +c. to Distribute and Publicly Perform the Work including as incorporated in +Collections; and, + +d. to Distribute and Publicly Perform Adaptations. + +e. For the avoidance of doubt: + + 1. **Non-waivable Compulsory License Schemes.** In those jurisdictions in + which the right to collect royalties through any statutory or compulsory + licensing scheme cannot be waived, the Licensor reserves the exclusive + right to collect such royalties for any exercise by You of the rights + granted under this License; + + 2. **Waivable Compulsory License Schemes.** In those jurisdictions in which + the right to collect royalties through any statutory or compulsory + licensing scheme can be waived, the Licensor waives the exclusive right to + collect such royalties for any exercise by You of the rights granted under + this License; and, + + 3. **Voluntary License Schemes.** The Licensor waives the right to collect + royalties, whether individually or, in the event that the Licensor is a + member of a collecting society that administers voluntary licensing + schemes, via that society, from any exercise by You of the rights granted + under this License. + +The above rights may be exercised in all media and formats whether now known or +hereafter devised. The above rights include the right to make such modifications +as are technically necessary to exercise the rights in other media and formats. +Subject to Section 8(f), all rights not expressly granted by Licensor are hereby +reserved. + +4. Restrictions +--------------- + +The license granted in Section 3 above is expressly made subject to and limited +by the following restrictions: + +a. You may Distribute or Publicly Perform the Work only under the terms of this +License. You must include a copy of, or the Uniform Resource Identifier (URI) +for, this License with every copy of the Work You Distribute or Publicly +Perform. You may not offer or impose any terms on the Work that restrict the +terms of this License or the ability of the recipient of the Work to exercise +the rights granted to that recipient under the terms of the License. You may not +sublicense the Work. You must keep intact all notices that refer to this License +and to the disclaimer of warranties with every copy of the Work You Distribute +or Publicly Perform. When You Distribute or Publicly Perform the Work, You may +not impose any effective technological measures on the Work that restrict the +ability of a recipient of the Work from You to exercise the rights granted to +that recipient under the terms of the License. This Section 4(a) applies to the +Work as incorporated in a Collection, but this does not require the Collection +apart from the Work itself to be made subject to the terms of this License. If +You create a Collection, upon notice from any Licensor You must, to the extent +practicable, remove from the Collection any credit as required by Section 4(c), +as requested. If You create an Adaptation, upon notice from any Licensor You +must, to the extent practicable, remove from the Adaptation any credit as +required by Section 4(c), as requested. + +b. You may Distribute or Publicly Perform an Adaptation only under the terms of: +(i) this License; (ii) a later version of this License with the same License +Elements as this License; (iii) a Creative Commons jurisdiction license (either +this or a later license version) that contains the same License Elements as this +License (e.g., Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons +Compatible License. If you license the Adaptation under one of the licenses +mentioned in (iv), you must comply with the terms of that license. If you +license the Adaptation under the terms of any of the licenses mentioned in (i), +(ii) or (iii) (the "Applicable License"), you must comply with the terms of the +Applicable License generally and the following provisions: (I) You must include +a copy of, or the URI for, the Applicable License with every copy of each +Adaptation You Distribute or Publicly Perform; (II) You may not offer or impose +any terms on the Adaptation that restrict the terms of the Applicable License or +the ability of the recipient of the Adaptation to exercise the rights granted to +that recipient under the terms of the Applicable License; (III) You must keep +intact all notices that refer to the Applicable License and to the disclaimer of +warranties with every copy of the Work as included in the Adaptation You +Distribute or Publicly Perform; (IV) when You Distribute or Publicly Perform the +Adaptation, You may not impose any effective technological measures on the +Adaptation that restrict the ability of a recipient of the Adaptation from You +to exercise the rights granted to that recipient under the terms of the +Applicable License. This Section 4(b) applies to the Adaptation as incorporated +in a Collection, but this does not require the Collection apart from the +Adaptation itself to be made subject to the terms of the Applicable License. + +c. If You Distribute, or Publicly Perform the Work or any Adaptations or +Collections, You must, unless a request has been made pursuant to Section 4(a), +keep intact all copyright notices for the Work and provide, reasonable to the +medium or means You are utilizing: (i) the name of the Original Author (or +pseudonym, if applicable) if supplied, and/or if the Original Author and/or +Licensor designate another party or parties (e.g., a sponsor institute, +publishing entity, journal) for attribution ("Attribution Parties") in +Licensor's copyright notice, terms of service or by other reasonable means, the +name of such party or parties; (ii) the title of the Work if supplied; (iii) to +the extent reasonably practicable, the URI, if any, that Licensor specifies to +be associated with the Work, unless such URI does not refer to the copyright +notice or licensing information for the Work; and (iv) , consistent with Section +3(b), in the case of an Adaptation, a credit identifying the use of the Work in +the Adaptation (e.g., "French translation of the Work by Original Author," or +"Screenplay based on original Work by Original Author"). The credit required by +this Section 4(c) may be implemented in any reasonable manner; provided, +however, that in the case of a Adaptation or Collection, at a minimum such +credit will appear, if a credit for all contributing authors of the Adaptation +or Collection appears, then as part of these credits and in a manner at least as +prominent as the credits for the other contributing authors. For the avoidance +of doubt, You may only use the credit required by this Section for the purpose +of attribution in the manner set out above and, by exercising Your rights under +this License, You may not implicitly or explicitly assert or imply any +connection with, sponsorship or endorsement by the Original Author, Licensor +and/or Attribution Parties, as appropriate, of You or Your use of the Work, +without the separate, express prior written permission of the Original Author, +Licensor and/or Attribution Parties. + +d. Except as otherwise agreed in writing by the Licensor or as may be otherwise +permitted by applicable law, if You Reproduce, Distribute or Publicly Perform +the Work either by itself or as part of any Adaptations or Collections, You must +not distort, mutilate, modify or take other derogatory action in relation to the +Work which would be prejudicial to the Original Author's honor or reputation. +Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise +of the right granted in Section 3(b) of this License (the right to make +Adaptations) would be deemed to be a distortion, mutilation, modification or +other derogatory action prejudicial to the Original Author's honor and +reputation, the Licensor will waive or not assert, as appropriate, this Section, +to the fullest extent permitted by the applicable national law, to enable You to +reasonably exercise Your right under Section 3(b) of this License (right to make +Adaptations) but not otherwise. + +5. Representations, Warranties and Disclaimer +--------------------------------------------- + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS +THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING +THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT +LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR +PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, +OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME +JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH +EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability +-------------------------- + +EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE +LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, +PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE +WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination +-------------- + +a. This License and the rights granted hereunder will terminate automatically +upon any breach by You of the terms of this License. Individuals or entities who +have received Adaptations or Collections from You under this License, however, +will not have their licenses terminated provided such individuals or entities +remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 +will survive any termination of this License. + +b. Subject to the above terms and conditions, the license granted here is +perpetual (for the duration of the applicable copyright in the Work). +Notwithstanding the above, Licensor reserves the right to release the Work under +different license terms or to stop distributing the Work at any time; provided, +however that any such election will not serve to withdraw this License (or any +other license that has been, or is required to be, granted under the terms of +this License), and this License will continue in full force and effect unless +terminated as stated above. + +8. Miscellaneous +---------------- + +a. Each time You Distribute or Publicly Perform the Work or a Collection, the +Licensor offers to the recipient a license to the Work on the same terms and +conditions as the license granted to You under this License. + +b. Each time You Distribute or Publicly Perform an Adaptation, Licensor offers +to the recipient a license to the original Work on the same terms and conditions +as the license granted to You under this License. + +c. If any provision of this License is invalid or unenforceable under applicable +law, it shall not affect the validity or enforceability of the remainder of the +terms of this License, and without further action by the parties to this +agreement, such provision shall be reformed to the minimum extent necessary to +make such provision valid and enforceable. + +d. No term or provision of this License shall be deemed waived and no breach +consented to unless such waiver or consent shall be in writing and signed by the +party to be charged with such waiver or consent. + +e. This License constitutes the entire agreement between the parties with +respect to the Work licensed here. There are no understandings, agreements or +representations with respect to the Work not specified here. Licensor shall not +be bound by any additional provisions that may appear in any communication from +You. This License may not be modified without the mutual written agreement of +the Licensor and You. + +f. The rights granted under, and the subject matter referenced, in this License +were drafted utilizing the terminology of the Berne Convention for the +Protection of Literary and Artistic Works (as amended on September 28, 1979), +the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO +Performances and Phonograms Treaty of 1996 and the Universal Copyright +Convention (as revised on July 24, 1971). These rights and subject matter take +effect in the relevant jurisdiction in which the License terms are sought to be +enforced according to the corresponding provisions of the implementation of +those treaty provisions in the applicable national law. If the standard suite of +rights granted under applicable copyright law includes additional rights not +granted under this License, such additional rights are deemed to be included in +the License; this License is not intended to restrict the license of any rights +under applicable law. diff --git a/_build/_themes/_exts/symfonycom/sphinx/lexer.py b/_build/_themes/_exts/symfonycom/sphinx/lexer.py index 4100b66d283..f1e87066236 100644 --- a/_build/_themes/_exts/symfonycom/sphinx/lexer.py +++ b/_build/_themes/_exts/symfonycom/sphinx/lexer.py @@ -10,7 +10,7 @@ class TerminalLexer(RegexLexer): tokens = { 'root': [ ('^\$', Generic.Prompt, 'bash-prompt'), - ('^[^\n>]+>', Generic.Prompt, 'dos-prompt'), + ('^>', Generic.Prompt, 'dos-prompt'), ('^#.+$', Comment.Single), ('^.+$', Generic.Output), ], diff --git a/_build/redirection_map b/_build/redirection_map index 02d9fc81cc6..183b3d0e9ed 100644 --- a/_build/redirection_map +++ b/_build/redirection_map @@ -421,7 +421,41 @@ /configuration/configuration_organization /configuration /configuration/environments /configuration /configuration/configuration_organization /configuration +/email/dev_environment /mailer +/email/spool /mailer +/email/testing /mailer /contributing/community/other /contributing/community /profiler/storage /profiler /setup/composer /setup /security/security_checker /setup +/service_container/parameters /configuration +/routing/generate_url_javascript /routing +/routing/slash_in_parameter /routing +/routing/scheme /routing +/routing/optional_placeholders /routing +/routing/conditions /routing +/routing/requirements /routing +/routing/redirect_trailing_slash /routing +/routing/debug /routing +/routing/service_container_parameters /routing +/routing/redirect_in_config /routing +/routing/external_resources /routing +/routing/hostname_pattern /routing +/routing/extra_information /routing +/console/request_context /routing +/form/action_method /forms +/reference/requirements /setup + /bundles/inheritance /bundles/override +/templating /templates +/templating/escaping /templates +/templating/syntax /templates +/templating/debug /templates +/templating/render_without_controller /templates +/templating/app_variable /templates +/templating/formats /templates +/templating/namespaced_paths /templates +/templating/embedding_controllers /templates +/templating/inheritance /templates +/testing/doctrine /testing/database +/doctrine/lifecycle_callbacks /doctrine/events +/doctrine/event_listeners_subscribers /doctrine/events diff --git a/_images/components/workflow/pull_request_puml_styled.png b/_images/components/workflow/pull_request_puml_styled.png new file mode 100644 index 00000000000..cda9233d731 Binary files /dev/null and b/_images/components/workflow/pull_request_puml_styled.png differ diff --git a/best_practices/business-logic.rst b/best_practices/business-logic.rst index 0e2674ade0a..a6f07c73ec6 100644 --- a/best_practices/business-logic.rst +++ b/best_practices/business-logic.rst @@ -123,9 +123,9 @@ library or strategy you want for this. In practice, many Symfony applications rely on the independent `Doctrine project`_ to define their model using entities and repositories. -Doctrine support is not enabled by default in Symfony. So to use Doctrine -as shown in the examples below you will need to install :doc:`Doctrine ORM support ` -by executing the following command: +:doc:`Doctrine ` support is not enabled by default in Symfony, so you +must install it first by adding the ``orm`` :ref:`Symfony pack ` +to your application: .. code-block:: terminal @@ -276,7 +276,6 @@ Next: :doc:`/best_practices/controllers` .. _`full definition`: https://en.wikipedia.org/wiki/Business_logic .. _`Doctrine project`: http://www.doctrine-project.org/ -.. _`Doctrine ORM support`: https://symfony.com/doc/current/doctrine.html .. _`fixture class`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html#writing-simple-fixtures .. _`PSR-1`: https://www.php-fig.org/psr/psr-1/ .. _`PSR-2`: https://www.php-fig.org/psr/psr-2/ diff --git a/best_practices/controllers.rst b/best_practices/controllers.rst index c2e9827b32e..83e38ed0391 100644 --- a/best_practices/controllers.rst +++ b/best_practices/controllers.rst @@ -47,14 +47,15 @@ this suffix is neither required nor recommended, so you can safely remove it. Routing Configuration --------------------- -To load routes defined as annotations in your controllers, add the following -configuration to the main routing configuration file: +To load routes defined as annotations in your controllers, run +``composer require doctrine/annotations``. Thanks to the :ref:`Flex recipe `, +a ``config/routes/annotations.yaml`` file will be created: .. code-block:: yaml - # config/routes.yaml + # config/routes/annotations.yaml controllers: - resource: '../src/Controller/' + resource: '../../src/Controller/' type: annotation This configuration will load annotations from any controller stored inside the diff --git a/best_practices/creating-the-project.rst b/best_practices/creating-the-project.rst index e323d6bcf7a..2c5ea8a5d55 100644 --- a/best_practices/creating-the-project.rst +++ b/best_practices/creating-the-project.rst @@ -39,10 +39,9 @@ create files and execute the following commands: This command creates a new directory called ``blog`` that contains a fresh new project based on the most recent stable Symfony version available. -.. tip:: +.. seealso:: - The technical requirements to run Symfony are simple. If you want to check - if your system meets those requirements, read :doc:`/reference/requirements`. + Check out the :ref:`technical requirements for running Symfony applications `. Structuring the Application --------------------------- diff --git a/best_practices/forms.rst b/best_practices/forms.rst index 74b3f70f3de..07596e73e54 100644 --- a/best_practices/forms.rst +++ b/best_practices/forms.rst @@ -212,6 +212,8 @@ Handling a form submit usually follows a similar template:: // render the template } +.. _best-practice-handle-form: + We recommend that you use a single action for both rendering the form and handling the form submit. For example, you *could* have a ``new()`` action that *only* renders the form and a ``create()`` action that *only* processes the form diff --git a/best_practices/security.rst b/best_practices/security.rst index 14b2e31132e..bd68fb21b4d 100644 --- a/best_practices/security.rst +++ b/best_practices/security.rst @@ -39,7 +39,7 @@ remain resistant to brute-force search attacks. .. note:: - :ref:`Argon2i ` is the hashing algorithm as + :ref:`Sodium ` is the hashing algorithm as recommended by industry standards, but this won't be available to you unless you are using PHP 7.2+ or have the `libsodium`_ extension installed. ``bcrypt`` is sufficient for most applications. @@ -376,6 +376,4 @@ via the even easier shortcut in a controller:: Next: :doc:`/best_practices/web-assets` .. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html -.. _`@Security annotation`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/security.html -.. _`FOSUserBundle`: https://github.com/FriendsOfSymfony/FOSUserBundle .. _`libsodium`: https://pecl.php.net/package/libsodium diff --git a/best_practices/templates.rst b/best_practices/templates.rst index a34f3d48b66..6570690dc56 100644 --- a/best_practices/templates.rst +++ b/best_practices/templates.rst @@ -119,4 +119,3 @@ be used as a Twig extension. Next: :doc:`/best_practices/forms` .. _`Twig`: https://twig.symfony.com/ -.. _`Parsedown`: https://parsedown.org/ diff --git a/bundles.rst b/bundles.rst index a16c2870ab6..cdef74b5c87 100644 --- a/bundles.rst +++ b/bundles.rst @@ -37,7 +37,7 @@ file:: .. tip:: - In a default Symfony application that uses :doc:`Symfony Flex `, + In a default Symfony application that uses :ref:`Symfony Flex `, bundles are enabled/disabled automatically for you when installing/removing them, so you don't need to look at or edit this ``bundles.php`` file. diff --git a/bundles/best_practices.rst b/bundles/best_practices.rst index 9223a351e00..dbb307285ce 100644 --- a/bundles/best_practices.rst +++ b/bundles/best_practices.rst @@ -197,12 +197,11 @@ of Symfony and the latest beta release: include: # Minimum supported dependencies with the latest and oldest PHP version - php: 7.2 - env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" SYMFONY_DEPRECATIONS_HELPER="weak_vendors" - - php: 7.0 - env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" SYMFONY_DEPRECATIONS_HELPER="weak_vendors" + env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" SYMFONY_DEPRECATIONS_HELPER="max[self]=0" + - php: 7.1 + env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" SYMFONY_DEPRECATIONS_HELPER="max[self]=0" # Test the latest stable release - - php: 7.0 - php: 7.1 - php: 7.2 env: COVERAGE=true PHPUNIT_FLAGS="-v --coverage-text" @@ -246,7 +245,7 @@ Installation ------------ Bundles should set ``"type": "symfony-bundle"`` in their ``composer.json`` file. -With this, :doc:`Symfony Flex ` will be able to automatically +With this, :ref:`Symfony Flex ` will be able to automatically enable your bundle when it's installed. If your bundle requires any setup (e.g. configuration, new files, changes to @@ -451,8 +450,11 @@ Bundles must be versioned following the `Semantic Versioning Standard`_. Services -------- -If the bundle defines services, they must be prefixed with the bundle alias. -For example, AcmeBlogBundle services must be prefixed with ``acme_blog``. +If the bundle defines services, they must be prefixed with the bundle alias +instead of using fully qualified class names like you do in your project +services. For example, AcmeBlogBundle services must be prefixed with ``acme_blog``. +The reason is that bundles shouldn't rely on features such as service autowiring +or autoconfiguration to not impose an overhead when compiling application services. In addition, services not meant to be used by the application directly, should be :ref:`defined as private `. For public services, @@ -531,4 +533,4 @@ Learn more .. _`choose any license`: https://choosealicense.com/ .. _`valid license identifier`: https://spdx.org/licenses/ .. _`Travis CI`: https://travis-ci.org/ -.. _`Travis Cron`: https://docs.travis-ci.com/user/cron-jobs/ +.. _`Travis cron`: https://docs.travis-ci.com/user/cron-jobs/ diff --git a/bundles/index.rst b/bundles/index.rst index 78dd8c6d4fb..e4af2cd357b 100644 --- a/bundles/index.rst +++ b/bundles/index.rst @@ -7,7 +7,6 @@ Bundles :maxdepth: 2 override - inheritance best_practices configuration extension diff --git a/bundles/inheritance.rst b/bundles/inheritance.rst deleted file mode 100644 index d8ce372adb4..00000000000 --- a/bundles/inheritance.rst +++ /dev/null @@ -1,11 +0,0 @@ -.. index:: - single: Bundle; Inheritance - -How to Use Bundle Inheritance to Override Parts of a Bundle -=========================================================== - -.. caution:: - - Bundle inheritance was removed in Symfony 4.0, but you can - :doc:`override any part of a bundle ` without - using bundle inheritance. diff --git a/cache.rst b/cache.rst index 2de7417fdc0..c841fb6f980 100644 --- a/cache.rst +++ b/cache.rst @@ -8,7 +8,7 @@ Using cache is a great way of making your application run quicker. The Symfony c component is shipped with many adapters to different storages. Every adapter is developed for high performance. -Basic uses of the cache looks like this:: +The following example shows a typical usage of the cache:: use Symfony\Contracts\Cache\ItemInterface; @@ -489,13 +489,13 @@ the same key could be invalidate with one function call:: use Symfony\Contracts\Cache\ItemInterface; $value0 = $pool->get('item_0', function (ItemInterface $item) { - $item->tag(['foo', 'bar']) + $item->tag(['foo', 'bar']); return 'debug'; }); $value1 = $pool->get('item_1', function (ItemInterface $item) { - $item->tag('foo') + $item->tag('foo'); return 'debug'; }); @@ -604,7 +604,7 @@ achieved by specifying the adapter. .. note:: - The interface :class:`Symfony\\Contracts\\Cache\\TagAwareCacheInterface`` is + The interface :class:`Symfony\\Contracts\\Cache\\TagAwareCacheInterface` is autowired to the ``cache.app`` service. Clearing the Cache @@ -623,6 +623,16 @@ The global clearer clears all the cache in every pool. The system cache clearer is used in the ``bin/console cache:clear`` command. The app clearer is the default clearer. +To see all available cache pools: + +.. code-block:: terminal + + $ php bin/console cache:pool:list + +.. versionadded:: 4.3 + + The ``cache:pool:list`` command was introduced in Symfony 4.3. + Clear one pool: .. code-block:: terminal diff --git a/components/asset.rst b/components/asset.rst index 139eb066cd6..625a3ea9f2f 100644 --- a/components/asset.rst +++ b/components/asset.rst @@ -402,5 +402,4 @@ Learn more * :doc:`How to manage CSS and JavaScript assets in Symfony applications ` * :doc:`WebLink component ` to preload assets using HTTP/2. -.. _Packagist: https://packagist.org/packages/symfony/asset .. _`Webpack`: https://webpack.js.org/ diff --git a/components/browser_kit.rst b/components/browser_kit.rst index 7eeec4ace52..1fa554a5c0f 100644 --- a/components/browser_kit.rst +++ b/components/browser_kit.rst @@ -10,9 +10,10 @@ The BrowserKit Component .. note:: - The BrowserKit component can only make internal requests to your application. - If you need to make requests to external sites and applications, consider - using `Goutte`_, a simple web scraper based on Symfony Components. + In Symfony versions prior to 4.3, the BrowserKit component could only make + internal requests to your application. Starting from Symfony 4.3, this + component can also :ref:`make HTTP requests to any public site ` + when using it in combination with the :doc:`HttpClient component `. Installation ------------ @@ -279,6 +280,41 @@ also delete all the cookies:: // reset the client (history and cookies are cleared too) $client->restart(); +.. _component-browserkit-external-requests: + +Making External HTTP Requests +----------------------------- + +So far, all the examples in this article have assumed that you are making +internal requests to your own application. However, you can run the exact same +examples when making HTTP requests to external web sites and applications. + +First, install and configure the :doc:`HttpClient component `. +Then, use the :class:`Symfony\\Component\\BrowserKit\\HttpBrowser` to create +the client that will make the external HTTP requests:: + + use Symfony\Component\BrowserKit\HttpBrowser; + use Symfony\Component\HttpClient\HttpClient; + + $browser = new HttpBrowser(HttpClient::create()); + +You can now use any of the methods shown in this article to extract information, +click links, submit forms, etc. This means that you no longer need to use a +dedicated web crawler or scraper such as `Goutte`_:: + + $browser = new HttpBrowser(HttpClient::create()); + + $browser->request('GET', 'https://github.com'); + $browser->clickLink('Sign in'); + $browser->submitForm('Sign in', ['login' => '...', 'password' => '...']); + $openPullRequests = trim($browser->clickLink('Pull requests')->filter( + '.table-list-header-toggle a:nth-child(1)' + )->text()); + +.. versionadded:: 4.3 + + The feature to make external HTTP requests was introduced in Symfony 4.3. + Learn more ---------- @@ -286,5 +322,4 @@ Learn more * :doc:`/components/css_selector` * :doc:`/components/dom_crawler` -.. _`Packagist`: https://packagist.org/packages/symfony/browser-kit .. _`Goutte`: https://github.com/FriendsOfPHP/Goutte diff --git a/components/cache.rst b/components/cache.rst index 700cfcb8448..d323113e9bd 100644 --- a/components/cache.rst +++ b/components/cache.rst @@ -203,7 +203,5 @@ Advanced Usage .. _`PSR-6`: http://www.php-fig.org/psr/psr-6/ .. _`Cache Contracts`: https://github.com/symfony/contracts/blob/master/Cache/CacheInterface.php -.. _`PSR-16`: http://www.php-fig.org/psr/psr-16/ -.. _Doctrine Cache: https://www.doctrine-project.org/projects/cache.html -.. _Stampede prevention: https://en.wikipedia.org/wiki/Cache_stampede +.. _`Stampede prevention`: https://en.wikipedia.org/wiki/Cache_stampede .. _Probabilistic early expiration: https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration diff --git a/components/cache/adapters/chain_adapter.rst b/components/cache/adapters/chain_adapter.rst index c1ad7630565..de7555029d2 100644 --- a/components/cache/adapters/chain_adapter.rst +++ b/components/cache/adapters/chain_adapter.rst @@ -15,16 +15,15 @@ given adapters. This exposes a simple and efficient method for creating a layere The ChainAdapter must be provided an array of adapters and optionally a maximum cache lifetime as its constructor arguments:: - use Symfony\Component\Cache\Adapter\ApcuAdapter; - - $cache = new ChainAdapter([ + use Symfony\Component\Cache\Adapter\ChainAdapter; + $cache = new ChainAdapter( // The ordered list of adapters used to fetch cached items array $adapters, // The max lifetime of items propagated from lower adapters to upper ones $maxLifetime = 0 - ]); + ); .. note:: @@ -59,11 +58,5 @@ incompatible adapters are silently ignored:: new FilesystemAdapter(), // DOES implement PruneableInterface ]); - // prune will proxy the call to FilesystemAdapter while silently skipping ApcuAdapter + // prune will proxy the call to FilesystemAdapter while silently skip ApcuAdapter $cache->prune(); - -.. note:: - - Since Symfony 3.4, this adapter implements :class:`Symfony\\Component\\Cache\\PruneableInterface`, - allowing for manual :ref:`pruning of expired cache entries ` by - calling its ``prune()`` method. diff --git a/components/cache/adapters/memcached_adapter.rst b/components/cache/adapters/memcached_adapter.rst index 6354aee661c..69d7afa48be 100644 --- a/components/cache/adapters/memcached_adapter.rst +++ b/components/cache/adapters/memcached_adapter.rst @@ -301,4 +301,3 @@ Available Options .. _`Memcached server`: https://memcached.org/ .. _`Memcached`: http://php.net/manual/en/class.memcached.php .. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name -.. _`Domain Name System (DNS)`: https://en.wikipedia.org/wiki/Domain_Name_System diff --git a/components/cache/adapters/redis_adapter.rst b/components/cache/adapters/redis_adapter.rst index 987abb5e38c..53b28c58466 100644 --- a/components/cache/adapters/redis_adapter.rst +++ b/components/cache/adapters/redis_adapter.rst @@ -94,10 +94,22 @@ Below are common examples of valid DSNs showing a combination of available value 'redis:?host[localhost]&host[localhost:6379]&host[/var/run/redis.sock:]&auth=my-password&redis_cluster=1' ); +`Redis Sentinel`_, which provides high availability for Redis, is also supported +when using the Predis library. Use the ``redis_sentinel`` parameter to set the +name of your service group:: + + RedisAdapter::createConnection( + 'redis:?host[redis1:26379]&host[redis2:26379]&host[redis3:26379]&redis_sentinel=mymaster' + ); + .. versionadded:: 4.2 The option to define multiple servers in a single DSN was introduced in Symfony 4.2. +.. versionadded:: 4.4 + + Redis Sentinel support was introduced in Symfony 4.4. + .. note:: See the :class:`Symfony\\Component\\Cache\\Traits\\RedisTrait` for more options @@ -184,3 +196,4 @@ Available Options .. _`Predis`: https://packagist.org/packages/predis/predis .. _`Predis Connection Parameters`: https://github.com/nrk/predis/wiki/Connection-Parameters#list-of-connection-parameters .. _`TCP-keepalive`: https://redis.io/topics/clients#tcp-keepalive +.. _`Redis Sentinel`: https://redis.io/topics/sentinel diff --git a/components/cache/psr6_psr16_adapters.rst b/components/cache/psr6_psr16_adapters.rst index 4dc676e8b9c..3c1e683b278 100644 --- a/components/cache/psr6_psr16_adapters.rst +++ b/components/cache/psr6_psr16_adapters.rst @@ -33,21 +33,23 @@ example:: But, you already have a PSR-16 cache object, and you'd like to pass this to the class instead. No problem! The Cache component provides the -:class:`Symfony\\Component\\Cache\\Adapter\\SimpleCacheAdapter` class for exactly +:class:`Symfony\\Component\\Cache\\Adapter\\Psr16Adapter` class for exactly this use-case:: - use Symfony\Component\Cache\Adapter\SimpleCacheAdapter; - use Symfony\Component\Cache\Simple\FilesystemCache; + use Symfony\Component\Cache\Adapter\Psr16Adapter; - // the PSR-16 cache object that you want to use - $psr16Cache = new FilesystemCache(); + // $psr16Cache is the PSR-16 object that you want to use as a PSR-6 one // a PSR-6 cache that uses your cache internally! - $psr6Cache = new SimpleCacheAdapter($psr16Cache); + $psr6Cache = new Psr16Adapter($psr16Cache); // now use this wherever you want $githubApiClient = new GitHubApiClient($psr6Cache); +.. versionadded:: 4.3 + + The ``Psr16Adapter`` class was introduced in Symfony 4.3. + Using a PSR-6 Cache Object as a PSR-16 Cache -------------------------------------------- @@ -70,19 +72,23 @@ example:: But, you already have a PSR-6 cache pool object, and you'd like to pass this to the class instead. No problem! The Cache component provides the -:class:`Symfony\\Component\\Cache\\Simple\\Psr6Cache` class for exactly +:class:`Symfony\\Component\\Cache\\Psr16Cache` class for exactly this use-case:: use Symfony\Component\Cache\Adapter\FilesystemAdapter; - use Symfony\Component\Cache\Simple\Psr6Cache; + use Symfony\Component\Cache\Psr16Cache; // the PSR-6 cache object that you want to use $psr6Cache = new FilesystemAdapter(); // a PSR-16 cache that uses your cache internally! - $psr16Cache = new Psr6Cache($psr6Cache); + $psr16Cache = new Psr16Cache($psr6Cache); // now use this wherever you want $githubApiClient = new GitHubApiClient($psr16Cache); +.. versionadded:: 4.3 + + The ``Psr16Cache`` class was introduced in Symfony 4.3. + .. _`PSR-16`: http://www.php-fig.org/psr/psr-16/ diff --git a/components/config.rst b/components/config.rst index 4b6ad1be442..c41b1a54a81 100644 --- a/components/config.rst +++ b/components/config.rst @@ -29,5 +29,3 @@ Learn More /bundles/configuration /bundles/extension /bundles/prepend_extension - -.. _Packagist: https://packagist.org/packages/symfony/config diff --git a/components/config/resources.rst b/components/config/resources.rst index 69a097fd095..3ee380a9828 100644 --- a/components/config/resources.rst +++ b/components/config/resources.rst @@ -10,6 +10,8 @@ Loading Resources :phpfunction:`parse_ini_file` function. Therefore, you can only set parameters to string values. To set parameters to other data types (e.g. boolean, integer, etc), the other loaders are recommended. + +Loaders populate the application's configuration from different sources like YAML files. The Config component defines the interface for such loaders. The :doc:`Dependency Injection ` and :doc:`Routing ` components come with specialized loaders for different file formats. Locating Resources ------------------ diff --git a/components/console.rst b/components/console.rst index 34072ce6b3a..6a2abe2366e 100644 --- a/components/console.rst +++ b/components/console.rst @@ -65,5 +65,3 @@ Learn more /components/console/* /components/console/helpers/index /console/* - -.. _Packagist: https://packagist.org/packages/symfony/console diff --git a/components/console/helpers/progressbar.rst b/components/console/helpers/progressbar.rst index 81fc8e4c489..743552f89c9 100644 --- a/components/console/helpers/progressbar.rst +++ b/components/console/helpers/progressbar.rst @@ -95,6 +95,33 @@ that the progress bar display is refreshed with a 100% completion. :method:`Symfony\\Component\\Console\\Helper\\ProgressBar::display` to show the progress bar again. +If the progress information is stored in an iterable variable (such as an array +or a PHP generator) you can use the +:method:`Symfony\\Component\\Console\\Helper\\ProgressBar::iterate` method, +which starts, advances and finishes the progress bar automatically:: + + use Symfony\Component\Console\Helper\ProgressBar; + + $progressBar = new ProgressBar($output); + + // $iterable can be for example an array ([1, 2, 3, ...]) or a generator + // $iterable = function () { yield 1; yield 2; ... }; + foreach ($progressBar->iterate($iterable) as $value) { + // ... do some work + } + +If ``$iterable = [1, 2]``, the previous code will output the following: + +.. code-block:: text + + 0/2 [>---------------------------] 0% + 1/2 [==============>-------------] 50% + 2/2 [============================] 100% + +.. versionadded:: 4.3 + + The ``iterate()`` method was introduced in Symfony 4.3. + Customizing the Progress Bar ---------------------------- diff --git a/components/console/helpers/questionhelper.rst b/components/console/helpers/questionhelper.rst index 83aeabb6506..aeee9dbe30b 100644 --- a/components/console/helpers/questionhelper.rst +++ b/components/console/helpers/questionhelper.rst @@ -179,6 +179,45 @@ will be autocompleted as the user types:: $bundleName = $helper->ask($input, $output, $question); } +In more complex use cases, it may be necessary to generate suggestions on the +fly, for instance if you wish to autocomplete a file path. In that case, you can +provide a callback function to dynamically generate suggestions:: + + use Symfony\Component\Console\Question\Question; + + // ... + public function execute(InputInterface $input, OutputInterface $output) + { + $helper = $this->getHelper('question'); + + // This function is called whenever the input changes and new + // suggestions are needed. + $callback = function (string $userInput): array { + // Strip any characters from the last slash to the end of the string + // to keep only the last directory and generate suggestions for it + $inputPath = preg_replace('%(/|^)[^/]*$%', '$1', $userInput); + $inputPath = '' === $inputPath ? '.' : $inputPath; + + // CAUTION - this example code allows unrestricted access to the + // entire filesystem. In real applications, restrict the directories + // where files and dirs can be found + $foundFilesAndDirs = @scandir($inputPath) ?: []; + + return array_map(function ($dirOrFile) use ($inputPath) { + return $inputPath.$dirOrFile; + }, $foundFilesAndDirs); + }; + + $question = new Question('Please provide the full path of a file to parse'); + $question->setAutocompleterCallback($callback); + + $filePath = $helper->ask($input, $output, $question); + } + +.. versionadded:: 4.3 + + The ``setAutocompleterCallback()`` method was introduced in Symfony 4.3. + Hiding the User's Response ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/components/css_selector.rst b/components/css_selector.rst index 67193589e91..2c15ef432dd 100644 --- a/components/css_selector.rst +++ b/components/css_selector.rst @@ -97,8 +97,6 @@ Several pseudo-classes are not yet supported: ``*:nth-last-of-type``, ``*:only-of-type``. (These work with an element name (e.g. ``li:first-of-type``) but not with ``*``). -.. _Packagist: https://packagist.org/packages/symfony/css-selector - Learn more ---------- diff --git a/components/debug.rst b/components/debug.rst index 015376d9095..6f116e25af6 100644 --- a/components/debug.rst +++ b/components/debug.rst @@ -87,5 +87,3 @@ Using the ``DebugClassLoader`` is done by calling its static use Symfony\Component\Debug\DebugClassLoader; DebugClassLoader::enable(); - -.. _Packagist: https://packagist.org/packages/symfony/debug diff --git a/components/dependency_injection.rst b/components/dependency_injection.rst index db41319af4c..af13905456c 100644 --- a/components/dependency_injection.rst +++ b/components/dependency_injection.rst @@ -311,4 +311,3 @@ Learn More /service_container/* .. _`PSR-11`: http://www.php-fig.org/psr/psr-11/ -.. _Packagist: https://packagist.org/packages/symfony/dependency-injection diff --git a/components/dom_crawler.rst b/components/dom_crawler.rst index 4e99f042a99..82aef8df80d 100644 --- a/components/dom_crawler.rst +++ b/components/dom_crawler.rst @@ -70,6 +70,17 @@ tree. isn't meant to dump content, you can see the "fixed" version of your HTML by :ref:`dumping it `. +.. note:: + + If you need better support for HTML5 contents or want to get rid of the + inconsistencies of PHP's DOM extension, install the `html5-php library`_. + The DomCrawler component will use it automatically when the content has + an HTML5 doctype. + + .. versionadded:: 4.3 + + The automatic support of the html5-php library was introduced in Symfony 4.3. + Node Filtering ~~~~~~~~~~~~~~ @@ -202,6 +213,13 @@ Access the value of the first node of the current selection:: // if the node does not exist, calling to text() will result in an exception $message = $crawler->filterXPath('//body/p')->text(); + // avoid the exception passing an argument that text() returns when node does not exist + $message = $crawler->filterXPath('//body/p')->text('Default text content'); + +.. versionadded:: 4.3 + + The default argument of ``text()`` was introduced in Symfony 4.3. + Access the attribute value of the first node of the current selection:: $class = $crawler->filterXPath('//body/p')->attr('class'); @@ -210,12 +228,17 @@ Extract attribute and/or node values from the list of nodes:: $attributes = $crawler ->filterXpath('//body/p') - ->extract(['_text', 'class']) + ->extract(['_name', '_text', 'class']) ; .. note:: - Special attribute ``_text`` represents a node value. + Special attribute ``_text`` represents a node value, while ``_name`` + represents the element name (the HTML tag name). + + .. versionadded:: 4.3 + + The special attribute ``_name`` was introduced in Symfony 4.3. Call an anonymous function on each node of the list:: @@ -307,6 +330,13 @@ and :phpclass:`DOMNode` objects:: // if the node does not exist, calling to html() will result in an exception $html = $crawler->html(); + // avoid the exception passing an argument that html() returns when node does not exist + $html = $crawler->html('Default HTML content'); + + .. versionadded:: 4.3 + + The default argument of ``html()`` was introduced in Symfony 4.3. + Expression Evaluation ~~~~~~~~~~~~~~~~~~~~~ @@ -369,16 +399,26 @@ This behavior is best illustrated with examples:: Links ~~~~~ -To find a link by name (or a clickable image by its ``alt`` attribute), use -the ``selectLink()`` method on an existing crawler. This returns a ``Crawler`` -instance with just the selected link(s). Calling ``link()`` gives you a special -:class:`Symfony\\Component\\DomCrawler\\Link` object:: +Use the ``filter()`` method to find links by their ``id`` or ``class`` +attributes and use the ``selectLink()`` method to find links by their content +(it also finds clickable images with that content in its ``alt`` attribute). - $linksCrawler = $crawler->selectLink('Go elsewhere...'); - $link = $linksCrawler->link(); +Both methods return a ``Crawler`` instance with just the selected link. Use the +``link()`` method to get the :class:`Symfony\\Component\\DomCrawler\\Link` object +that represents the link:: - // or do this all at once - $link = $crawler->selectLink('Go elsewhere...')->link(); + // first, select the link by id, class or content... + $linkCrawler = $crawler->filter('#sign-up'); + $linkCrawler = $crawler->filter('.user-profile'); + $linkCrawler = $crawler->selectLink('Log in'); + + // ...then, get the Link object: + $link = $linkCrawler->link(); + + // or do all this at once: + $link = $crawler->filter('#sign-up')->link(); + $link = $crawler->filter('.user-profile')->link(); + $link = $crawler->selectLink('Log in')->link(); The :class:`Symfony\\Component\\DomCrawler\\Link` object has several useful methods to get more information about the selected link itself:: @@ -481,6 +521,9 @@ To work with multi-dimensional fields:: + + + Pass an array of values:: @@ -494,6 +537,11 @@ Pass an array of values:: 'dimensional' => 'an other value', ]]); + // tick multiple checkboxes at once + $form->setValues(['multi' => [ + 'dimensional' => [1, 3] // it uses the input value to determine which checkbox to tick + ]]); + This is great, but it gets better! The ``Form`` object allows you to interact with your form like a browser, selecting radio values, ticking checkboxes, and uploading files:: @@ -568,11 +616,11 @@ the whole form or specific field(s):: $form->disableValidation(); $form['country']->select('Invalid value'); -.. _`Goutte`: https://github.com/FriendsOfPHP/Goutte -.. _Packagist: https://packagist.org/packages/symfony/dom-crawler - Learn more ---------- * :doc:`/testing` * :doc:`/components/css_selector` + +.. _`Goutte`: https://github.com/FriendsOfPHP/Goutte +.. _`html5-php library`: https://github.com/Masterminds/html5-php diff --git a/components/dotenv.rst b/components/dotenv.rst index 8fac29c6db9..acd67399a9c 100644 --- a/components/dotenv.rst +++ b/components/dotenv.rst @@ -141,6 +141,14 @@ Use environment variables in values by prefixing variables with ``$``: DB_USER=root DB_PASS=${DB_USER}pass # Include the user as a password prefix +.. note:: + + The order is important when some env var depends on the value of other env + vars. In the above example, ``DB_PASS`` must be defined after ``DB_USER``. + Moreover, if you define multiple ``.env`` files and put ``DB_PASS`` first, + its value will depend on the ``DB_USER`` value defined in other files + instead of the value defined in this file. + Embed commands via ``$()`` (not supported on Windows): .. code-block:: terminal @@ -151,5 +159,4 @@ Embed commands via ``$()`` (not supported on Windows): Note that using ``$()`` might not work depending on your shell. -.. _Packagist: https://packagist.org/packages/symfony/dotenv .. _twelve-factor applications: http://www.12factor.net/ diff --git a/components/event_dispatcher.rst b/components/event_dispatcher.rst index b51679963f8..89853d22941 100644 --- a/components/event_dispatcher.rst +++ b/components/event_dispatcher.rst @@ -111,7 +111,7 @@ Often times, data about a specific event needs to be passed along with the case, a special subclass that has additional methods for retrieving and overriding information can be passed when dispatching an event. For example, the ``kernel.response`` event uses a -:class:`Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent`, which +:class:`Symfony\\Component\\HttpKernel\\Event\\ResponseEvent`, which contains methods to get and even replace the ``Response`` object. The Dispatcher @@ -291,8 +291,8 @@ Dispatch the Event The :method:`Symfony\\Component\\EventDispatcher\\EventDispatcher::dispatch` method notifies all listeners of the given event. It takes two arguments: -the name of the event to dispatch and the ``Event`` instance to pass to -each listener of that event:: +the ``Event`` instance to pass to each listener of that event and the name +of the event to dispatch and :: use Acme\Store\Event\OrderPlacedEvent; use Acme\Store\Order; @@ -303,7 +303,7 @@ each listener of that event:: // creates the OrderPlacedEvent and dispatches it $event = new OrderPlacedEvent($order); - $dispatcher->dispatch(OrderPlacedEvent::NAME, $event); + $dispatcher->dispatch($event, OrderPlacedEvent::NAME); Notice that the special ``OrderPlacedEvent`` object is created and passed to the ``dispatch()`` method. Now, any listener to the ``order.placed`` @@ -334,7 +334,7 @@ Take the following example of a subscriber that subscribes to the use Acme\Store\Event\OrderPlacedEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\FilterResponseEvent; + use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; class StoreSubscriber implements EventSubscriberInterface @@ -350,12 +350,12 @@ Take the following example of a subscriber that subscribes to the ]; } - public function onKernelResponsePre(FilterResponseEvent $event) + public function onKernelResponsePre(ResponseEvent $event) { // ... } - public function onKernelResponsePost(FilterResponseEvent $event) + public function onKernelResponsePost(ResponseEvent $event) { // ... } @@ -423,7 +423,7 @@ It is possible to detect if an event was stopped by using the method which returns a boolean value:: // ... - $dispatcher->dispatch('foo.event', $event); + $dispatcher->dispatch($event, 'foo.event'); if ($event->isPropagationStopped()) { // ... } @@ -441,36 +441,6 @@ name and a reference to itself to the listeners. This can lead to some advanced applications of the ``EventDispatcher`` including dispatching other events inside listeners, chaining events or even lazy loading listeners into the dispatcher object. -.. index:: - single: EventDispatcher; Dispatcher shortcuts - -.. _event_dispatcher-shortcuts: - -Dispatcher Shortcuts -~~~~~~~~~~~~~~~~~~~~ - -If you do not need a custom event object, you can rely on a plain -:class:`Symfony\\Contracts\\EventDispatcher\\Event` object. You do not even -need to pass this to the dispatcher as it will create one by default unless you -specifically pass one:: - - $dispatcher->dispatch('order.placed'); - -Moreover, the event dispatcher always returns whichever event object that -was dispatched, i.e. either the event that was passed or the event that -was created internally by the dispatcher. This allows for nice shortcuts:: - - if (!$dispatcher->dispatch('foo.event')->isPropagationStopped()) { - // ... - } - -Or:: - - $event = new OrderPlacedEvent($order); - $order = $dispatcher->dispatch('bar.event', $event)->getOrder(); - -and so on. - .. index:: single: EventDispatcher; Event name introspection @@ -520,4 +490,3 @@ Learn More .. _Observer: https://en.wikipedia.org/wiki/Observer_pattern .. _Closures: https://php.net/manual/en/functions.anonymous.php .. _PHP callable: https://php.net/manual/en/language.pseudo-types.php#language.types.callback -.. _Packagist: https://packagist.org/packages/symfony/event-dispatcher diff --git a/components/event_dispatcher/generic_event.rst b/components/event_dispatcher/generic_event.rst index 6504ff715b8..1f9be477151 100644 --- a/components/event_dispatcher/generic_event.rst +++ b/components/event_dispatcher/generic_event.rst @@ -53,7 +53,7 @@ Passing a subject:: use Symfony\Component\EventDispatcher\GenericEvent; $event = new GenericEvent($subject); - $dispatcher->dispatch('foo', $event); + $dispatcher->dispatch($event, 'foo'); class FooListener { @@ -74,7 +74,7 @@ access the event arguments:: $subject, ['type' => 'foo', 'counter' => 0] ); - $dispatcher->dispatch('foo', $event); + $dispatcher->dispatch($event, 'foo'); class FooListener { @@ -93,7 +93,7 @@ Filtering data:: use Symfony\Component\EventDispatcher\GenericEvent; $event = new GenericEvent($subject, ['data' => 'Foo']); - $dispatcher->dispatch('foo', $event); + $dispatcher->dispatch($event, 'foo'); class FooListener { diff --git a/components/event_dispatcher/traceable_dispatcher.rst b/components/event_dispatcher/traceable_dispatcher.rst index 57f05ba6e0d..87d58023445 100644 --- a/components/event_dispatcher/traceable_dispatcher.rst +++ b/components/event_dispatcher/traceable_dispatcher.rst @@ -38,7 +38,7 @@ to register event listeners and dispatch events:: // dispatches an event $event = ...; - $traceableEventDispatcher->dispatch('event.the_name', $event); + $traceableEventDispatcher->dispatch($event, 'event.the_name'); After your application has been processed, you can use the :method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcherInterface::getCalledListeners` diff --git a/components/expression_language.rst b/components/expression_language.rst index a421f59e7f5..7e955a8b891 100644 --- a/components/expression_language.rst +++ b/components/expression_language.rst @@ -129,5 +129,3 @@ Learn More /components/expression_language/* /service_container/expression_language /reference/constraints/Expression - -.. _Packagist: https://packagist.org/packages/symfony/expression-language diff --git a/components/filesystem.rst b/components/filesystem.rst index 03ec34f6105..10406ce68c9 100644 --- a/components/filesystem.rst +++ b/components/filesystem.rst @@ -321,5 +321,4 @@ Whenever something wrong happens, an exception implementing An :class:`Symfony\\Component\\Filesystem\\Exception\\IOException` is thrown if directory creation fails. -.. _`Packagist`: https://packagist.org/packages/symfony/filesystem .. _`umask`: https://en.wikipedia.org/wiki/Umask diff --git a/components/finder.rst b/components/finder.rst index 9d2816b4a57..2201150ffa1 100644 --- a/components/finder.rst +++ b/components/finder.rst @@ -143,6 +143,17 @@ default when looking for files and directories, but you can change this with the $finder->ignoreVCS(false); +If the search directory contains a ``.gitignore`` file, you can reuse those +rules to exclude files and directories from the results with the +:method:`Symfony\\Component\\Finder\\Finder::ignoreVCSIgnored` method:: + + // excludes files/directories matching the .gitignore patterns + $finder->ignoreVCSIgnored(true); + +.. versionadded:: 4.3 + + The ``ignoreVCSIgnored()`` method was introduced in Symfony 4.3. + File Name ~~~~~~~~~ @@ -401,5 +412,4 @@ The contents of returned files can be read with .. _`PHP wrapper for URL-style protocols`: https://php.net/manual/en/wrappers.php .. _`PHP streams`: https://php.net/streams .. _`IEC standard`: https://physics.nist.gov/cuu/Units/binary.html -.. _`Packagist`: https://packagist.org/packages/symfony/finder .. _`natural sort order`: https://en.wikipedia.org/wiki/Natural_sort_order diff --git a/components/form.rst b/components/form.rst index 60b4abf874c..bcfce8939d7 100644 --- a/components/form.rst +++ b/components/form.rst @@ -80,8 +80,8 @@ object to read data off of the correct PHP superglobals (i.e. ``$_POST`` or .. seealso:: If you need more control over exactly when your form is submitted or which - data is passed to it, you can use the :method:`Symfony\\Component\\Form\\FormInterface::submit` - for this. Read more about it :ref:`form-call-submit-directly`. + data is passed to it, + :doc:`use the submit() method to handle form submissions `. .. sidebar:: Integration with the HttpFoundation Component @@ -775,6 +775,5 @@ Learn more /form/* -.. _Packagist: https://packagist.org/packages/symfony/form .. _Twig: https://twig.symfony.com .. _`Twig Configuration`: https://twig.symfony.com/doc/2.x/intro.html diff --git a/components/http_client.rst b/components/http_client.rst new file mode 100644 index 00000000000..9b62301ba39 --- /dev/null +++ b/components/http_client.rst @@ -0,0 +1,838 @@ +.. index:: + single: HttpClient + single: Components; HttpClient + +The HttpClient Component +======================== + + The HttpClient component is a low-level HTTP client with support for both + PHP stream wrappers and cURL. It provides utilities to consume APIs and + supports synchronous and asynchronous operations. + +.. versionadded:: 4.3 + + The HttpClient component was introduced in Symfony 4.3 and it's still + considered an :doc:`experimental feature `. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/http-client + +.. include:: /components/require_autoload.rst.inc + +Basic Usage +----------- + +Use the :class:`Symfony\\Component\\HttpClient\\HttpClient` class to create the +low-level HTTP client that makes requests, like the following ``GET`` request:: + + use Symfony\Component\HttpClient\HttpClient; + + $client = HttpClient::create(); + $response = $client->request('GET', 'https://api.github.com/repos/symfony/symfony-docs'); + + $statusCode = $response->getStatusCode(); + // $statusCode = 200 + $contentType = $response->getHeaders()['content-type'][0]; + // $contentType = 'application/json' + $content = $response->getContent(); + // $content = '{"id":521583, "name":"symfony-docs", ...}' + $content = $response->toArray(); + // $content = ['id' => 521583, 'name' => 'symfony-docs', ...] + +Performance +----------- + +The component is built for maximum HTTP performance. By design, it is compatible +with HTTP/2 and with doing concurrent asynchronous streamed and multiplexed +requests/responses. Even when doing regular synchronous calls, this design +allows keeping connections to remote hosts open between requests, improving +performance by saving repetitive DNS resolution, SSL negotiation, etc. +To leverage all these design benefits, the cURL extension is needed. + +Enabling cURL Support +~~~~~~~~~~~~~~~~~~~~~ + +This component supports both the native PHP streams and cURL to make the HTTP +requests. Although both are interchangeable and provide the same features, +including concurrent requests, HTTP/2 is only supported when using cURL. + +``HttpClient::create()`` selects the cURL transport if the `cURL PHP extension`_ +is enabled and falls back to PHP streams otherwise. If you prefer to select +the transport explicitly, use the following classes to create the client:: + + use Symfony\Component\HttpClient\CurlHttpClient; + use Symfony\Component\HttpClient\NativeHttpClient; + + // uses native PHP streams + $client = new NativeHttpClient(); + + // uses the cURL PHP extension + $client = new CurlHttpClient(); + +When using this component in a full-stack Symfony application, this behavior is +not configurable and cURL will be used automatically if the cURL PHP extension +is installed and enabled. Otherwise, the native PHP streams will be used. + +HTTP/2 Support +~~~~~~~~~~~~~~ + +When requesting an ``https`` URL, HTTP/2 is enabled by default if libcurl >= 7.36 +is used. To force HTTP/2 for ``http`` URLs, you need to enable it explicitly via +the ``http_version`` option:: + + $client = HttpClient::create(['http_version' => '2.0']); + +Support for HTTP/2 PUSH works out of the box when libcurl >= 7.61 is used with +PHP >= 7.2.17 / 7.3.4: pushed responses are put into a temporary cache and are +used when a subsequent request is triggered for the corresponding URLs. + +Making Requests +--------------- + +The client created with the ``HttpClient`` class provides a single ``request()`` +method to perform all kinds of HTTP requests:: + + $response = $client->request('GET', 'https://...'); + $response = $client->request('POST', 'https://...'); + $response = $client->request('PUT', 'https://...'); + // ... + +Responses are always asynchronous, so that the call to the method returns +immediately instead of waiting to receive the response:: + + // code execution continues immediately; it doesn't wait to receive the response + $response = $client->request('GET', 'http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso'); + + // getting the response headers waits until they arrive + $contentType = $response->getHeaders()['content-type'][0]; + + // trying to get the response contents will block the execution until + // the full response contents are received + $contents = $response->getContent(); + +This component also supports :ref:`streaming responses ` +for full asynchronous applications. + +.. note:: + + HTTP compression and chunked transfer encoding are automatically enabled when + both your PHP runtime and the remote server support them. + +Authentication +~~~~~~~~~~~~~~ + +The HTTP client supports different authentication mechanisms. They can be +defined globally when creating the client (to apply it to all requests) and to +each request (which overrides any global authentication):: + + // Use the same authentication for all requests + $client = HttpClient::create([ + // HTTP Basic authentication with only the username and not a password + 'auth_basic' => ['the-username'], + + // HTTP Basic authentication with a username and a password + 'auth_basic' => ['the-username', 'the-password'], + + // HTTP Bearer authentication (also called token authentication) + 'auth_bearer' => 'the-bearer-token', + ]); + + $response = $client->request('GET', 'https://...', [ + // use a different HTTP Basic authentication only for this request + 'auth_basic' => ['the-username', 'the-password'], + + // ... + ]); + +Query String Parameters +~~~~~~~~~~~~~~~~~~~~~~~ + +You can either append them manually to the requested URL, or define them as an +associative array via the ``query`` option, that will be merged with the URL:: + + // it makes an HTTP GET request to https://httpbin.org/get?token=...&name=... + $response = $client->request('GET', 'https://httpbin.org/get', [ + // these values are automatically encoded before including them in the URL + 'query' => [ + 'token' => '...', + 'name' => '...', + ], + ]); + +Headers +~~~~~~~ + +Use the ``headers`` option to define both the default headers added to all +requests and the specific headers for each request:: + + // this header is added to all requests made by this client + $client = HttpClient::create(['headers' => [ + 'User-Agent' => 'My Fancy App', + ]]); + + // this header is only included in this request and overrides the value + // of the same header if defined globally by the HTTP client + $response = $client->request('POST', 'https://...', [ + 'headers' => [ + 'Content-Type' => 'text/plain', + ], + ]); + +Uploading Data +~~~~~~~~~~~~~~ + +This component provides several methods for uploading data using the ``body`` +option. You can use regular strings, closures, iterables and resources and they'll be +processed automatically when making the requests:: + + $response = $client->request('POST', 'https://...', [ + // defining data using a regular string + 'body' => 'raw data', + + // defining data using an array of parameters + 'body' => ['parameter1' => 'value1', '...'], + + // using a closure to generate the uploaded data + 'body' => function (int $size): string { + // ... + }, + + // using a resource to get the data from it + 'body' => fopen('/path/to/file', 'r'), + ]); + +When uploading data with the ``POST`` method, if you don't define the +``Content-Type`` HTTP header explicitly, Symfony assumes that you're uploading +form data and adds the required +``'Content-Type: application/x-www-form-urlencoded'`` header for you. + +When the ``body`` option is set as a closure, it will be called several times until +it returns the empty string, which signals the end of the body. Each time, the +closure should return a string smaller than the amount requested as argument. + +A generator or any ``Traversable`` can also be used instead of a closure. + +.. tip:: + + When uploading JSON payloads, use the ``json`` option instead of ``body``. The + given content will be JSON-encoded automatically and the request will add the + ``Content-Type: application/json`` automatically too:: + + $response = $client->request('POST', 'https://...', [ + 'json' => ['param1' => 'value1', '...'], + ]); + + $decodedPayload = $response->toArray(); + +To submit a form with file uploads, it is your responsibility to encode the body +according to the ``multipart/form-data`` content-type. The +:doc:`Symfony Mime ` component makes it a few lines of code:: + + use Symfony\Component\Mime\Part\DataPart; + use Symfony\Component\Mime\Part\Multipart\FormDataPart; + + $formFields = [ + 'regular_field' => 'some value', + 'file_field' => DataPart::fromPath('/path/to/uploaded/file'), + ]; + $formData = new FormDataPart($formFields); + $client->request('POST', 'https://...', [ + 'headers' => $formData->getPreparedHeaders()->toArray(), + 'body' => $formData->bodyToIterable(), + ]); + +Cookies +~~~~~~~ + +The HTTP client provided by this component is stateless but handling cookies +requires a stateful storage (because responses can update cookies and they must +be used for subsequent requests). That's why this component doesn't handle +cookies automatically. + +You can either handle cookies yourself using the ``Cookie`` HTTP header or use +the :doc:`BrowserKit component ` which provides this +feature and integrates seamlessly with the HttpClient component. + +Redirects +~~~~~~~~~ + +By default, the HTTP client follows redirects, up to a maximum of 20, when +making a request. Use the ``max_redirects`` setting to configure this behavior +(if the number of redirects is higher than the configured value, you'll get a +:class:`Symfony\\Component\\HttpClient\\Exception\\RedirectionException`):: + + $response = $client->request('GET', 'https://...', [ + // 0 means to not follow any redirect + 'max_redirects' => 0, + ]); + +HTTP Proxies +~~~~~~~~~~~~ + +By default, this component honors the standard environment variables that your +Operating System defines to direct the HTTP traffic through your local proxy. +This means there is usually nothing to configure to have the client work with +proxies, provided these env vars are properly configured. + +You can still set or override these settings using the ``proxy`` and ``no_proxy`` +options: + +* ``proxy`` should be set to the ``http://...`` URL of the proxy to get through + +* ``no_proxy`` disables the proxy for a comma-separated list of hosts that do not + require it to get reached. + +Progress Callback +~~~~~~~~~~~~~~~~~ + +By providing a callable to the ``on_progress`` option, one can track +uploads/downloads as they complete. This callback is guaranteed to be called on +DNS resolution, on arrival of headers and on completion; additionally it is +called when new data is uploaded or downloaded and at least once per second:: + + $response = $client->request('GET', 'https://...', [ + 'on_progress' => function (int $dlNow, int $dlSize, array $info): void { + // $dlNow is the number of bytes downloaded so far + // $dlSize is the total size to be downloaded or -1 if it is unknown + // $info is what $response->getInfo() would return at this very time + }, + ]); + +Any exceptions thrown from the callback will be wrapped in an instance of +``TransportExceptionInterface`` and will abort the request. + +Advanced Options +~~~~~~~~~~~~~~~~ + +The :class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface` defines all the +options you might need to take full control of the way the request is performed, +including DNS pre-resolution, SSL parameters, public key pinning, etc. + +Processing Responses +-------------------- + +The response returned by all HTTP clients is an object of type +:class:`Symfony\\Contracts\\HttpClient\\ResponseInterface` which provides the +following methods:: + + $response = $client->request('GET', 'https://...'); + + // gets the HTTP status code of the response + $statusCode = $response->getStatusCode(); + + // gets the HTTP headers as string[][] with the header names lower-cased + $headers = $response->getHeaders(); + + // gets the response body as a string + $content = $response->getContent(); + + // casts the response JSON contents to a PHP array + $content = $response->toArray(); + + // cancels the request/response + $response->cancel(); + + // returns info coming from the transport layer, such as "response_headers", + // "redirect_count", "start_time", "redirect_url", etc. + $httpInfo = $response->getInfo(); + // you can get individual info too + $startTime = $response->getInfo('start_time'); + + // returns detailed logs about the requests and responses of the HTTP transaction + $httpLogs = $response->getInfo('debug'); + +.. note:: + + ``$response->getInfo()`` is non-blocking: it returns *live* information + about the response. Some of them might not be known yet (e.g. ``http_code``) + when you'll call it. + +.. _http-client-streaming-responses: + +Streaming Responses +~~~~~~~~~~~~~~~~~~~ + +Call the ``stream()`` method of the HTTP client to get *chunks* of the +response sequentially instead of waiting for the entire response:: + + $url = 'https://releases.ubuntu.com/18.04.1/ubuntu-18.04.1-desktop-amd64.iso'; + $response = $client->request('GET', $url, [ + // optional: if you don't want to buffer the response in memory + 'buffer' => false, + ]); + + // Responses are lazy: this code is executed as soon as headers are received + if (200 !== $response->getStatusCode()) { + throw new \Exception('...'); + } + + // get the response contents in chunk and save them in a file + // response chunks implement Symfony\Contracts\HttpClient\ChunkInterface + $fileHandler = fopen('/ubuntu.iso', 'w'); + foreach ($client->stream($response) as $chunk) { + fwrite($fileHandler, $chunk->getContent()); + } + +Canceling Responses +~~~~~~~~~~~~~~~~~~~ + +To abort a request (e.g. because it didn't complete in due time, or you want to +fetch only the first bytes of the response, etc.), you can either use the +``cancel()`` method of ``ResponseInterface``:: + + $response->cancel() + +Or throw an exception from a progress callback:: + + $response = $client->request('GET', 'https://...', [ + 'on_progress' => function (int $dlNow, int $dlSize, array $info): void { + // ... + + throw new \MyException(); + }, + ]); + +The exception will be wrapped in an instance of ``TransportExceptionInterface`` +and will abort the request. + +Handling Exceptions +~~~~~~~~~~~~~~~~~~~ + +When the HTTP status code of the response is in the 300-599 range (i.e. 3xx, +4xx or 5xx) your code is expected to handle it. If you don't do that, the +``getHeaders()`` and ``getContent()`` methods throw an appropriate exception:: + + // the response of this request will be a 403 HTTP error + $response = $client->request('GET', 'https://httpbin.org/status/403'); + + // this code results in a Symfony\Component\HttpClient\Exception\ClientException + // because it doesn't check the status code of the response + $content = $response->getContent(); + + // pass FALSE as the optional argument to not throw an exception and return + // instead the original response content (even if it's an error message) + $content = $response->getContent(false); + +Concurrent Requests +------------------- + +Thanks to responses being lazy, requests are always managed concurrently. +On a fast enough network, the following code makes 379 requests in less than +half a second when cURL is used:: + + use Symfony\Component\HttpClient\CurlHttpClient; + + $client = new CurlHttpClient(); + + $responses = []; + + for ($i = 0; $i < 379; ++$i) { + $uri = "https://http2.akamai.com/demo/tile-$i.png"; + $responses[] = $client->request('GET', $uri); + } + + foreach ($responses as $response) { + $content = $response->getContent(); + // ... + } + +As you can read in the first "for" loop, requests are issued but are not consumed +yet. That's the trick when concurrency is desired: requests should be sent +first and be read later on. This will allow the client to monitor all pending +requests while your code waits for a specific one, as done in each iteration of +the above "foreach" loop. + +Multiplexing Responses +~~~~~~~~~~~~~~~~~~~~~~ + +If you look again at the snippet above, responses are read in requests' order. +But maybe the 2nd response came back before the 1st? Fully asynchronous operations +require being able to deal with the responses in whatever order they come back. + +In order to do so, the ``stream()`` method of HTTP clients accepts a list of +responses to monitor. As mentioned :ref:`previously `, +this method yields response chunks as they arrive from the network. By replacing +the "foreach" in the snippet with this one, the code becomes fully async:: + + foreach ($client->stream($responses) as $response => $chunk) { + if ($chunk->isFirst()) { + // headers of $response just arrived + // $response->getHeaders() is now a non-blocking call + } elseif ($chunk->isLast()) { + // the full content of $response just completed + // $response->getContent() is now a non-blocking call + } else { + // $chunk->getContent() will return a piece + // of the response body that just arrived + } + } + +.. tip:: + + Use the ``user_data`` option combined with ``$response->getInfo('user_data')`` + to track the identity of the responses in your foreach loops. + +Dealing with Network Timeouts +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This component allows dealing with both request and response timeouts. + +A timeout can happen when e.g. DNS resolution takes too much time, when the TCP +connection cannot be opened in the given time budget, or when the response +content pauses for too long. This can be configured with the ``timeout`` request +option:: + + // A TransportExceptionInterface will be issued if nothing + // happens for 2.5 seconds when accessing from the $response + $response = $client->request('GET', 'https://...', ['timeout' => 2.5]); + +The ``default_socket_timeout`` PHP ini setting is used if the option is not set. + +The option can be overridden by using the 2nd argument of the ``stream()`` method. +This allows monitoring several responses at once and applying the timeout to all +of them in a group. If all responses become inactive for the given duration, the +method will yield a special chunk whose ``isTimeout()`` will return ``true``:: + + foreach ($client->stream($responses, 1.5) as $response => $chunk) { + if ($chunk->isTimeout()) { + // $response staled for more than 1.5 seconds + } + } + +A timeout is not necessarily an error: you can decide to stream again the +response and get remaining contents that might come back in a new timeout, etc. + +.. tip:: + + Passing ``0`` as timeout allows monitoring responses in a non-blocking way. + +.. note:: + + Timeouts control how long one is willing to wait *while the HTTP transaction + is idle*. Big responses can last as long as needed to complete, provided they + remain active during the transfer and never pause for longer than specified. + +Dealing with Network Errors +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Network errors (broken pipe, failed DNS resolution, etc.) are thrown as instances +of :class:`Symfony\\Contracts\\HttpClient\\Exception\\TransportExceptionInterface`. + +First of all, you don't *have* to deal with them: letting errors bubble to your +generic exception-handling stack might be really fine in most use cases. + +If you want to handle them, here is what you need to know: + +To catch errors, you need to wrap calls to ``$client->request()`` but also calls +to any methods of the returned responses. This is because responses are lazy, so +that network errors can happen when calling e.g. ``getStatusCode()`` too:: + + try { + // both lines can potentially throw + $response = $client->request(...); + $headers = $response->getHeaders(); + // ... + } catch (TransportExceptionInterface $e) { + // ... + } + +.. note:: + + Because ``$response->getInfo()`` is non-blocking, it shouldn't throw by design. + +When multiplexing responses, you can deal with errors for individual streams by +catching ``TransportExceptionInterface`` in the foreach loop:: + + foreach ($client->stream($responses) as $response => $chunk) { + try { + if ($chunk->isLast()) { + // ... do something with $response + } + } catch (TransportExceptionInterface $e) { + // ... + } + } + +Caching Requests and Responses +------------------------------ + +This component provides a :class:`Symfony\\Component\\HttpClient\\CachingHttpClient` +decorator that allows caching responses and serving them from the local storage +for next requests. The implementation leverages the +:class:`Symfony\\Component\\HttpKernel\\HttpCache\\HttpCache` class under the hood +so that the :doc:`HttpKernel component ` needs to be +installed in your application:: + + use Symfony\Component\HttpClient\CachingHttpClient; + use Symfony\Component\HttpClient\HttpClient; + use Symfony\Component\HttpKernel\HttpCache\Store; + + $store = new Store('/path/to/cache/storage/'); + $client = HttpClient::create(); + $client = new CachingHttpClient($client, $store); + + // this won't hit the network if the resource is already in the cache + $response = $client->request('GET', 'https://example.com/cacheable-resource'); + +``CachingHttpClient`` accepts a third argument to set the options of the ``HttpCache``. + +Scoping Client +-------------- + +It's common that some of the HTTP client options depend on the URL of the +request (e.g. you must set some headers when making requests to GitHub API but +not for other hosts). If that's your case, this component provides a special +HTTP client via the :class:`Symfony\\Component\\HttpClient\\ScopingHttpClient` +class to autoconfigure the HTTP client based on the requested URL:: + + use Symfony\Component\HttpClient\HttpClient; + use Symfony\Component\HttpClient\ScopingHttpClient; + + $client = HttpClient::create(); + $client = new ScopingHttpClient($client, [ + // the options defined as values apply only to the URLs matching + // the regular expressions defined as keys + 'https://api\.github\.com/' => [ + 'headers' => [ + 'Accept' => 'application/vnd.github.v3+json', + 'Authorization' => 'token '.$githubToken, + ], + ], + // ... + ]); + +You can define several scopes, so that each set of options is added only if a +requested URL matches one of the regular expressions provided as keys. + +If the request URL is relative (because you use the ``base_uri`` option), the +scoping HTTP client can't make a match. That's why you can define a third +optional argument in its constructor which will be considered the default +regular expression applied to relative URLs:: + + // ... + + $client = new ScopingHttpClient($client, + [ + 'https://api\.github\.com/' => [ + 'base_uri' => 'https://api.github.com/', + // ... + ], + ], + // this is the index in the previous array that defines + // the base URI that shoud be used to resolve relative URLs + 'https://api\.github\.com/' + ); + +The above example can be reduced to a simpler call:: + + // ... + + $client = ScopingHttpClient::forBaseUri($client, 'https://api.github.com/', [ + // ... + ]); + +This way, the provided options will be used only if the requested URL is relative +or if it matches the ``https://api.github.com/`` base URI. + +Interoperability +---------------- + +The component is interoperable with two different abstractions for HTTP clients: +`Symfony Contracts`_ and `PSR-18`_. If your application uses libraries that need +any of them, the component is compatible with both. They also benefit from +:ref:`autowiring aliases ` when the +:ref:`framework bundle ` is used. + +If you are writing or maintaining a library that makes HTTP requests, you can +decouple it from any specific HTTP client implementations by coding against +either Symfony Contracts (recommended) or PSR-18. + +Symfony Contracts +~~~~~~~~~~~~~~~~~ + +The interfaces found in the ``symfony/http-client-contracts`` package define +the primary abstractions implemented by the component. Its entry point is the +:class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface`. That's the +interface you need to code against when a client is needed:: + + use Symfony\Contracts\HttpClient\HttpClientInterface; + + class MyApiLayer + { + private $client; + + public function __construct(HttpClientInterface $client) + { + $this->client = $client + } + + // [...] + } + +All request options mentioned above (e.g. timeout management) are also defined +in the wordings of the interface, so that any compliant implementations (like +this component) is guaranteed to provide them. That's a major difference with +the PSR-18 abstraction, which provides none related to the transport itself. + +Another major feature covered by the Symfony Contracts is async/multiplexing, +as described in the previous sections. + +PSR-18 +~~~~~~ + +This component implements the `PSR-18`_ (HTTP Client) specifications via the +:class:`Symfony\\Component\\HttpClient\\Psr18Client` class, which is an adapter +to turn a Symfony ``HttpClientInterface`` into a PSR-18 ``ClientInterface``. + +To use it, you need the ``psr/http-client`` package and a `PSR-17`_ implementation: + +.. code-block:: terminal + + # installs the PSR-18 ClientInterface + $ composer require psr/http-client + + # installs an efficient implementation of response and stream factories + # with autowiring aliases provided by Symfony Flex + $ composer require nyholm/psr7 + +Now you can make HTTP requests with the PSR-18 client as follows:: + + use Nyholm\Psr7\Factory\Psr17Factory; + use Symfony\Component\HttpClient\Psr18Client; + + $psr17Factory = new Psr17Factory(); + $psr18Client = new Psr18Client(); + + $url = 'https://symfony.com/versions.json'; + $request = $psr17Factory->createRequest('GET', $url); + $response = $psr18Client->sendRequest($request); + + $content = json_decode($response->getBody()->getContents(), true); + +Symfony Framework Integration +----------------------------- + +When using this component in a full-stack Symfony application, you can configure +multiple clients with different configurations and inject them into your services. + +Configuration +~~~~~~~~~~~~~ + +Use the ``framework.http_client`` key to configure the default HTTP client used +in the application. Check out the full +:ref:`http_client config reference ` to learn about all +the available config options: + +.. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + http_client: + max_host_connections: 10 + default_options: + max_redirects: 7 + +If you want to define multiple HTTP clients, use this other expanded configuration: + +.. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + http_client: + scoped_clients: + crawler.client: + headers: { 'X-Powered-By': 'ACME App' } + http_version: '1.0' + some_api.client: + max_redirects: 5 + +Injecting the HTTP Client into Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your application only needs one HTTP client, you can inject the default one +into any services by type-hinting a constructor argument with the +:class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface`:: + + use Symfony\Contracts\HttpClient\HttpClientInterface; + + class SomeService + { + private $client; + + public function __construct(HttpClientInterface $client) + { + $this->client = $client; + } + } + +If you have several clients, you must use any of the methods defined by Symfony +to :ref:`choose a specific service `. Each client +has a unique service named after its configuration. + +Each scoped client also defines a corresponding named autowiring alias. +If you use for example +``Symfony\Contracts\HttpClient\HttpClientInterface $myApiClient`` +as the type and name of an argument, autowiring will inject the ``my_api.client`` +service into your autowired classes. + +Testing HTTP Clients and Responses +---------------------------------- + +This component includes the ``MockHttpClient`` and ``MockResponse`` classes to +use them in tests that need an HTTP client which doesn't make actual HTTP +requests. + +The first way of using ``MockHttpClient`` is to pass a list of responses to its +constructor. These will be yielded in order when requests are made:: + + use Symfony\Component\HttpClient\MockHttpClient; + use Symfony\Component\HttpClient\Response\MockResponse; + + $responses = [ + new MockResponse($body1, $info1), + new MockResponse($body2, $info2), + ]; + + $client = new MockHttpClient($responses); + // responses are returned in the same order as passed to MockHttpClient + $response1 = $client->request('...'); // returns $responses[0] + $response2 = $client->request('...'); // returns $responses[1] + +Another way of using ``MockHttpClient`` is to pass a callback that generates the +responses dynamically when it's called:: + + use Symfony\Component\HttpClient\MockHttpClient; + use Symfony\Component\HttpClient\Response\MockResponse; + + $callback = function ($method, $url, $options) { + return new MockResponse('...'); + }; + + $client = new MockHttpClient($callback); + $response = $client->request('...'); // calls $callback to get the response + +The responses provided to the mock client don't have to be instances of +``MockResponse``. Any class implementing ``ResponseInterface`` will work (e.g. +``$this->createMock(ResponseInterface::class)``). + +However, using ``MockResponse`` allows simulating chunked responses and timeouts:: + + $body = function () { + yield 'hello'; + // empty strings are turned into timeouts so that they are easy to test + yield ''; + yield 'world'; + }; + + $mockResponse = new MockResponse($body()); + +.. _`cURL PHP extension`: https://php.net/curl +.. _`PSR-17`: https://www.php-fig.org/psr/psr-17/ +.. _`PSR-18`: https://www.php-fig.org/psr/psr-18/ +.. _`Symfony Contracts`: https://github.com/symfony/contracts diff --git a/components/http_foundation.rst b/components/http_foundation.rst index cc92f3e7c81..5db716ecdf9 100644 --- a/components/http_foundation.rst +++ b/components/http_foundation.rst @@ -57,6 +57,8 @@ which is almost equivalent to the more verbose, but also more flexible, $_SERVER ); +.. _accessing-request-data: + Accessing Request Data ~~~~~~~~~~~~~~~~~~~~~~ @@ -698,7 +700,6 @@ Learn More /session/* /http_cache/* -.. _Packagist: https://packagist.org/packages/symfony/http-foundation .. _Nginx: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/ .. _Apache: https://tn123.org/mod_xsendfile/ .. _`JSON Hijacking`: http://haacked.com/archive/2009/06/25/json-hijacking.aspx diff --git a/components/http_kernel.rst b/components/http_kernel.rst index 469690c0a26..4c320d12dbf 100644 --- a/components/http_kernel.rst +++ b/components/http_kernel.rst @@ -292,7 +292,7 @@ have been determined (e.g. the controller, routing information) but before the controller is executed. For some examples, see the Symfony section below. Listeners to this event can also change the controller callable completely -by calling :method:`FilterControllerEvent::setController ` +by calling :method:`ControllerEvent::setController ` on the event object that's passed to listeners on this event. .. sidebar:: ``kernel.controller`` in the Symfony Framework @@ -524,9 +524,9 @@ to the exception. -Each listener to this event is passed a :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent` +Each listener to this event is passed a :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` object, which you can use to access the original exception via the -:method:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent::getException` +:method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::getException` method. A typical listener on this event will check for a certain type of exception and create an appropriate error ``Response``. @@ -602,18 +602,31 @@ each event has their own event object: .. _component-http-kernel-event-table: -=========================== ====================================== =================================================================================== +=========================== ====================================== ======================================================================== Name ``KernelEvents`` Constant Argument passed to the listener -=========================== ====================================== =================================================================================== -kernel.request ``KernelEvents::REQUEST`` :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent` -kernel.controller ``KernelEvents::CONTROLLER`` :class:`Symfony\\Component\\HttpKernel\\Event\\FilterControllerEvent` -kernel.controller_arguments ``KernelEvents::CONTROLLER_ARGUMENTS`` :class:`Symfony\\Component\\HttpKernel\\Event\\FilterControllerArgumentsEvent` -kernel.view ``KernelEvents::VIEW`` :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForControllerResultEvent` -kernel.response ``KernelEvents::RESPONSE`` :class:`Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent` +=========================== ====================================== ======================================================================== +kernel.request ``KernelEvents::REQUEST`` :class:`Symfony\\Component\\HttpKernel\\Event\\RequestEvent` +kernel.controller ``KernelEvents::CONTROLLER`` :class:`Symfony\\Component\\HttpKernel\\Event\\ControllerEvent` +kernel.controller_arguments ``KernelEvents::CONTROLLER_ARGUMENTS`` :class:`Symfony\\Component\\HttpKernel\\Event\\ControllerArgumentsEvent` +kernel.view ``KernelEvents::VIEW`` :class:`Symfony\\Component\\HttpKernel\\Event\\ViewEvent` +kernel.response ``KernelEvents::RESPONSE`` :class:`Symfony\\Component\\HttpKernel\\Event\\ResponseEvent` kernel.finish_request ``KernelEvents::FINISH_REQUEST`` :class:`Symfony\\Component\\HttpKernel\\Event\\FinishRequestEvent` -kernel.terminate ``KernelEvents::TERMINATE`` :class:`Symfony\\Component\\HttpKernel\\Event\\PostResponseEvent` -kernel.exception ``KernelEvents::EXCEPTION`` :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent` -=========================== ====================================== =================================================================================== +kernel.terminate ``KernelEvents::TERMINATE`` :class:`Symfony\\Component\\HttpKernel\\Event\\TerminateEvent` +kernel.exception ``KernelEvents::EXCEPTION`` :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` +=========================== ====================================== ======================================================================== + +.. deprecated:: 4.3 + + Since Symfony 4.3, most of the event classes were renamed. + The following old classes were deprecated: + + * `GetResponseEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\RequestEvent` + * `FilterControllerEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\ControllerEvent` + * `FilterControllerArgumentsEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\ControllerArgumentsEvent` + * `GetResponseForControllerResultEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\ViewEvent` + * `FilterResponseEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\ResponseEvent` + * `PostResponseEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\TerminateEvent` + * `GetResponseForExceptionEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` .. _http-kernel-working-example: @@ -709,10 +722,10 @@ can be used to check if the current request is a "master" or "sub" request. For example, a listener that only needs to act on the master request may look like this:: - use Symfony\Component\HttpKernel\Event\GetResponseEvent; + use Symfony\Component\HttpKernel\Event\RequestEvent; // ... - public function onKernelRequest(GetResponseEvent $event) + public function onKernelRequest(RequestEvent $event) { if (!$event->isMasterRequest()) { return; @@ -741,10 +754,6 @@ directory of FooBundle. The HttpKernel component provides a method called :method:`Symfony\\Component\\HttpKernel\\Kernel::locateResource` which can be used to transform logical paths into physical paths:: - use Symfony\Component\HttpKernel\HttpKernel; - - // ... - $kernel = new HttpKernel($dispatcher, $resolver); $path = $kernel->locateResource('@FooBundle/Resources/config/services.xml'); Learn more @@ -756,10 +765,8 @@ Learn more /reference/events -.. _Packagist: https://packagist.org/packages/symfony/http-kernel .. _reflection: https://php.net/manual/en/book.reflection.php .. _FOSRestBundle: https://github.com/friendsofsymfony/FOSRestBundle -.. _`Create your own framework... on top of the Symfony2 Components`: http://fabien.potencier.org/article/50/create-your-own-framework-on-top-of-the-symfony2-components-part-1 .. _`PHP FPM`: https://php.net/manual/en/install.fpm.php .. _`SensioFrameworkExtraBundle`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html .. _`@ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html diff --git a/components/inflector.rst b/components/inflector.rst new file mode 100644 index 00000000000..5e9f4325884 --- /dev/null +++ b/components/inflector.rst @@ -0,0 +1,64 @@ +.. index:: + single: Inflector + single: Components; Inflector + +The Inflector Component +======================= + + The Inflector component converts English words between their singular and + plural forms. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/inflector + +.. include:: /components/require_autoload.rst.inc + +When you May Need an Inflector +------------------------------ + +In some scenarios such as code generation and code introspection, it's usually +required to convert words from/to singular/plural. For example, if you need to +know which property is associated with an *adder* method, you must convert from +plural to singular (``addStories()`` method -> ``$story`` property). + +Although most human languages define simple pluralization rules, they also +define lots of exceptions. For example, the general rule in English is to add an +``s`` at the end of the word (``book`` -> ``books``) but there are lots of +exceptions even for common words (``woman`` -> ``women``, ``life`` -> ``lives``, +``news`` -> ``news``, ``radius`` -> ``radii``, etc.) + +This component abstracts all those pluralization rules so you can convert +from/to singular/plural with confidence. However, due to the complexity of the +human languages, this component only provides support for the English language. + +Usage +----- + +The Inflector component provides two static methods to convert from/to +singular/plural:: + + use Symfony\Component\Inflector\Inflector; + + Inflector::singularize('alumni'); // 'alumnus' + Inflector::singularize('knives'); // 'knife' + Inflector::singularize('mice'); // 'mouse' + + Inflector::pluralize('grandchild'); // 'grandchildren' + Inflector::pluralize('news'); // 'news' + Inflector::pluralize('bacterium'); // 'bacteria' + +Sometimes it's not possible to determine a unique singular/plural form for the +given word. In those cases, the methods return an array with all the possible +forms:: + + use Symfony\Component\Inflector\Inflector; + + Inflector::singularize('indices'); // ['index', 'indix', 'indice'] + Inflector::singularize('leaves'); // ['leaf', 'leave', 'leaff'] + + Inflector::pluralize('matrix'); // ['matricies', 'matrixes'] + Inflector::pluralize('person'); // ['persons', 'people'] diff --git a/components/intl.rst b/components/intl.rst index d50f233b037..94ee45ff018 100644 --- a/components/intl.rst +++ b/components/intl.rst @@ -5,8 +5,8 @@ The Intl Component ================== - A PHP replacement layer for the C `intl extension`_ that also provides - access to the localization data of the `ICU library`_. + This component provides access to the localization data of the `ICU library`_. + It also provides a PHP replacement layer for the C `intl extension`_. .. caution:: @@ -52,282 +52,290 @@ replace the intl classes: Composer automatically exposes these classes in the global namespace. -Writing and Reading Resource Bundles ------------------------------------- +Accessing ICU Data +------------------ -The :phpclass:`ResourceBundle` class is not currently supported by this component. -Instead, it includes a set of readers and writers for reading and writing -arrays (or array-like objects) from/to resource bundle files. The following -classes are supported: +This component provides the following ICU data: -* `TextBundleWriter`_ -* `PhpBundleWriter`_ -* `BinaryBundleReader`_ -* `PhpBundleReader`_ -* `BufferedBundleReader`_ -* `StructuredBundleReader`_ +* `Language and Script Names`_ +* `Country Names`_ +* `Locales`_ +* `Currencies`_ +* `Timezones`_ -Continue reading if you are interested in how to use these classes. Otherwise -skip this section and jump to `Accessing ICU Data`_. +Language and Script Names +~~~~~~~~~~~~~~~~~~~~~~~~~ -TextBundleWriter -~~~~~~~~~~~~~~~~ +The ``Languages`` class provides access to the name of all languages +according to the `ISO 639-1 alpha-2`_ list and the `ISO 639-2 alpha-3`_ list:: -The :class:`Symfony\\Component\\Intl\\ResourceBundle\\Writer\\TextBundleWriter` -writes an array or an array-like object to a plain-text resource bundle. The -resulting .txt file can be converted to a binary .res file with the -:class:`Symfony\\Component\\Intl\\ResourceBundle\\Compiler\\BundleCompiler` -class:: + use Symfony\Component\Intl\Languages; - use Symfony\Component\Intl\ResourceBundle\Compiler\BundleCompiler; - use Symfony\Component\Intl\ResourceBundle\Writer\TextBundleWriter; + \Locale::setDefault('en'); - $writer = new TextBundleWriter(); - $writer->write('/path/to/bundle', 'en', [ - 'Data' => [ - 'entry1', - 'entry2', - // ... - ], - ]); + $languages = Languages::getNames(); + // ('languageCode' => 'languageName') + // => ['ab' => 'Abkhazian', 'ace' => 'Achinese', ...] - $compiler = new BundleCompiler(); - $compiler->compile('/path/to/bundle', '/path/to/binary/bundle'); + $language = Languages::getName('fr'); + // => 'French' -The command "genrb" must be available for the -:class:`Symfony\\Component\\Intl\\ResourceBundle\\Compiler\\BundleCompiler` to -work. If the command is located in a non-standard location, you can pass its -path to the -:class:`Symfony\\Component\\Intl\\ResourceBundle\\Compiler\\BundleCompiler` -constructor. +All methods accept the translation locale as the last, optional parameter, +which defaults to the current default locale:: -PhpBundleWriter -~~~~~~~~~~~~~~~ + $languages = Languages::getNames('de'); + // => ['ab' => 'Abchasisch', 'ace' => 'Aceh', ...] -The :class:`Symfony\\Component\\Intl\\ResourceBundle\\Writer\\PhpBundleWriter` -writes an array or an array-like object to a .php resource bundle:: + $language = Languages::getName('fr', 'de'); + // => 'Französisch' - use Symfony\Component\Intl\ResourceBundle\Writer\PhpBundleWriter; +If the given locale doesn't exist, the methods trigger a +:class:`Symfony\\Component\\Intl\\Exception\\MissingResourceException`. In addition +to catching the exception, you can also check if a given language code is valid:: - $writer = new PhpBundleWriter(); - $writer->write('/path/to/bundle', 'en', [ - 'Data' => [ - 'entry1', - 'entry2', - // ... - ], - ]); + $isValidLanguage = Languages::exists($languageCode); -BinaryBundleReader -~~~~~~~~~~~~~~~~~~ +.. versionadded:: 4.3 -The :class:`Symfony\\Component\\Intl\\ResourceBundle\\Reader\\BinaryBundleReader` -reads binary resource bundle files and returns an array or an array-like object. -This class currently only works with the `intl extension`_ installed:: + The ``Languages`` class was introduced in Symfony 4.3. - use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader; +The ``Scripts`` class provides access to the optional four-letter script code +that can follow the language code according to the `Unicode ISO 15924 Registry`_ +(e.g. ``HANS`` in ``zh_HANS`` for simplified Chinese and ``HANT`` in ``zh_HANT`` +for traditional Chinese):: - $reader = new BinaryBundleReader(); - $data = $reader->read('/path/to/bundle', 'en'); + use Symfony\Component\Intl\Scripts; - var_dump($data['Data']['entry1']); + \Locale::setDefault('en'); -PhpBundleReader -~~~~~~~~~~~~~~~ + $scripts = Scripts::getNames(); + // ('scriptCode' => 'scriptName') + // => ['Adlm' => 'Adlam', 'Afak' => 'Afaka', ...] -The :class:`Symfony\\Component\\Intl\\ResourceBundle\\Reader\\PhpBundleReader` -reads resource bundles from .php files and returns an array or an array-like -object:: + $script = Scripts::getName('Hans'); + // => 'Simplified' - use Symfony\Component\Intl\ResourceBundle\Reader\PhpBundleReader; +All methods accept the translation locale as the last, optional parameter, +which defaults to the current default locale:: - $reader = new PhpBundleReader(); - $data = $reader->read('/path/to/bundle', 'en'); + $scripts = Scripts::getNames('de'); + // => ['Adlm' => 'Adlam', 'Afak' => 'Afaka', ...] - var_dump($data['Data']['entry1']); + $script = Scripts::getName('Hans', 'de'); + // => 'Vereinfacht' -BufferedBundleReader -~~~~~~~~~~~~~~~~~~~~ +If the given script code doesn't exist, the methods trigger a +:class:`Symfony\\Component\\Intl\\Exception\\MissingResourceException`. In addition +to catching the exception, you can also check if a given script code is valid:: -The :class:`Symfony\\Component\\Intl\\ResourceBundle\\Reader\\BufferedBundleReader` -wraps another reader, but keeps the last N reads in a buffer, where N is a -buffer size passed to the constructor:: + $isValidScript = Scripts::exists($scriptCode); - use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader; - use Symfony\Component\Intl\ResourceBundle\Reader\BufferedBundleReader; +.. versionadded:: 4.3 - $reader = new BufferedBundleReader(new BinaryBundleReader(), 10); + The ``Scripts`` class was introduced in Symfony 4.3. - // actually reads the file - $data = $reader->read('/path/to/bundle', 'en'); +Country Names +~~~~~~~~~~~~~ - // returns data from the buffer - $data = $reader->read('/path/to/bundle', 'en'); +The ``Countries`` class provides access to the name of all countries according +to the `ISO 3166-1 alpha-2`_ list of officially recognized countries and +territories:: - // actually reads the file - $data = $reader->read('/path/to/bundle', 'fr'); + use Symfony\Component\Intl\Countries; -StructuredBundleReader -~~~~~~~~~~~~~~~~~~~~~~ + \Locale::setDefault('en'); -The :class:`Symfony\\Component\\Intl\\ResourceBundle\\Reader\\StructuredBundleReader` -wraps another reader and offers a -:method:`Symfony\\Component\\Intl\\ResourceBundle\\Reader\\StructuredBundleReaderInterface::readEntry` -method for reading an entry of the resource bundle without having to worry -whether array keys are set or not. If a path cannot be resolved, ``null`` is -returned:: + $countries = Countries::getNames(); + // ('countryCode' => 'countryName') + // => ['AF' => 'Afghanistan', 'AX' => 'Åland Islands', ...] - use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader; - use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReader; + $country = Countries::getName('GB'); + // => 'United Kingdom' - $reader = new StructuredBundleReader(new BinaryBundleReader()); +All methods accept the translation locale as the last, optional parameter, +which defaults to the current default locale:: - $data = $reader->read('/path/to/bundle', 'en'); + $countries = Countries::getNames('de'); + // => ['AF' => 'Afghanistan', 'EG' => 'Ägypten', ...] - // produces an error if the key "Data" does not exist - var_dump($data['Data']['entry1']); + $country = Countries::getName('GB', 'de'); + // => 'Vereinigtes Königreich' - // returns null if the key "Data" does not exist - var_dump($reader->readEntry('/path/to/bundle', 'en', ['Data', 'entry1'])); +If the given country code doesn't exist, the methods trigger a +:class:`Symfony\\Component\\Intl\\Exception\\MissingResourceException`. In addition +to catching the exception, you can also check if a given country code is valid:: -Additionally, the -:method:`Symfony\\Component\\Intl\\ResourceBundle\\Reader\\StructuredBundleReaderInterface::readEntry` -method resolves fallback locales. For example, the fallback locale of "en_GB" is -"en". For single-valued entries (strings, numbers etc.), the entry will be read -from the fallback locale if it cannot be found in the more specific locale. For -multi-valued entries (arrays), the values of the more specific and the fallback -locale will be merged. In order to suppress this behavior, the last parameter -``$fallback`` can be set to ``false``:: + $isValidCountry = Countries::exists($countryCode); - var_dump($reader->readEntry( - '/path/to/bundle', - 'en', - ['Data', 'entry1'], - false - )); +.. versionadded:: 4.3 -Accessing ICU Data ------------------- + The ``Countries`` class was introduced in Symfony 4.3. -The ICU data is located in several "resource bundles". You can access a PHP -wrapper of these bundles through the static -:class:`Symfony\\Component\\Intl\\Intl` class. At the moment, the following -data is supported: +Locales +~~~~~~~ -* `Language and Script Names`_ -* `Country Names`_ -* `Locales`_ -* `Currencies`_ +A locale is the combination of a language and a region. For example, "Chinese" +is the language and ``zh_Hans_MO`` is the locale for "Chinese" (language) + +"Simplified" (script) + "Macau SAR China" (region). The ``Locales`` class +provides access to the name of all locales:: -Language and Script Names -~~~~~~~~~~~~~~~~~~~~~~~~~ + use Symfony\Component\Intl\Locales; -The translations of language and script names can be found in the language -bundle:: + \Locale::setDefault('en'); - use Symfony\Component\Intl\Intl; + $locales = Locales::getNames(); + // ('localeCode' => 'localeName') + // => ['af' => 'Afrikaans', 'af_NA' => 'Afrikaans (Namibia)', ...] - \Locale::setDefault('en'); + $locale = Locales::getName('zh_Hans_MO'); + // => 'Chinese (Simplified, Macau SAR China)' - $languages = Intl::getLanguageBundle()->getLanguageNames(); - // => ['ab' => 'Abkhazian', ...] +All methods accept the translation locale as the last, optional parameter, +which defaults to the current default locale:: - $language = Intl::getLanguageBundle()->getLanguageName('de'); - // => 'German' + $locales = Locales::getNames('de'); + // => ['af' => 'Afrikaans', 'af_NA' => 'Afrikaans (Namibia)', ...] - $language = Intl::getLanguageBundle()->getLanguageName('de', 'AT'); - // => 'Austrian German' + $locale = Locales::getName('zh_Hans_MO', 'de'); + // => 'Chinesisch (Vereinfacht, Sonderverwaltungsregion Macau)' - $scripts = Intl::getLanguageBundle()->getScriptNames(); - // => ['Arab' => 'Arabic', ...] +If the given locale code doesn't exist, the methods trigger a +:class:`Symfony\\Component\\Intl\\Exception\\MissingResourceException`. In addition +to catching the exception, you can also check if a given locale code is valid:: - $script = Intl::getLanguageBundle()->getScriptName('Hans'); - // => 'Simplified' + $isValidLocale = Locales::exists($localeCode); -All methods accept the translation locale as the last, optional parameter, -which defaults to the current default locale:: +.. versionadded:: 4.3 - $languages = Intl::getLanguageBundle()->getLanguageNames('de'); - // => ['ab' => 'Abchasisch', ...] + The ``Locales`` class was introduced in Symfony 4.3. -Country Names -~~~~~~~~~~~~~ +Currencies +~~~~~~~~~~ -The translations of country names can be found in the region bundle:: +The ``Currencies`` class provides access to the name of all currencies as well +as some of their information (symbol, fraction digits, etc.):: - use Symfony\Component\Intl\Intl; + use Symfony\Component\Intl\Currencies; \Locale::setDefault('en'); - $countries = Intl::getRegionBundle()->getCountryNames(); - // => ['AF' => 'Afghanistan', ...] + $currencies = Currencies::getNames(); + // ('currencyCode' => 'currencyName') + // => ['AFN' => 'Afghan Afghani', 'ALL' => 'Albanian Lek', ...] - $country = Intl::getRegionBundle()->getCountryName('GB'); - // => 'United Kingdom' + $currency = Currencies::getName('INR'); + // => 'Indian Rupee' -All methods accept the translation locale as the last, optional parameter, -which defaults to the current default locale:: + $symbol = Currencies::getSymbol('INR'); + // => '₹' - $countries = Intl::getRegionBundle()->getCountryNames('de'); - // => ['AF' => 'Afghanistan', ...] + $fractionDigits = Currencies::getFractionDigits('INR'); + // => 2 -Locales -~~~~~~~ + $roundingIncrement = Currencies::getRoundingIncrement('INR'); + // => 0 + +All methods (except for ``getFractionDigits()`` and ``getRoundingIncrement()``) +accept the translation locale as the last, optional parameter, which defaults to +the current default locale:: + + $currencies = Currencies::getNames('de'); + // => ['AFN' => 'Afghanischer Afghani', 'EGP' => 'Ägyptisches Pfund', ...] + + $currency = Currencies::getName('INR', 'de'); + // => 'Indische Rupie' -The translations of locale names can be found in the locale bundle:: +If the given currency code doesn't exist, the methods trigger a +:class:`Symfony\\Component\\Intl\\Exception\\MissingResourceException`. In addition +to catching the exception, you can also check if a given currency code is valid:: - use Symfony\Component\Intl\Intl; + $isValidCurrency = Currencies::exists($currencyCode); + +.. versionadded:: 4.3 + + The ``Currencies`` class was introduced in Symfony 4.3. + +.. _component-intl-timezones: + +Timezones +~~~~~~~~~ + +The ``Timezones`` class provides several utilities related to timezones. First, +you can get the name and values of all timezones in all languages:: + + use Symfony\Component\Intl\Timezones; \Locale::setDefault('en'); - $locales = Intl::getLocaleBundle()->getLocaleNames(); - // => ['af' => 'Afrikaans', ...] + $timezones = Timezones::getNames(); + // ('timezoneID' => 'timezoneValue') + // => ['America/Eirunepe' => 'Acre Time (Eirunepe)', 'America/Rio_Branco' => 'Acre Time (Rio Branco)', ...] - $locale = Intl::getLocaleBundle()->getLocaleName('zh_Hans_MO'); - // => 'Chinese (Simplified, Macau SAR China)' + $timezone = Timezones::getName('Africa/Nairobi'); + // => 'East Africa Time (Nairobi)' All methods accept the translation locale as the last, optional parameter, which defaults to the current default locale:: - $locales = Intl::getLocaleBundle()->getLocaleNames('de'); - // => ['af' => 'Afrikaans', ...] + $timezones = Timezones::getNames('de'); + // => ['America/Eirunepe' => 'Acre-Zeit (Eirunepe)', 'America/Rio_Branco' => 'Acre-Zeit (Rio Branco)', ...] -Currencies -~~~~~~~~~~ + $timezone = Timezones::getName('Africa/Nairobi', 'de'); + // => 'Ostafrikanische Zeit (Nairobi)' -The translations of currency names and other currency-related information can -be found in the currency bundle:: +You can also get all the timezones that exist in a given country. The +``forCountryCode()`` method returns one or more timezone IDs, which you can +translate into any locale with the ``getName()`` method shown earlier:: - use Symfony\Component\Intl\Intl; + // unlike language codes, country codes are always uppercase (CL = Chile) + $timezones = Timezones::forCountryCode('CL'); + // => ['America/Punta_Arenas', 'America/Santiago', 'Pacific/Easter'] - \Locale::setDefault('en'); +The reverse lookup is also possible thanks to the ``getCountryCode()`` method, +which returns the code of the country where the given timezone ID belongs to:: - $currencies = Intl::getCurrencyBundle()->getCurrencyNames(); - // => ['AFN' => 'Afghan Afghani', ...] + $countryCode = Timezones::getCountryCode('America/Vancouver') + // => $countryCode = 'CA' (CA = Canada) - $currency = Intl::getCurrencyBundle()->getCurrencyName('INR'); - // => 'Indian Rupee' +The `UTC/GMT time offsets`_ of all timezones are provided by ``getRawOffset()`` +(which returns an integer representing the offset in seconds) and +``getGmtOffset()`` (which returns a string representation of the offset to +display it to users):: - $symbol = Intl::getCurrencyBundle()->getCurrencySymbol('INR'); - // => '₹' + $offset = Timezones::getRawOffset('Etc/UTC'); // $offset = 0 + $offset = Timezones::getRawOffset('America/Buenos_Aires'); // $offset = -10800 + $offset = Timezones::getRawOffset('Asia/Katmandu'); // $offset = 20700 - $fractionDigits = Intl::getCurrencyBundle()->getFractionDigits('INR'); - // => 2 + $offset = Timezones::getGmtOffset('Etc/UTC'); // $offset = 'GMT+00:00' + $offset = Timezones::getGmtOffset('America/Buenos_Aires'); // $offset = 'GMT-03:00' + $offset = Timezones::getGmtOffset('Asia/Katmandu'); // $offset = 'GMT+05:45' - $roundingIncrement = Intl::getCurrencyBundle()->getRoundingIncrement('INR'); - // => 0 +The timezone offset can vary in time because of the `daylight saving time (DST)`_ +practice. By default these methods use the ``time()`` PHP function to get the +current timezone offset value, but you can pass a timestamp as their second +arguments to get the offset at any given point in time:: + + // In 2019, the DST period in Madrid (Spain) went from March 31 to October 27 + $offset = Timezones::getRawOffset('Europe/Madrid', strtotime('March 31, 2019')); // $offset = 3600 + $offset = Timezones::getRawOffset('Europe/Madrid', strtotime('April 1, 2019')); // $offset = 7200 + $offset = Timezones::getGmtOffset('Europe/Madrid', strtotime('October 27, 2019')); // $offset = 'GMT+02:00' + $offset = Timezones::getGmtOffset('Europe/Madrid', strtotime('October 28, 2019')); // $offset = 'GMT+01:00' + +The string representation of the GMT offset can vary depending on the locale, so +you can pass the locale as the third optional argument:: + + $offset = Timezones::getGmtOffset('Europe/Madrid', strtotime('October 28, 2019'), 'ar')); // $offset = 'غرينتش+01:00' + $offset = Timezones::getGmtOffset('Europe/Madrid', strtotime('October 28, 2019'), 'dz')); // $offset = 'ཇི་ཨེམ་ཏི་+01:00' + +If the given timezone ID doesn't exist, the methods trigger a +:class:`Symfony\\Component\\Intl\\Exception\\MissingResourceException`. In addition +to catching the exception, you can also check if a given timezone ID is valid:: -All methods (except for -:method:`Symfony\\Component\\Intl\\ResourceBundle\\CurrencyBundleInterface::getFractionDigits` -and -:method:`Symfony\\Component\\Intl\\ResourceBundle\\CurrencyBundleInterface::getRoundingIncrement`) -accept the translation locale as the last, optional parameter, which defaults -to the current default locale:: + $isValidTimezone = Timezones::exists($timezoneId); - $currencies = Intl::getCurrencyBundle()->getCurrencyNames('de'); - // => ['AFN' => 'Afghanische Afghani', ...] +.. versionadded:: 4.3 -That's all you need to know for now. Have fun coding! + The ``Timezones`` class was introduced in Symfony 4.3. Learn more ---------- @@ -340,9 +348,14 @@ Learn more /reference/forms/types/currency /reference/forms/types/language /reference/forms/types/locale + /reference/forms/types/timezone -.. _Packagist: https://packagist.org/packages/symfony/intl -.. _Icu component: https://packagist.org/packages/symfony/icu .. _intl extension: https://php.net/manual/en/book.intl.php .. _install the intl extension: https://php.net/manual/en/intl.setup.php .. _ICU library: http://site.icu-project.org/ +.. _`Unicode ISO 15924 Registry`: https://www.unicode.org/iso15924/iso15924-codes.html +.. _`ISO 3166-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 +.. _`UTC/GMT time offsets`: https://en.wikipedia.org/wiki/List_of_UTC_time_offsets +.. _`daylight saving time (DST)`: https://en.wikipedia.org/wiki/Daylight_saving_time +.. _`ISO 639-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_639-1 +.. _`ISO 639-2 alpha-3`: https://en.wikipedia.org/wiki/ISO_639-2 diff --git a/components/ldap.rst b/components/ldap.rst index 7368fb8af66..50b7eb5b917 100644 --- a/components/ldap.rst +++ b/components/ldap.rst @@ -145,7 +145,6 @@ delete existing ones:: // Removing an existing entry $entryManager->remove(new Entry('cn=Test User,dc=symfony,dc=com')); - Batch Updating ______________ @@ -173,5 +172,3 @@ Possible operation types are ``LDAP_MODIFY_BATCH_ADD``, ``LDAP_MODIFY_BATCH_REMO ``LDAP_MODIFY_BATCH_REMOVE_ALL``, ``LDAP_MODIFY_BATCH_REPLACE``. Parameter ``$values`` must be ``NULL`` when using ``LDAP_MODIFY_BATCH_REMOVE_ALL`` operation type. - -.. _Packagist: https://packagist.org/packages/symfony/ldap diff --git a/components/lock.rst b/components/lock.rst index 5f90353ba8d..d6e1220f2b7 100644 --- a/components/lock.rst +++ b/components/lock.rst @@ -398,7 +398,8 @@ Remote Stores ~~~~~~~~~~~~~ Remote stores (:ref:`MemcachedStore `, -:ref:`PdoStore `, :ref:`RedisStore `, and +:ref:`PdoStore `, +:ref:`RedisStore ` and :ref:`ZookeeperStore `) use a unique token to recognize the true owner of the lock. This token is stored in the :class:`Symfony\\Component\\Lock\\Key` object and is used internally by @@ -422,7 +423,8 @@ Expiring Stores ~~~~~~~~~~~~~~~ Expiring stores (:ref:`MemcachedStore `, -:ref:`PdoStore ` and :ref:`RedisStore `) +:ref:`PdoStore ` and +:ref:`RedisStore `) guarantee that the lock is acquired only for the defined duration of time. If the task takes longer to be accomplished, then the lock can be released by the store and acquired by someone else. @@ -659,7 +661,6 @@ are still running. .. _`ACID`: https://en.wikipedia.org/wiki/ACID .. _`locks`: https://en.wikipedia.org/wiki/Lock_(computer_science) -.. _Packagist: https://packagist.org/packages/symfony/lock .. _`PHP semaphore functions`: https://php.net/manual/en/book.sem.php .. _`PDO`: https://php.net/pdo .. _`Doctrine DBAL Connection`: https://github.com/doctrine/dbal/blob/master/lib/Doctrine/DBAL/Connection.php diff --git a/components/mailer.rst b/components/mailer.rst new file mode 100644 index 00000000000..ecdeef798f2 --- /dev/null +++ b/components/mailer.rst @@ -0,0 +1,184 @@ +.. index:: + single: Mailer + single: Components; Mailer + +The Mailer Component +==================== + + The Mailer component helps sending emails. + +If you're using the Symfony Framework, read the +:doc:`Symfony Framework Mailer documentation `. + +.. versionadded:: 4.3 + + The Mailer component was introduced in Symfony 4.3 and it's still + considered an :doc:`experimental feature `. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/mailer + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +The Mailer component has two main classes: a ``Transport`` and the ``Mailer`` itself:: + + use Symfony\Component\Mailer\Mailer; + use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; + + $transport = new SmtpTransport('localhost'); + $mailer = new Mailer($transport); + $mailer->send($email); + +The ``$email`` object is created via the :doc:`Mime component `. + +Transport +--------- + +The only transport that comes pre-installed is SMTP. + +Below is the list of other popular providers with built-in support: + +================== ============================================= +Service Install with +================== ============================================= +Amazon SES ``composer require symfony/amazon-mailer`` +Gmail ``composer require symfony/google-mailer`` +MailChimp ``composer require symfony/mailchimp-mailer`` +Mailgun ``composer require symfony/mailgun-mailer`` +Postmark ``composer require symfony/postmark-mailer`` +SendGrid ``composer require symfony/sendgrid-mailer`` +================== ============================================= + +For example, suppose you want to use Google's Gmail SMTP server. First, install +it: + +.. code-block:: terminal + + $ composer require symfony/google-mailer + +Then, use the SMTP Gmail transport:: + + use Symfony\Component\Mailer\Bridge\Google\Smtp\GmailTransport; + + $transport = new GmailTransport('user', 'pass'); + $mailer = new Mailer($transport); + $mailer->send($email); + +Each provider provides up to 3 transports: standard SMTP, HTTP (it uses the +provider's API but the body is created by the mailer component), API (it uses +the full API of the provider with no control over the body creation -- features +might be limited as well). + +.. _mailer_dsn: + +The mailer component provides a convenient way to create a transport from a +DSN:: + + use Symfony\Component\Mailer\Transport; + + $transport = Transport::fromDsn($dsn); + +Where ``$dsn`` depends on the provider you want to use. For plain SMTP, use +``smtp://user:pass@example.com`` or ``smtp://sendmail`` to use the ``sendmail`` +binary. For third-party providers, refers to the following table: + +==================== ================================== ================================== ================================ + Provider SMTP HTTP API +==================== ================================== ================================== ================================ + Amazon SES smtp://ACCESS_KEY:SECRET_KEY@ses http://ACCESS_KEY:SECRET_KEY@ses api://ACCESS_KEY:SECRET_KEY@ses + Google Gmail smtp://USERNAME:PASSWORD@gmail n/a n/a + Mailchimp Mandrill smtp://USERNAME:PASSWORD@mandrill http://KEY@mandrill api://KEY@mandrill + Mailgun smtp://USERNAME:PASSWORD@mailgun http://KEY:DOMAIN@mailgun api://KEY:DOMAIN@mailgun + Postmark smtp://ID:ID@postmark n/a api://KEY@postmark + Sendgrid smtp://apikey:KEY@sendgrid n/a api://KEY@sendgrid +==================== ================================== ================================== ================================ + +High Availability +----------------- + +Symfony's mailer supports `high availability`_ via a technique called "failover" +to ensure that emails are sent even if one mailer server fails . + +A failover transport is configured with two or more transports joined by the +``||`` operator:: + + $dsn = 'api://id@postmark || smtp://key@sendgrid'; + +The mailer will start using the first transport. If the sending fails, the +mailer won't retry it with the other transports, but it will switch to the next +transport automatically for the following deliveries. + +Load Balancing +-------------- + +Symfony's mailer supports `load balancing`_ via a technique called "round-robin" +to distribute the mailing workload across multiple transports . + +A round-robin transport is configured with two or more transports joined by the +``&&`` operator:: + + $dsn = 'api://id@postmark && smtp://key@sendgrid' + +The mailer will start using the first transport and if it fails, it will retry +the same delivery with the next transports until one of them succeeds (or until +all of them fail). + +Sending emails asynchronously +----------------------------- + +If you want to send emails asynchronously, install the :doc:`Messenger component +`. + +.. code-block:: terminal + + $ composer require symfony/messenger + +Then, instantiate and pass a ``MessageBus`` as a second argument to ``Mailer``:: + + use Symfony\Component\Mailer\Mailer; + use Symfony\Component\Mailer\Messenger\MessageHandler; + use Symfony\Component\Mailer\Messenger\SendEmailMessage; + use Symfony\Component\Mailer\SmtpEnvelope; + use Symfony\Component\Mailer\Transport; + use Symfony\Component\Messenger\Handler\HandlersLocator; + use Symfony\Component\Messenger\MessageBus; + use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; + use Symfony\Component\Mime\Address; + + $dsn = 'change-dsn-accordingly'; + + $transport = Transport::fromDsn($dsn); + $handler = new MessageHandler($transport); + + $bus = new MessageBus([ + new HandleMessageMiddleware(new HandlersLocator([ + SendEmailMessage::class => [$handler], + ])), + ]); + + $mailer = new Mailer($transport, $bus); + $mailer->send($email); + + // you can pass an optional Envelope + $mailer->send($email, new SmtpEnvelope( + new Address('sender@example.com'), + [ + new Address('recipient@example.com'), + ] + )); + +Learn More +----------- + +To learn more about how to use the mailer component, refer to the +:doc:`Symfony Framework Mailer documentation `. + +.. _`high availability`: https://en.wikipedia.org/wiki/High_availability +.. _`load balancing`: https://en.wikipedia.org/wiki/Load_balancing_(computing) diff --git a/components/messenger.rst b/components/messenger.rst index 5fb8eed7a9b..a72b42f30b2 100644 --- a/components/messenger.rst +++ b/components/messenger.rst @@ -8,8 +8,8 @@ The Messenger Component The Messenger component helps applications send and receive messages to/from other applications or via message queues. - The component is greatly inspired by Matthias Noback's series of `blog posts - about command buses`_ and the `SimpleBus project`_. + The component is greatly inspired by Matthias Noback's series of + `blog posts about command buses`_ and the `SimpleBus project`_. .. seealso:: @@ -79,6 +79,11 @@ are configured for you: #. :class:`Symfony\\Component\\Messenger\\Middleware\\SendMessageMiddleware` (enables asynchronous processing) #. :class:`Symfony\\Component\\Messenger\\Middleware\\HandleMessageMiddleware` (calls the registered handler(s)) +.. deprecated:: 4.3 + + The ``LoggingMiddleware`` is deprecated since Symfony 4.3 and will be + removed in 5.0. Pass a logger to ``SendMessageMiddleware`` instead. + Example:: use App\Message\MyMessage; @@ -130,6 +135,8 @@ through the transport layer, use the ``SerializerStamp`` stamp:: $bus->dispatch( (new Envelope($message))->with(new SerializerStamp([ + // groups are applied to the whole message, so make sure + // to define the group for every embedded object 'groups' => ['my_serialization_groups'], ])) ); @@ -201,7 +208,8 @@ Your own Sender Imagine that you already have an ``ImportantAction`` message going through the message bus and being handled by a handler. Now, you also want to send this -message as an email. +message as an email (using the :doc:`Mime ` and +:doc:`Mailer ` components). Using the :class:`Symfony\\Component\\Messenger\\Transport\\Sender\\SenderInterface`, you can create your own message sender:: @@ -209,15 +217,17 @@ you can create your own message sender:: namespace App\MessageSender; use App\Message\ImportantAction; + use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Transport\Sender\SenderInterface; + use Symfony\Component\Mime\Email; class ImportantActionToEmailSender implements SenderInterface { private $mailer; private $toEmail; - public function __construct(\Swift_Mailer $mailer, string $toEmail) + public function __construct(MailerInterface $mailer, string $toEmail) { $this->mailer = $mailer; $this->toEmail = $toEmail; @@ -232,12 +242,10 @@ you can create your own message sender:: } $this->mailer->send( - (new \Swift_Message('Important action made')) - ->setTo($this->toEmail) - ->setBody( - '

Important action

Made by '.$message->getUsername().'

', - 'text/html' - ) + (new Email()) + ->to($this->toEmail) + ->subject('Important action made') + ->html('

Important action

Made by '.$message->getUsername().'

') ); return $envelope; @@ -276,23 +284,38 @@ do is to write your own CSV receiver:: $this->filePath = $filePath; } - public function receive(callable $handler): void + public function get(): iterable { $ordersFromCsv = $this->serializer->deserialize(file_get_contents($this->filePath), 'csv'); foreach ($ordersFromCsv as $orderFromCsv) { $order = new NewOrder($orderFromCsv['id'], $orderFromCsv['account_id'], $orderFromCsv['amount']); - $handler(new Envelope($order)); + $envelope = new Envelope($order); + + $handler($envelope); } + + return [$envelope]; } - public function stop(): void + public function ack(Envelope $envelope): void { - // noop + // Add information about the handled message + } + + public function reject(Envelope $envelope): void + { + // Reject the message if needed } } +.. versionadded:: 4.3 + + In Symfony 4.3, the ``ReceiverInterface`` has changed its methods as shown + in the example above. You may need to update your code if you used this + interface in previous Symfony versions. + Receiver and Sender on the same Bus ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -311,5 +334,5 @@ Learn more /messenger /messenger/* -.. _blog posts about command buses: https://matthiasnoback.nl/tags/command%20bus/ -.. _SimpleBus project: http://docs.simplebus.io/en/latest/ +.. _`blog posts about command buses`: https://matthiasnoback.nl/tags/command%20bus/ +.. _`SimpleBus project`: http://docs.simplebus.io/en/latest/ diff --git a/components/mime.rst b/components/mime.rst new file mode 100644 index 00000000000..501b10a8dc5 --- /dev/null +++ b/components/mime.rst @@ -0,0 +1,306 @@ +.. index:: + single: MIME + single: MIME Messages + single: Components; MIME + +The Mime Component +================== + + The Mime component allows manipulating the MIME messages used to send emails + and provides utilities related to MIME types. + +.. versionadded:: 4.3 + + The Mime component was introduced in Symfony 4.3 and it's still + considered an :doc:`experimental feature `. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/mime + +.. include:: /components/require_autoload.rst.inc + +Introduction +------------ + +`MIME`_ (Multipurpose Internet Mail Extensions) is an Internet standard that +extends the original basic format of emails to support features like: + +* Headers and text contents using non-ASCII characters; +* Message bodies with multiple parts (e.g. HTML and plain text contents); +* Non-text attachments: audio, video, images, PDF, etc. + +The entire MIME standard is complex and huge, but Symfony abstracts all that +complexity to provide two ways of creating MIME messages: + +* A high-level API based on the :class:`Symfony\\Component\\Mime\\Email` class + to quickly create email messages with all the common features; +* A low-level API based on the :class:`Symfony\\Component\\Mime\\Message` class + to have an absolute control over every single part of the email message. + +Usage +----- + +Use the :class:`Symfony\\Component\\Mime\\Email` class and their *chainable* +methods to compose the entire email message:: + + use Symfony\Component\Mime\Email; + + $email = (new Email()) + ->from('fabien@symfony.com') + ->to('foo@example.com') + ->cc('bar@example.com') + ->bcc('baz@example.com') + ->replyTo('fabien@symfony.com') + ->priority(Email::PRIORITY_HIGH) + ->subject('Important Notification') + ->text('Lorem ipsum...') + ->html('

Lorem ipsum

...

') + ; + +This only purpose of this component is to create the email messages. Use the +:doc:`Mailer component ` to actually send them. In Symfony +applications, it's easier to use the :doc:`Mailer integration `. + +Most of the details about how to create Email objects, including Twig integration, +can be found in the :doc:`Mailer documentation `. + +Twig Integration +---------------- + +The Mime component comes with excellent integration with Twig, allowing you to +create messages from Twig templates, embed images, inline CSS and more. Details +on how to use those features can be found in the Mailer documentation: +:ref:`Twig: HTML & CSS `. + +But if you're using the Mime component without the Symfony framework, you'll need +to handle a few setup details. + +Twig Setup +~~~~~~~~~~ + +To integrate with Twig, use the :class:`Symfony\\Bridge\\Twig\\Mime\\BodyRenderer` +class to render the template and update the email message contents with the results:: + + // ... + use Symfony\Bridge\Twig\Mime\BodyRenderer; + use Twig\Environment; + use Twig\Loader\FilesystemLoader; + + // when using the Mime component inside a full-stack Symfony application, you + // don't need to do this Twig setup. You only have to inject the 'twig' service + $loader = new FilesystemLoader(__DIR__.'/templates'); + $twig = new Environment($loader); + + $renderer = new BodyRenderer($twig); + // this updates the $email object contents with the result of rendering + // the template defined earlier with the given context + $renderer->render($email); + +Inlining CSS Styles (and other Extensions) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To use the :ref:`inline_css ` filter, first install the Twig +extension: + +.. code-block:: terminal + + $ composer require twig/cssinliner-extension + +Now, enable the extension:: + + // ... + use Twig\CssInliner\CssInlinerExtension; + + $loader = new FilesystemLoader(__DIR__.'/templates'); + $twig = new Environment($loader); + $twig->addExtension(new CssInlinerExtension()); + +The same process should be used for enabling other extensions, like the +:ref:`MarkdownExtension ` and :ref:`InkyExtension `. + +Creating Raw Email Messages +--------------------------- + +This is useful for advanced applications that need absolute control over every +email part. It's not recommended for applications with regular email +requirements because it adds complexity for no real gain. + +Before continuing, it's important to have a look at the low level structure of +an email message. Consider a message which includes some content as both text +and HTML, a single PNG image embedded in those contents and a PDF file attached +to it. The MIME standard allows structuring this message in different ways, but +the following tree is the one that works on most email clients: + +.. code-block:: text + + multipart/mixed + ├── multipart/related + │ ├── multipart/alternative + │ │ ├── text/plain + │ │ └── text/html + │ └── image/png + └── application/pdf + +This is the purpose of each MIME message part: + +* ``multipart/alternative``: used when two or more parts are alternatives of the + same (or very similar) content. The preferred format must be added last. +* ``multipart/mixed``: used to send different content types in the same message, + such as when attaching files. +* ``multipart/related``: used to indicate that each message part is a component + of an aggregate whole. The most common usage is to display images embedded + in the message contents. + +When using the low-level :class:`Symfony\\Component\\Mime\\Message` class to +create the email message, you must keep all the above in mind to define the +different parts of the email by hand:: + + use Symfony\Component\Mime\Header\Headers; + use Symfony\Component\Mime\Message; + use Symfony\Component\Mime\Part\Multipart\AlternativePart; + use Symfony\Component\Mime\Part\TextPart; + + $headers = (new Headers()) + ->addMailboxListHeader('From', ['fabien@symfony.com']) + ->addMailboxListHeader('To', ['foo@example.com']) + ->addTextHeader('Subject', 'Important Notification') + ; + + $textContent = new TextPart('Lorem ipsum...'); + $htmlContent = new TextPart('

Lorem ipsum

...

', 'html'); + $body = new AlternativePart($textContent, $htmlContent); + + $email = new Message($headers, $body); + +Embedding images and attaching files is possible by creating the appropriate +email multiparts:: + + // ... + use Symfony\Component\Mime\Part\DataPart; + use Symfony\Component\Mime\Part\Multipart\MixedPart; + use Symfony\Component\Mime\Part\Multipart\RelatedPart; + + // ... + $embeddedImage = new DataPart(fopen('/path/to/images/logo.png', 'r'), null, 'image/png'); + $imageCid = $embeddedImage->getContentId(); + + $attachedFile = new DataPart(fopen('/path/to/documents/terms-of-use.pdf', 'r'), null, 'application/pdf'); + + $textContent = new TextPart('Lorem ipsum...'); + $htmlContent = new TextPart(sprintf( + '

Lorem ipsum

...

', $imageCid + ), 'html'); + $bodyContent = new AlternativePart($textContent, $htmlContent); + $body = new RelatedPart($bodyContent, $embeddedImage); + + $messageParts = new MixedPart($body, $attachedFile); + + $email = new Message($headers, $messageParts); + +Serializing Email Messages +-------------------------- + +Email messages created with either the ``Email`` or ``Message`` classes can be +serialized because they are simple data objects:: + + $email = (new Email()) + ->from('fabien@symfony.com') + // ... + ; + + $serializedEmail = serialize($email); + +A common use case is to store serialized email messages, include them in a +message sent with the :doc:`Messenger component ` and +recreate them later when sending them. Use the +:class:`Symfony\\Component\\Mime\\RawMessage` class to recreate email messages +from their serialized contents:: + + use Symfony\Component\Mime\RawMessage; + + // ... + $serializedEmail = serialize($email); + + // later, recreate the original message to actually send it + $message = new RawMessage(unserialize($serializedEmail)); + +MIME Types Utilities +-------------------- + +Although MIME was designed mainly for creating emails, the content types (also +known as `MIME types`_ and "media types") defined by MIME standards are also of +importance in communication protocols outside of email, such as HTTP. That's +why this component also provides utilities to work with MIME types. + +The :class:`Symfony\\Component\\Mime\\MimeTypes` class transforms between +MIME types and file name extensions:: + + use Symfony\Component\Mime\MimeTypes; + + $mimeTypes = new MimeTypes(); + $exts = $mimeTypes->getExtensions('application/javascript'); + // $exts = ['js', 'jsm', 'mjs'] + $exts = $mimeTypes->getExtensions('image/jpeg'); + // $exts = ['jpeg', 'jpg', 'jpe'] + + $mimeTypes = $mimeTypes->getMimeTypes('js'); + // $mimeTypes = ['application/javascript', 'application/x-javascript', 'text/javascript'] + $mimeTypes = $mimeTypes->getMimeTypes('apk'); + // $mimeTypes = ['application/vnd.android.package-archive'] + +These methods return arrays with one or more elements. The element position +indicates its priority, so the first returned extension is the preferred one. + +.. _components-mime-type-guess: + +Guessing the MIME Type +~~~~~~~~~~~~~~~~~~~~~~ + +Another useful utility allows to guess the MIME type of any given file:: + + use Symfony\Component\Mime\MimeTypes; + + $mimeTypes = new MimeTypes(); + $mimeType = $mimeTypes->guessMimeType('/some/path/to/image.gif'); + // Guessing is not based on the file name, so $mimeType will be 'image/gif' + // only if the given file is truly a GIF image + +Guessing the MIME type is a time-consuming process that requires inspecting +part of the file contents. Symfony applies multiple guessing mechanisms, one +of them based on the PHP `fileinfo extension`_. It's recommended to install +that extension to improve the guessing performance. + +Adding a MIME Type Guesser +.......................... + +You can register your own MIME type guesser by creating a class that implements +:class:`Symfony\\Component\\Mime\\MimeTypeGuesserInterface`:: + + namespace App; + + use Symfony\Component\Mime\MimeTypeGuesserInterface; + + class SomeMimeTypeGuesser implements MimeTypeGuesserInterface + { + public function isGuesserSupported(): bool + { + // return true when the guesser is supported (might depend on the OS for instance) + return true; + } + + public function guessMimeType(string $path): ?string + { + // inspect the contents of the file stored in $path to guess its + // type and return a valid MIME type ... or null if unknown + + return '...'; + } + } + +.. _`MIME`: https://en.wikipedia.org/wiki/MIME +.. _`MIME types`: https://en.wikipedia.org/wiki/Media_type +.. _`fileinfo extension`: https://php.net/fileinfo diff --git a/components/options_resolver.rst b/components/options_resolver.rst index 0930df8ed19..4c2ef3c1254 100644 --- a/components/options_resolver.rst +++ b/components/options_resolver.rst @@ -434,6 +434,16 @@ if you need to use other options during normalization:: } } +To normalize a new allowed value in sub-classes that are being normalized +in parent classes use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addNormalizer`. +This way, the ``$value`` argument will receive the previously normalized +value, otherwise you can prepend the new normalizer by passing ``true`` as +third argument. + +.. versionadded:: 4.3 + + The ``addNormalizer()`` method was introduced in Symfony 4.3. + Default Values that Depend on another Option ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -829,6 +839,3 @@ method ``clearOptionsConfig()`` and call it periodically:: That's it! You now have all the tools and knowledge needed to process options in your code. - -.. _Packagist: https://packagist.org/packages/symfony/options-resolver -.. _CHANGELOG: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/OptionsResolver/CHANGELOG.md#260 diff --git a/components/phpunit_bridge.rst b/components/phpunit_bridge.rst index 6981f3b1e1d..c187e59eded 100644 --- a/components/phpunit_bridge.rst +++ b/components/phpunit_bridge.rst @@ -21,19 +21,19 @@ It comes with the following features: * Displays the stack trace of a deprecation on-demand; * Provides a ``ClockMock``, ``DnsMock`` and ``ClassExistsMock`` classes for tests - sensitive to time, network or class existence. + sensitive to time, network or class existence; * Provides a modified version of PHPUnit that allows 1. separating the dependencies of your app from those of phpunit to prevent any unwanted constraints to apply; 2. running tests in parallel when a test suite is split - in several phpunit.xml files; 3. recording and replaying skipped tests. + in several phpunit.xml files; 3. recording and replaying skipped tests; Installation ------------ .. code-block:: terminal - $ composer require --dev "symfony/phpunit-bridge:*" + $ composer require --dev symfony/phpunit-bridge .. include:: /components/require_autoload.rst.inc @@ -128,6 +128,32 @@ The summary includes: +Running Tests in Parallel +------------------------- + +The modified PHPUnit script allows running tests in parallel by providing +a directory containing multiple test suites with their own ``phpunit.xml.dist``. + +.. code-block:: terminal + + ├── tests/ + │   ├── Functional/ + │   │   ├── ... + │   │   └── phpunit.xml.dist + │   ├── Unit/ + │   │   ├── ... + │   │   └── phpunit.xml.dist + +.. code-block:: terminal + + $ ./vendor/bin/simple-phpunit tests/ + +The modified PHPUnit script will recursively go through the provided directory, +up to a depth of 3 subfolders or the value specified by the environment variable +``SYMFONY_PHPUNIT_MAX_DEPTH``, looking for ``phpunit.xml.dist`` files and then +running each suite it finds in parallel, collecting their output and displaying +each test suite's results in their own section. + Trigger Deprecation Notices --------------------------- @@ -183,47 +209,95 @@ message, enclosed with ``/``. For example, with: - + -PHPUnit_ will stop your test suite once a deprecation notice is triggered whose +`PHPUnit`_ will stop your test suite once a deprecation notice is triggered whose message contains the ``"foobar"`` string. Making Tests Fail ~~~~~~~~~~~~~~~~~ -By default, any non-legacy-tagged or any non-`@-silenced`_ deprecation notices -will make tests fail. Alternatively, setting ``SYMFONY_DEPRECATIONS_HELPER`` to -an arbitrary value (ex: ``320``) will make the tests fails only if a higher -number of deprecation notices is reached (``0`` is the default value). You can -also set the value ``"weak"`` which will make the bridge ignore any deprecation -notices. This is useful to projects that must use deprecated interfaces for -backward compatibility reasons. +By default, any non-legacy-tagged or any non-`@-silenced`_ deprecation +notices will make tests fail. Alternatively, you can configure an +arbitrary threshold by setting ``SYMFONY_DEPRECATIONS_HELPER`` to +``max[total]=320`` for instance. It will make the tests fails only if a +higher number of deprecation notices is reached (``0`` is the default +value). + +You can have even finer-grained control by using other keys of the ``max`` +array, which are ``self``, ``direct``, and ``indirect``. The +``SYMFONY_DEPRECATIONS_HELPER`` environment variable accepts an URL-encoded +string, meaning you can combine thresholds and any other configuration setting, +like this: ``SYMFONY_DEPRECATIONS_HELPER=max[total]=42&max[self]=0&verbose=0`` + +Internal deprecations +..................... When you maintain a library, having the test suite fail as soon as a dependency introduces a new deprecation is not desirable, because it shifts the burden of -fixing that deprecation to any contributor that happens to submit a pull -request shortly after a new vendor release is made with that deprecation. To -mitigate this, you can either use tighter requirements, in the hope that +fixing that deprecation to any contributor that happens to submit a pull request +shortly after a new vendor release is made with that deprecation. + +To mitigate this, you can either use tighter requirements, in the hope that dependencies will not introduce deprecations in a patch version, or even commit -the Composer lock file, which would create another class of issues. Libraries -will often use ``SYMFONY_DEPRECATIONS_HELPER=weak`` because of this. This has -the drawback of allowing contributions that introduce deprecations but: +the ``composer.lock`` file, which would create another class of issues. +Libraries will often use ``SYMFONY_DEPRECATIONS_HELPER=max[total]=999999`` +because of this. This has the drawback of allowing contributions that introduce +deprecations but: * forget to fix the deprecated calls if there are any; * forget to mark appropriate tests with the ``@group legacy`` annotations. -By using the ``"weak_vendors"`` value, deprecations that are triggered outside -the ``vendors`` directory will make the test suite fail, while deprecations -triggered from a library inside it will not, giving you the best of both -worlds. +By using ``SYMFONY_DEPRECATIONS_HELPER=max[self]=0``, deprecations that are +triggered outside the ``vendors`` directory will be accounted for seperately, +while deprecations triggered from a library inside it will not (unless you reach +999999 of these), giving you the best of both worlds. + +Direct and Indirect Deprecations +................................ + +When working on a project, you might be more interested in ``max[direct]``. +Let's say you want to fix deprecations as soon as they appear. A problem many +developers experience is that some dependencies they have tend to lag behind +their own dependencies, meaning they do not fix deprecations as soon as +possible, which means you should create a pull request on the outdated vendor, +and ignore these deprecations until your pull request is merged. + +The ``max[direct]`` config allows you to put a threshold on direct deprecations +only, allowing you to notice when *your code* is using deprecated APIs, and to +keep up with the changes. You can still use ``max[indirect]`` if you want to +keep indirect deprecations under a given threshold. + +Here is a summary that should help you pick the right configuration: + ++------------------------+-----------------------------------------------------+ +| Value | Recommended situation | ++========================+=====================================================+ +| max[total]=0 | Recommended for actively maintained projects | +| | with robust/no dependencies | ++------------------------+-----------------------------------------------------+ +| max[direct]=0 | Recommended for projects with dependencies | +| | that fail to keep up with new deprecations. | ++------------------------+-----------------------------------------------------+ +| max[self]=0 | Recommended for libraries that use | +| | the deprecation system themselves and | +| | cannot afford to use one of the modes above. | ++------------------------+-----------------------------------------------------+ + +Disabling the Verbose Output +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the bridge will display a detailed output with the number of +deprecations and where they arise. If this is too much for you, you can use +``SYMFONY_DEPRECATIONS_HELPER=verbose=0`` to turn the verbose output off. Disabling the Deprecation Helper ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Set the ``SYMFONY_DEPRECATIONS_HELPER`` environment variable to ``disabled`` to -completely disable the deprecation helper. This is useful to make use of the +Set the ``SYMFONY_DEPRECATIONS_HELPER`` environment variable to ``disabled=1`` +to completely disable the deprecation helper. This is useful to make use of the rest of features provided by this component without getting errors or messages related to deprecations. @@ -251,6 +325,10 @@ class autoloading time. This can be disabled with the ``debug-class-loader`` opt +.. versionadded:: 4.2 + + The ``DebugClassLoader`` integration was introduced in Symfony 4.2. + Write Assertions about Deprecations ----------------------------------- @@ -331,11 +409,15 @@ Clock Mocking ~~~~~~~~~~~~~ The :class:`Symfony\\Bridge\\PhpUnit\\ClockMock` class provided by this bridge -allows you to mock the PHP's built-in time functions ``time()``, -``microtime()``, ``sleep()`` and ``usleep()``. Additionally the function -``date()`` is mocked so it uses the mocked time if no timestamp is specified. +allows you to mock the PHP's built-in time functions ``time()``, ``microtime()``, +``sleep()``, ``usleep()`` and ``gmdate()``. Additionally the function ``date()`` +is mocked so it uses the mocked time if no timestamp is specified. + Other functions with an optional timestamp parameter that defaults to ``time()`` -will still use the system time instead of the mocked time. +will still use the system time instead of the mocked time. This means that you +may need to change some code in your tests. For example, instead of ``new DateTime()``, +you should use ``DateTime::createFromFormat('U', time())`` to use the mocked +``time()`` function. To use the ``ClockMock`` class in your test, add the ``@group time-sensitive`` annotation to its class or methods. This annotation only works when executing @@ -515,6 +597,78 @@ conditions:: ], ]); +Class Existence Based Tests +--------------------------- + +Tests that behave differently depending on existing classes, for example Composer's +development dependencies, are often hard to test for the alternate case. For that +reason, this component also provides mocks for these PHP functions: + +* :phpfunction:`class_exists` +* :phpfunction:`interface_exists` +* :phpfunction:`trait_exists` + +Use Case +~~~~~~~~ + +Consider the following example that relies on the ``Vendor\DependencyClass`` to +toggle a behavior:: + + use Vendor\DependencyClass; + + class MyClass + { + public function hello(): string + { + if (class_exists(DependencyClass::class)) { + return 'The dependency bahavior.'; + } + + return 'The default behavior.'; + } + } + +A regular test case for ``MyClass`` (assuming the development dependencies +are installed during tests) would look like:: + + use MyClass; + use PHPUnit\Framework\TestCase; + + class MyClassTest extends TestCase + { + public function testHello() + { + $class = new MyClass(); + $result = $class->hello(); // "The dependency bahavior." + + // ... + } + } + +In order to test the default behavior instead use the +``ClassExistsMock::withMockedClasses()`` to configure the expected +classes, interfaces and/or traits for the code to run:: + + use MyClass; + use PHPUnit\Framework\TestCase; + use Vendor\DependencyClass; + + class MyClassTest extends TestCase + { + // ... + + public function testHelloDefault() + { + ClassExistsMock::register(MyClass::class); + ClassExistsMock::withMockedClasses([DependencyClass::class => false]); + + $class = new MyClass(); + $result = $class->hello(); // "The default bahavior." + + // ... + } + } + Troubleshooting --------------- @@ -721,12 +875,11 @@ not find the SUT: -.. _PHPUnit: https://phpunit.de +.. _`PHPUnit`: https://phpunit.de .. _`PHPUnit event listener`: https://phpunit.de/manual/current/en/extending-phpunit.html#extending-phpunit.PHPUnit_Framework_TestListener .. _`PHPUnit's assertStringMatchesFormat()`: https://phpunit.de/manual/current/en/appendixes.assertions.html#appendixes.assertions.assertStringMatchesFormat .. _`PHP error handler`: https://php.net/manual/en/book.errorfunc.php .. _`environment variable`: https://phpunit.de/manual/current/en/appendixes.configuration.html#appendixes.configuration.php-ini-constants-variables -.. _Packagist: https://packagist.org/packages/symfony/phpunit-bridge .. _`@-silencing operator`: https://php.net/manual/en/language.operators.errorcontrol.php .. _`@-silenced`: https://php.net/manual/en/language.operators.errorcontrol.php .. _`Travis CI`: https://travis-ci.org/ diff --git a/components/polyfill_iconv.rst b/components/polyfill_iconv.rst index 8801f11deca..e50b926e6bd 100644 --- a/components/polyfill_iconv.rst +++ b/components/polyfill_iconv.rst @@ -55,5 +55,3 @@ extension are installed: * :phpfunction:`iconv_mime_decode` .. _`PHP iconv extension`: https://secure.php.net/manual/en/book.iconv.php -.. _`mbstring`: https://secure.php.net/manual/en/book.mbstring.php -.. _`xml`: https://secure.php.net/manual/en/book.xml.php diff --git a/components/process.rst b/components/process.rst index 42389b5acbd..e80ecff4896 100644 --- a/components/process.rst +++ b/components/process.rst @@ -103,7 +103,8 @@ Using array of arguments is the recommended way to define commands. This saves you from any escaping and allows sending signals seamlessly (e.g. to stop processes before completion):: - $process = new Process(['/path/command', '--flag', 'arg 1', 'etc.']); + $process = new Process(['/path/command', '--option', 'argument', 'etc.']); + $process = new Process(['/path/to/php', '--define', 'memory_limit=1024M', '/path/to/script.php']); If you need to use stream redirections, conditional execution, or any other feature provided by the shell of your operating system, you can also define @@ -167,6 +168,12 @@ anonymous function to the } }); +.. note:: + + This feature won't work as expected in servers using PHP output buffering. + In those cases, either disable the `output_buffering`_ PHP option or use the + :phpfunction:`ob_flush` PHP function to force sending the output buffer. + Running Processes Asynchronously -------------------------------- @@ -363,7 +370,7 @@ a different timeout (in seconds) to the ``setTimeout()`` method:: $process->run(); If the timeout is reached, a -:class:`Symfony\\Component\\Process\\Exception\\RuntimeException` is thrown. +:class:`Symfony\\Component\\Process\\Exception\\ProcessTimedOutException` is thrown. For long running commands, it is your responsibility to perform the timeout check regularly:: @@ -474,11 +481,7 @@ whether `TTY`_ is supported on the current operating system:: $process = (new Process())->setTty(Process::isTtySupported()); -.. _`Symfony Issue#5759`: https://github.com/symfony/symfony/issues/5759 -.. _`PHP Bug#39992`: https://bugs.php.net/bug.php?id=39992 -.. _`exec`: https://en.wikipedia.org/wiki/Exec_(operating_system) .. _`pid`: https://en.wikipedia.org/wiki/Process_identifier -.. _`PHP Documentation`: https://php.net/manual/en/pcntl.constants.php -.. _Packagist: https://packagist.org/packages/symfony/process .. _`PHP streams`: https://www.php.net/manual/en/book.stream.php +.. _`output_buffering`: https://www.php.net/manual/en/outcontrol.configuration.php .. _`TTY`: https://en.wikipedia.org/wiki/Tty_(unix) diff --git a/components/property_access.rst b/components/property_access.rst index 50524056679..81aed5ab2c2 100644 --- a/components/property_access.rst +++ b/components/property_access.rst @@ -166,6 +166,36 @@ getters, this means that you can do something like this:: This will produce: ``He is an author`` +Accessing a non Existing Property Path +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 4.3 + + The ``disableExceptionOnInvalidPropertyPath()`` method was introduced in + Symfony 4.3. + +By default a :class:`Symfony\\Component\\PropertyAccess\\Exception\\NoSuchPropertyException` +is thrown if the property path passed to :method:`PropertyAccessor::getValue` +does not exist. You can change this behavior using the +:method:`Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder::disableExceptionOnInvalidPropertyPath` +method:: + + // ... + class Person + { + public $name; + } + + $person = new Person(); + + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() + ->disableExceptionOnInvalidPropertyPath() + ->getPropertyAccessor(); + + // instead of throwing an exception the following code returns null + $value = $propertyAccessor->getValue($person, 'birthday'); + + Magic ``__get()`` Method ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -461,5 +491,4 @@ Or you can pass parameters directly to the constructor (not the recommended way) // ... $propertyAccessor = new PropertyAccessor(true); // this enables handling of magic __call -.. _Packagist: https://packagist.org/packages/symfony/property-access .. _The Inflector component: https://github.com/symfony/inflector diff --git a/components/property_info.rst b/components/property_info.rst index cb86db3b49c..5f1c56826d6 100644 --- a/components/property_info.rst +++ b/components/property_info.rst @@ -497,7 +497,6 @@ service by defining it as a service with one or more of the following * ``property_info.description_extractor`` if it provides description information. * ``property_info.access_extractor`` if it provides access information. -.. _Packagist: https://packagist.org/packages/symfony/property-info .. _`phpDocumentor Reflection`: https://github.com/phpDocumentor/ReflectionDocBlock .. _`phpdocumentor/reflection-docblock`: https://packagist.org/packages/phpdocumentor/reflection-docblock .. _`Doctrine ORM`: https://www.doctrine-project.org/projects/orm.html diff --git a/components/routing.rst b/components/routing.rst index a621a3b4f3d..22b9ea9f342 100644 --- a/components/routing.rst +++ b/components/routing.rst @@ -6,7 +6,8 @@ The Routing Component ===================== The Routing component maps an HTTP request to a set of configuration - variables. + variables. It's used to build routing systems for web applications where + each URL is associated with some code to execute. Installation ------------ @@ -20,45 +21,51 @@ Installation Usage ----- -.. seealso:: +The main :doc:`Symfony routing ` article explains all the features of +this component when used inside a Symfony application. This article only +explains the things you need to do to use it in a non-Symfony PHP application. - This article explains how to use the Routing features as an independent - component in any PHP application. Read the :doc:`/routing` article to learn - about how to use it in Symfony applications. +Routing System Setup +-------------------- -In order to set up a basic routing system you need three parts: +A routing system has three parts: -* A :class:`Symfony\\Component\\Routing\\RouteCollection`, which contains the route definitions (instances of the class :class:`Symfony\\Component\\Routing\\Route`) -* A :class:`Symfony\\Component\\Routing\\RequestContext`, which has information about the request -* A :class:`Symfony\\Component\\Routing\\Matcher\\UrlMatcher`, which performs the mapping of the path to a single route +* A :class:`Symfony\\Component\\Routing\\RouteCollection`, which contains the + route definitions (instances of the class :class:`Symfony\\Component\\Routing\\Route`); +* A :class:`Symfony\\Component\\Routing\\RequestContext`, which has information + about the request; +* A :class:`Symfony\\Component\\Routing\\Matcher\\UrlMatcher`, which performs + the mapping of the path to a single route. -Here is a quick example. Notice that this assumes that you've already configured -your autoloader to load the Routing component:: +Here is a quick example:: + use App\Controller\BlogController; use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; - $route = new Route('/foo', ['_controller' => 'MyController']); + $route = new Route('/blog/{slug}', ['_controller' => BlogController::class]) $routes = new RouteCollection(); - $routes->add('route_name', $route); + $routes->add('blog_show', $route); $context = new RequestContext('/'); + // Routing can match routes with incoming requests $matcher = new UrlMatcher($routes, $context); + $parameters = $matcher->match('/blog/lorem-ipsum'); + // $parameters = [ + // '_controller' => 'App\Controller\BlogController', + // 'slug' => 'lorem-ipsum', + // '_route' => 'blog_show' + // ] - $parameters = $matcher->match('/foo'); - // ['_controller' => 'MyController', '_route' => 'route_name'] - -.. note:: - - The :class:`Symfony\\Component\\Routing\\RequestContext` parameters can be populated - with the values stored in ``$_SERVER``, but it's easier to use the HttpFoundation - component as explained :ref:`below `. - -You can add as many routes as you like to a -:class:`Symfony\\Component\\Routing\\RouteCollection`. + // Routing can also generate URLs for a given route + $generator = new UrlGenerator($routes, $context); + $url = $generator->generate('blog_show', [ + 'slug' => 'my-blog-post', + ]); + // $url = '/blog/my-blog-post' The :method:`RouteCollection::add() ` method takes two arguments. The first is the name of the route. The second @@ -68,50 +75,19 @@ of custom variables can be *anything* that's significant to your application, and is returned when that route is matched. The :method:`UrlMatcher::match() ` -returns the variables you set on the route as well as the wildcard placeholders -(see below). Your application can now use this information to continue -processing the request. In addition to the configured variables, a ``_route`` -key is added, which holds the name of the matched route. +returns the variables you set on the route as well as the route parameters. +Your application can now use this information to continue processing the request. +In addition to the configured variables, a ``_route`` key is added, which holds +the name of the matched route. If no matching route can be found, a :class:`Symfony\\Component\\Routing\\Exception\\ResourceNotFoundException` will be thrown. Defining Routes -~~~~~~~~~~~~~~~ - -A full route definition can contain up to eight parts: - -#. The URL pattern. This is matched against the URL passed to the - ``RequestContext``. It is not a regular expression, but can contain named - wildcard placeholders (e.g. ``{slug}``) to match dynamic parts in the URL. - The component will create the regular expression from it. - -#. An array of default parameters. This contains an array of arbitrary values - that will be returned when the request matches the route. It is used by - convention to map a controller to the route. - -#. An array of requirements. These define constraints for the values of the - placeholders in the pattern as regular expressions. - -#. An array of options. These contain advanced settings for the route and - can be used to control encoding or customize compilation. - See :ref:`routing-unicode-support` below. You can learn more about them by - reading :method:`Symfony\\Component\\Routing\\Route::setOptions` implementation. - -#. A host. This is matched against the host of the request. See - :doc:`/routing/hostname_pattern` for more details. - -#. An array of schemes. These enforce a certain HTTP scheme (``http``, ``https``). - -#. An array of methods. These enforce a certain HTTP request method (``HEAD``, - ``GET``, ``POST``, ...). - -#. A condition, using the :doc:`/components/expression_language/syntax`. - A string that must evaluate to ``true`` so the route matches. See - :doc:`/routing/conditions` for more details. +--------------- -Take the following route, which combines several of these ideas:: +A full route definition can contain up to eight parts:: $route = new Route( '/archive/{month}', // path @@ -137,36 +113,13 @@ Take the following route, which combines several of these ideas:: $parameters = $matcher->match('/archive/foo'); // throws ResourceNotFoundException -In this case, the route is matched by ``/archive/2012-01``, because the ``{month}`` -wildcard matches the regular expression wildcard given. However, ``/archive/foo`` -does *not* match, because "foo" fails the month wildcard. - -When using wildcards, these are returned in the array result when calling -``match``. The part of the path that the wildcard matched (e.g. ``2012-01``) is used -as value. - -A placeholder matches any character except slashes ``/`` by default, unless you define -a specific requirement for it. -The reason is that they are used by convention to separate different placeholders. - -If you want a placeholder to match anything, it must be the last of the route:: - - $route = new Route( - '/start/{required}/{anything}', - ['required' => 'default'], // should always be defined - ['anything' => '.*'] // explicit requirement to allow "/" - ); - -Learn more about it by reading :ref:`routing/slash_in_parameter`. - -Using Prefixes and Collection Settings -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Route Collections +----------------- You can add routes or other instances of :class:`Symfony\\Component\\Routing\\RouteCollection` to *another* collection. -This way you can build a tree of routes. Additionally you can define a prefix -and default values for the parameters, requirements, options, schemes and the -host to all routes of a subtree using methods provided by the +This way you can build a tree of routes. Additionally you can define common +options for all routes of a subtree using methods provided by the ``RouteCollection`` class:: $rootCollection = new RouteCollection(); @@ -185,8 +138,8 @@ host to all routes of a subtree using methods provided by the $rootCollection->addCollection($subCollection); -Set the Request Parameters -~~~~~~~~~~~~~~~~~~~~~~~~~~ +Setting the Request Parameters +------------------------------ The :class:`Symfony\\Component\\Routing\\RequestContext` provides information about the current request. You can define all parameters of an HTTP request @@ -216,67 +169,15 @@ Normally you can pass the values from the ``$_SERVER`` variable to populate the $context = new RequestContext(); $context->fromRequest(Request::createFromGlobals()); -Generate a URL -~~~~~~~~~~~~~~ - -While the :class:`Symfony\\Component\\Routing\\Matcher\\UrlMatcher` tries -to find a route that fits the given request you can also build a URL from -a certain route with the :class:`Symfony\\Component\\Routing\\Generator\\UrlGenerator`:: - - use Symfony\Component\Routing\Generator\UrlGenerator; - use Symfony\Component\Routing\RequestContext; - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('show_post', new Route('/show/{slug}')); - - $context = new RequestContext('/'); +Loading Routes +-------------- - $generator = new UrlGenerator($routes, $context); +The Routing component comes with a number of loader classes, each giving you the +ability to load a collection of route definitions from external resources. - $url = $generator->generate('show_post', [ - 'slug' => 'my-blog-post', - ]); - // /show/my-blog-post +File Routing Loaders +~~~~~~~~~~~~~~~~~~~~ -.. note:: - - If you have defined a scheme, an absolute URL is generated if the scheme - of the current :class:`Symfony\\Component\\Routing\\RequestContext` does - not match the requirement. - -Check if a Route Exists -~~~~~~~~~~~~~~~~~~~~~~~ - -In highly dynamic applications, it may be necessary to check whether a route -exists before using it to generate a URL. In those cases, don't use the -:method:`Symfony\\Component\\Routing\\Router::getRouteCollection` method because -that regenerates the routing cache and slows down the application. - -Instead, try to generate the URL and catch the -:class:`Symfony\\Component\\Routing\\Exception\\RouteNotFoundException` thrown -when the route doesn't exist:: - - use Symfony\Component\Routing\Exception\RouteNotFoundException; - - // ... - - try { - $url = $generator->generate($dynamicRouteName, $parameters); - } catch (RouteNotFoundException $e) { - // the route is not defined... - } - -Load Routes from a File -~~~~~~~~~~~~~~~~~~~~~~~ - -You've already seen how you can add routes to a collection right inside -PHP. But you can also load routes from a number of different files. - -The Routing component comes with a number of loader classes, each giving -you the ability to load a collection of route definitions from an external -file of some format. Each loader expects a :class:`Symfony\\Component\\Config\\FileLocator` instance as the constructor argument. You can use the :class:`Symfony\\Component\\Config\\FileLocator` to define an array of paths in which the loader will look for the requested files. @@ -291,7 +192,6 @@ If you're using the ``YamlFileLoader``, then route definitions look like this: path: /foo controller: MyController::fooAction methods: GET|HEAD - route2: path: /foo/bar controller: FooBarInvokableController @@ -315,7 +215,8 @@ other loaders that work the same way: * :class:`Symfony\\Component\\Routing\\Loader\\PhpFileLoader` If you use the :class:`Symfony\\Component\\Routing\\Loader\\PhpFileLoader` you -have to provide the name of a PHP file which returns a callable handling a :class:`Symfony\\Component\\Routing\\Loader\\Configurator\\RoutingConfigurator`. +have to provide the name of a PHP file which returns a callable handling a +:class:`Symfony\\Component\\Routing\\Loader\\Configurator\\RoutingConfigurator`. This class allows to chain imports, collections or simple route definition calls:: // RouteProvider.php @@ -328,8 +229,8 @@ This class allows to chain imports, collections or simple route definition calls ; }; -Routes as Closures -.................. +Closure Routing Loaders +~~~~~~~~~~~~~~~~~~~~~~~ There is also the :class:`Symfony\\Component\\Routing\\Loader\\ClosureLoader`, which calls a closure and uses the result as a :class:`Symfony\\Component\\Routing\\RouteCollection`:: @@ -343,14 +244,28 @@ calls a closure and uses the result as a :class:`Symfony\\Component\\Routing\\Ro $loader = new ClosureLoader(); $routes = $loader->load($closure); -Routes as Annotations -..................... +Annotation Routing Loaders +~~~~~~~~~~~~~~~~~~~~~~~~~~ Last but not least there are :class:`Symfony\\Component\\Routing\\Loader\\AnnotationDirectoryLoader` and :class:`Symfony\\Component\\Routing\\Loader\\AnnotationFileLoader` to load -route definitions from class annotations. The specific details are left -out here. +route definitions from class annotations:: + + use Doctrine\Common\Annotations\AnnotationReader; + use Symfony\Bundle\FrameworkBundle\Routing\AnnotatedRouteControllerLoader; + use Symfony\Component\Config\FileLocator; + use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader; + + $loader = new AnnotationDirectoryLoader( + new FileLocator(__DIR__.'/app/controllers/'), + new AnnotatedRouteControllerLoader( + new AnnotationReader() + ) + ); + + $routes = $loader->load(__DIR__.'/app/controllers/'); + // ... .. include:: /_includes/_annotation_loader_tip.rst.inc @@ -392,152 +307,6 @@ automatically in the background if you want to use it. A basic example of the are saved in the ``cache_dir``. This means your script must have write permissions for that location. -.. _routing-unicode-support: - -Unicode Routing Support -~~~~~~~~~~~~~~~~~~~~~~~ - -The Routing component supports UTF-8 characters in route paths and requirements. -Thanks to the ``utf8`` route option, you can make Symfony match and generate -routes with UTF-8 characters: - -.. configuration-block:: - - .. code-block:: php-annotations - - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class DefaultController extends AbstractController - { - /** - * @Route("/category/{name}", name="route1", options={"utf8": true}) - */ - public function category() - { - // ... - } - - .. code-block:: yaml - - route1: - path: /category/{name} - controller: App\Controller\DefaultController::category - options: - utf8: true - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // config/routes.php - use App\Controller\DefaultController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('route1', '/category/{name}') - ->controller([DefaultController::class, 'category']) - ->options([ - 'utf8' => true, - ]) - ; - }; - -In this route, the ``utf8`` option set to ``true`` makes Symfony consider the -``.`` requirement to match any UTF-8 characters instead of just a single -byte character. This means that so the following URLs would match: -``/category/日本語``, ``/category/فارسی``, ``/category/한국어``, etc. In case you -are wondering, this option also allows to include and match emojis in URLs. - -You can also include UTF-8 strings as routing requirements: - -.. configuration-block:: - - .. code-block:: php-annotations - - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class DefaultController extends AbstractController - { - /** - * @Route( - * "/category/{name}", - * name="route2", - * defaults={"name": "한국어"}, - * options={"utf8": true} - * ) - */ - public function category() - { - // ... - } - - .. code-block:: yaml - - route2: - path: /category/{name} - controller: App\Controller\DefaultController::category - defaults: - name: "한국어" - options: - utf8: true - - .. code-block:: xml - - - - - - 한국어 - - - - - .. code-block:: php - - // config/routes.php - use App\Controller\DefaultController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('route2', '/category/{name}') - ->controller([DefaultController::class, 'category']) - ->defaults([ - 'name' => '한국어', - ]) - ->options([ - 'utf8' => true, - ]) - ; - }; - -.. tip:: - - In addition to UTF-8 characters, the Routing component also supports all - the `PCRE Unicode properties`_, which are escape sequences that match - generic character types. For example, ``\p{Lu}`` matches any uppercase - character in any language, ``\p{Greek}`` matches any Greek character, - ``\P{Han}`` matches any character not included in the Chinese Han script. - Learn more ---------- @@ -549,6 +318,3 @@ Learn more /routing/* /controller /controller/* - -.. _Packagist: https://packagist.org/packages/symfony/routing -.. _PCRE Unicode properties: http://php.net/manual/en/regexp.reference.unicode.php diff --git a/components/security.rst b/components/security.rst index 6df0b4fc531..ee6c1580472 100644 --- a/components/security.rst +++ b/components/security.rst @@ -57,5 +57,4 @@ Learn More /reference/configuration/security /reference/constraints/UserPassword -.. _Packagist: https://packagist.org/packages/symfony/security .. _`CSRF attacks`: https://en.wikipedia.org/wiki/Cross-site_request_forgery diff --git a/components/security/authentication.rst b/components/security/authentication.rst index 80bd3991ecf..849ea53a992 100644 --- a/components/security/authentication.rst +++ b/components/security/authentication.rst @@ -13,7 +13,7 @@ an *authenticated* token if the supplied credentials were found to be valid. The listener should then store the authenticated token using :class:`the token storage `:: - use Symfony\Component\HttpKernel\Event\GetResponseEvent; + use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; @@ -38,7 +38,7 @@ The listener should then store the authenticated token using // ... - public function handle(GetResponseEvent $event) + public function handle(RequestEvent $event) { $request = $event->getRequest(); @@ -276,14 +276,15 @@ Authentication Events The security component provides 4 related authentication events: -=============================== ================================================ ============================================================================== -Name Event Constant Argument Passed to the Listener -=============================== ================================================ ============================================================================== -security.authentication.success ``AuthenticationEvents::AUTHENTICATION_SUCCESS`` :class:`Symfony\\Component\\Security\\Core\\Event\\AuthenticationEvent` -security.authentication.failure ``AuthenticationEvents::AUTHENTICATION_FAILURE`` :class:`Symfony\\Component\\Security\\Core\\Event\\AuthenticationFailureEvent` -security.interactive_login ``SecurityEvents::INTERACTIVE_LOGIN`` :class:`Symfony\\Component\\Security\\Http\\Event\\InteractiveLoginEvent` -security.switch_user ``SecurityEvents::SWITCH_USER`` :class:`Symfony\\Component\\Security\\Http\\Event\\SwitchUserEvent` -=============================== ================================================ ============================================================================== +=============================== ================================================================= ============================================================================== +Name Event Constant Argument Passed to the Listener +=============================== ================================================================= ============================================================================== +security.authentication.success ``AuthenticationEvents::AUTHENTICATION_SUCCESS`` :class:`Symfony\\Component\\Security\\Core\\Event\\AuthenticationEvent` +security.authentication.failure ``AuthenticationEvents::AUTHENTICATION_FAILURE`` :class:`Symfony\\Component\\Security\\Core\\Event\\AuthenticationFailureEvent` +security.interactive_login ``SecurityEvents::INTERACTIVE_LOGIN`` :class:`Symfony\\Component\\Security\\Http\\Event\\InteractiveLoginEvent` +security.switch_user ``SecurityEvents::SWITCH_USER`` :class:`Symfony\\Component\\Security\\Http\\Event\\SwitchUserEvent` +security.logout_on_change ``Symfony\Component\Security\Http\Event\DeauthenticatedEvent`` :class:`Symfony\\Component\\Security\\Http\\Event\\DeauthenticatedEvent` +=============================== ================================================================= ============================================================================== Authentication Success and Failure Events ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -314,10 +315,16 @@ order to give your user a welcome flash message every time they log in. The ``security.switch_user`` event is triggered every time you activate the ``switch_user`` firewall listener. +The ``Symfony\Component\Security\Http\Event\DeauthenticatedEvent`` event is triggered when a token has been deauthenticated +because of a user change, it can help you doing some clean-up task when a logout has been triggered. + +.. versionadded:: 4.3 + + The ``Symfony\Component\Security\Http\Event\DeauthenticatedEvent`` event was introduced in Symfony 4.3. + .. seealso:: For more information on switching users, see :doc:`/security/impersonating_user`. .. _`CVE-2013-5750`: https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form -.. _`BasePasswordEncoder::checkPasswordLength`: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php diff --git a/components/security/authorization.rst b/components/security/authorization.rst index f267bd149ce..52f9b8bacd4 100644 --- a/components/security/authorization.rst +++ b/components/security/authorization.rst @@ -19,7 +19,7 @@ by an instance of :class:`Symfony\\Component\\Security\\Core\\Authorization\\Acc An authorization decision will always be based on a few things: * The current token - For instance, the token's :method:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface::getRoles` + For instance, the token's :method:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface::getRoleNames` method may be used to retrieve the roles of the current user (e.g. ``ROLE_SUPER_ADMIN``), or a decision may be based on the class of the token. * A set of attributes @@ -125,7 +125,7 @@ RoleVoter The :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\RoleVoter` supports attributes starting with ``ROLE_`` and grants access to the user when the required ``ROLE_*`` attributes can all be found in the array of -roles returned by the token's :method:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface::getRoles` +roles returned by the token's :method:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface::getRoleNames` method:: use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter; @@ -200,24 +200,10 @@ expressions have access to a number of Roles ----- -Roles are objects that give expression to a certain right the user has. The only -requirement is that they must define a ``getRole()`` method that returns a -string representation of the role itself. To do so, you can optionally extend -from the default :class:`Symfony\\Component\\Security\\Core\\Role\\Role` class, -which returns its first constructor argument in this method:: - - use Symfony\Component\Security\Core\Role\Role; - - $role = new Role('ROLE_ADMIN'); - - // shows 'ROLE_ADMIN' - var_dump($role->getRole()); - -.. note:: - - Most authentication tokens extend from :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\AbstractToken`, - which means that the roles given to its constructor will be - automatically converted from strings to these simple ``Role`` objects. +Roles are strings that give expression to a certain right the user has (e.g. +*"edit a blog post"*, *"create an invoice"*). You can freely choose those +strings. The only requirement is that they must start with the ``ROLE_`` prefix +(e.g. ``ROLE_POST_EDIT``, ``ROLE_INVOICE_CREATE``). Using the Decision Manager -------------------------- diff --git a/components/serializer.rst b/components/serializer.rst index aa0c3fd8c08..4eba1055433 100644 --- a/components/serializer.rst +++ b/components/serializer.rst @@ -212,6 +212,22 @@ The serializer can also be used to update an existing object:: This is a common need when working with an ORM. +The ``OBJECT_TO_POPULATE`` is only used for the top level object. If that object +is the root of a tree structure, all child elements that exist in the +normalized data will be re-created with new instances. + +When the ``AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE`` option is set to +true, existing children of the root ``OBJECT_TO_POPULATE`` are updated from the +normalized data, instead of the denormalizer re-creating them. Note that +``DEEP_OBJECT_TO_POPULATE`` only works for single child objects, but not for +arrays of objects. Those will still be replaced when present in the normalized +data. + +.. versionadded:: 4.3 + + The ``AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE`` option was + introduced in Symfony 4.3. + .. _component-serializer-attributes-groups: Attributes Groups @@ -639,16 +655,16 @@ When serializing, you can set a callback to format a specific object property:: $encoder = new JsonEncoder(); // all callback parameters are optional (you can omit the ones you don't use) - $callback = function ($innerObject, $outerObject, string $attributeName, string $format = null, array $context = []) { + $dateCallback = function ($innerObject, $outerObject, string $attributeName, string $format = null, array $context = []) { return $innerObject instanceof \DateTime ? $innerObject->format(\DateTime::ISO8601) : ''; }; $defaultContext = [ AbstractNormalizer::CALLBACKS => [ - 'createdAt' => $callback, + 'createdAt' => $dateCallback, ], ]; - + $normalizer = new GetSetMethodNormalizer(null, null, null, null, null, $defaultContext); $serializer = new Serializer([$normalizer], [$encoder]); @@ -719,7 +735,7 @@ There are several types of normalizers available: :class:`Symfony\\Component\\Serializer\\Normalizer\\DateTimeNormalizer` This normalizer converts :phpclass:`DateTimeInterface` objects (e.g. :phpclass:`DateTime` and :phpclass:`DateTimeImmutable`) into strings. - By default, it uses the RFC3339_ format. + By default, it uses the `RFC3339`_ format. :class:`Symfony\\Component\\Serializer\\Normalizer\\DataUriNormalizer` This normalizer converts :phpclass:`SplFileInfo` objects into a data URI @@ -760,17 +776,17 @@ Built-in Encoders The Serializer component provides several built-in encoders: :class:`Symfony\\Component\\Serializer\\Encoder\\JsonEncoder` - This class encodes and decodes data in JSON_. + This class encodes and decodes data in `JSON`_. :class:`Symfony\\Component\\Serializer\\Encoder\\XmlEncoder` - This class encodes and decodes data in XML_. + This class encodes and decodes data in `XML`_. :class:`Symfony\\Component\\Serializer\\Encoder\\YamlEncoder` - This encoder encodes and decodes data in YAML_. This encoder requires the + This encoder encodes and decodes data in `YAML`_. This encoder requires the :doc:`Yaml Component `. :class:`Symfony\\Component\\Serializer\\Encoder\\CsvEncoder` - This encoder encodes and decodes data in CSV_. + This encoder encodes and decodes data in `CSV`_. All these encoders are enabled by default when using the Serializer component in a Symfony application. @@ -1476,7 +1492,6 @@ Learn more .. _`PSR-1 standard`: https://www.php-fig.org/psr/psr-1/ .. _`JMS serializer`: https://github.com/schmittjoh/serializer -.. _Packagist: https://packagist.org/packages/symfony/serializer .. _RFC3339: https://tools.ietf.org/html/rfc3339#section-5.8 .. _JSON: http://www.json.org/ .. _XML: https://www.w3.org/XML/ diff --git a/components/stopwatch.rst b/components/stopwatch.rst index 96cb5922b79..a000bca1be8 100644 --- a/components/stopwatch.rst +++ b/components/stopwatch.rst @@ -119,5 +119,3 @@ method and specifying the id of the section to be reopened:: $stopwatch->openSection('routing'); $stopwatch->start('building_config_tree'); $stopwatch->stopSection('routing'); - -.. _Packagist: https://packagist.org/packages/symfony/stopwatch diff --git a/components/templating.rst b/components/templating.rst index dd7c6bca16b..a104ea3e1d8 100644 --- a/components/templating.rst +++ b/components/templating.rst @@ -28,8 +28,8 @@ Usage .. seealso:: This article explains how to use the Templating features as an independent - component in any PHP application. Read the :doc:`/templating` article to - learn about how to work with templates in Symfony applications. + component in any PHP application. Read the article about :doc:`templates ` + to learn about how to work with templates in Symfony applications. The :class:`Symfony\\Component\\Templating\\PhpEngine` class is the entry point of the component. It needs a @@ -211,7 +211,5 @@ Learn More :glob: /components/templating/* - /templating + /templates /templating/* - -.. _Packagist: https://packagist.org/packages/symfony/templating diff --git a/components/translation.rst b/components/translation.rst index cfac5c97d80..fe7ab4c3d79 100644 --- a/components/translation.rst +++ b/components/translation.rst @@ -232,6 +232,5 @@ Learn More /translation/* /validation/translations -.. _Packagist: https://packagist.org/packages/symfony/translation .. _`ISO 3166-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes .. _`ISO 639-1`: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes diff --git a/components/translation/custom_message_formatter.rst b/components/translation/custom_message_formatter.rst new file mode 100644 index 00000000000..81e40a4c544 --- /dev/null +++ b/components/translation/custom_message_formatter.rst @@ -0,0 +1,38 @@ +.. index:: + single: Translation; Create Custom Message formatter + +Create a Custom Message Formatter +================================= + +The default message formatter provided by Symfony solves the most common needs +when translating messages, such as using variables and pluralization. However, +if your needs are different, you can create your own message formatter. + +Message formatters are PHP classes that implement the +:class:`Symfony\\Component\\Translation\\Formatter\\MessageFormatterInterface`:: + + + use Symfony\Component\Translation\Formatter\MessageFormatterInterface; + + class MyCustomMessageFormatter implements MessageFormatterInterface + { + public function format($message, $locale, array $parameters = []) + { + // ... format the message according to your needs + + return $message; + } + } + +Now, pass an instance of this formatter as the second argument of the translator +to use it when translating messages:: + + use Symfony\Component\Translation\Translator; + + $translator = new Translator('fr_FR', new IntlMessageFormatter()); + $message = $translator->trans($originalMessage, $translationParameters); + +If you want to use this formatter to translate all messages in your Symfony +application, define a service for the formatter and use the +:ref:`translator.formatter ` option +to set that service as the default formatter. diff --git a/components/translation/usage.rst b/components/translation/usage.rst index 40c883e30f2..5bf2b2df6de 100644 --- a/components/translation/usage.rst +++ b/components/translation/usage.rst @@ -91,7 +91,10 @@ recommended format. These files are parsed by one of the loader classes. read "Symfony is really great" in the default locale. The choice of which method to use is entirely up to you, but the "keyword" - format is often recommended. + format is often recommended for multi-language applications, whereas for + shared bundles that contain translation resources we recommend the real + message, so you application can choose to disable the translator layer + and you will see a readable message. Additionally, the ``php`` and ``yaml`` file formats support nested ids to avoid repeating yourself if you use keywords instead of real text for your @@ -265,4 +268,3 @@ code needed to generate the previous XLIFF file:: ]); .. _`L10n`: https://en.wikipedia.org/wiki/Internationalization_and_localization -.. _`ISO 31-11`: https://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals diff --git a/components/validator.rst b/components/validator.rst index afd1865e64c..9028f8d6874 100644 --- a/components/validator.rst +++ b/components/validator.rst @@ -89,4 +89,3 @@ Learn More /validation/* .. _`JSR-303 Bean Validation specification`: http://jcp.org/en/jsr/detail?id=303 -.. _Packagist: https://packagist.org/packages/symfony/validator diff --git a/components/validator/resources.rst b/components/validator/resources.rst index 9e14cf8c073..a7cf3678c9d 100644 --- a/components/validator/resources.rst +++ b/components/validator/resources.rst @@ -192,5 +192,3 @@ You can set this custom implementation using Since you are using a custom metadata factory, you can't configure loaders and caches using the ``add*Mapping()`` methods anymore. You now have to inject them into your custom metadata factory yourself. - -.. _`Packagist`: https://packagist.org diff --git a/components/var_dumper.rst b/components/var_dumper.rst index 1ed83baff82..1c48a021855 100644 --- a/components/var_dumper.rst +++ b/components/var_dumper.rst @@ -406,5 +406,3 @@ Learn More :glob: var_dumper/* - -.. _Packagist: https://packagist.org/packages/symfony/var-dumper diff --git a/components/var_dumper/advanced.rst b/components/var_dumper/advanced.rst index 18db7465fa2..c90d94e6cdb 100644 --- a/components/var_dumper/advanced.rst +++ b/components/var_dumper/advanced.rst @@ -217,19 +217,22 @@ output produced by the different casters. If ``DUMP_STRING_LENGTH`` is set, then the length of a string is displayed next to its content:: + use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\AbstractDumper; use Symfony\Component\VarDumper\Dumper\CliDumper; + $varCloner = new VarCloner(); $var = ['test']; + $dumper = new CliDumper(); - echo $dumper->dump($var, true); + echo $dumper->dump($varCloner->cloneVar($var), true); // array:1 [ // 0 => "test" // ] $dumper = new CliDumper(null, null, AbstractDumper::DUMP_STRING_LENGTH); - echo $dumper->dump($var, true); + echo $dumper->dump($varCloner->cloneVar($var), true); // (added string length before the string) // array:1 [ @@ -239,19 +242,22 @@ next to its content:: If ``DUMP_LIGHT_ARRAY`` is set, then arrays are dumped in a shortened format similar to PHP's short array notation:: + use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\AbstractDumper; use Symfony\Component\VarDumper\Dumper\CliDumper; + $varCloner = new VarCloner(); $var = ['test']; + $dumper = new CliDumper(); - echo $dumper->dump($var, true); + echo $dumper->dump($varCloner->cloneVar($var), true); // array:1 [ // 0 => "test" // ] $dumper = new CliDumper(null, null, AbstractDumper::DUMP_LIGHT_ARRAY); - echo $dumper->dump($var, true); + echo $dumper->dump($varCloner->cloneVar($var), true); // (no more array:1 prefix) // [ @@ -261,12 +267,15 @@ similar to PHP's short array notation:: If you would like to use both options, then you can combine them by using the logical OR operator ``|``:: + use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\AbstractDumper; use Symfony\Component\VarDumper\Dumper\CliDumper; + $varCloner = new VarCloner(); $var = ['test']; + $dumper = new CliDumper(null, null, AbstractDumper::DUMP_STRING_LENGTH | AbstractDumper::DUMP_LIGHT_ARRAY); - echo $dumper->dump($var, true); + echo $dumper->dump($varCloner->cloneVar($var), true); // [ // 0 => (4) "test" diff --git a/components/var_exporter.rst b/components/var_exporter.rst index 4fea8905484..6b403f79263 100644 --- a/components/var_exporter.rst +++ b/components/var_exporter.rst @@ -128,5 +128,5 @@ created by using the special ``"\0"`` property name to define their internal val "\0" => [$inputArray] ]); -.. _`OPCache`: https://php.net/opcache +.. _`OPcache`: https://php.net/opcache .. _`PSR-2`: https://www.php-fig.org/psr/psr-2/ diff --git a/components/workflow.rst b/components/workflow.rst index 3d3ac24cc2c..f04571d19b4 100644 --- a/components/workflow.rst +++ b/components/workflow.rst @@ -32,28 +32,30 @@ a ``Definition`` and a way to write the states to the objects (i.e. an instance of a :class:`Symfony\\Component\\Workflow\\MarkingStore\\MarkingStoreInterface`). Consider the following example for a blog post. A post can have one of a number -of predefined statuses (`draft`, `review`, `rejected`, `published`). In a workflow, +of predefined statuses (`draft`, `reviewed`, `rejected`, `published`). In a workflow, these statuses are called **places**. You can define the workflow like this:: use Symfony\Component\Workflow\DefinitionBuilder; - use Symfony\Component\Workflow\MarkingStore\SingleStateMarkingStore; + use Symfony\Component\Workflow\MarkingStore\MethodMarkingStore; use Symfony\Component\Workflow\Transition; use Symfony\Component\Workflow\Workflow; $definitionBuilder = new DefinitionBuilder(); - $definition = $definitionBuilder->addPlaces(['draft', 'review', 'rejected', 'published']) + $definition = $definitionBuilder->addPlaces(['draft', 'reviewed', 'rejected', 'published']) // Transitions are defined with a unique name, an origin place and a destination place - ->addTransition(new Transition('to_review', 'draft', 'review')) - ->addTransition(new Transition('publish', 'review', 'published')) - ->addTransition(new Transition('reject', 'review', 'rejected')) + ->addTransition(new Transition('to_review', 'draft', 'reviewed')) + ->addTransition(new Transition('publish', 'reviewed', 'published')) + ->addTransition(new Transition('reject', 'reviewed', 'rejected')) ->build() ; - $marking = new SingleStateMarkingStore('currentState'); + $singleState = true; // true if the subject can be in only one state at a given time + $property = 'currentState'; // subject property name where the state is stored + $marking = new MethodMarkingStore($singleState, $property); $workflow = new Workflow($definition, $marking); -The ``Workflow`` can now help you to decide what actions are allowed -on a blog post depending on what *place* it is in. This will keep your domain +The ``Workflow`` can now help you to decide what *transitions* (actions) are allowed +on a blog post depending on what *place* (state) it is in. This will keep your domain logic in one place and not spread all over your application. When you define multiple workflows you should consider using a ``Registry``, @@ -66,31 +68,31 @@ are trying to use it with:: use Symfony\Component\Workflow\Registry; use Symfony\Component\Workflow\SupportStrategy\InstanceOfSupportStrategy; - $blogWorkflow = ... + $blogPostWorkflow = ... $newsletterWorkflow = ... $registry = new Registry(); - $registry->addWorkflow($blogWorkflow, new InstanceOfSupportStrategy(BlogPost::class)); + $registry->addWorkflow($blogPostWorkflow, new InstanceOfSupportStrategy(BlogPost::class)); $registry->addWorkflow($newsletterWorkflow, new InstanceOfSupportStrategy(Newsletter::class)); Usage ----- When you have configured a ``Registry`` with your workflows, -you can retreive a workflow from it and use it as follows:: +you can retrieve a workflow from it and use it as follows:: // ... - // Consider that $post is in state "draft" by default - $post = new BlogPost(); - $workflow = $registry->get($post); + // Consider that $blogPost is in place "draft" by default + $blogPost = new BlogPost(); + $workflow = $registry->get($blogPost); - $workflow->can($post, 'publish'); // False - $workflow->can($post, 'to_review'); // True + $workflow->can($blogPost, 'publish'); // False + $workflow->can($blogPost, 'to_review'); // True - $workflow->apply($post, 'to_review'); // $post is now in state "review" + $workflow->apply($blogPost, 'to_review'); // $blogPost is now in place "reviewed" - $workflow->can($post, 'publish'); // True - $workflow->getEnabledTransitions($post); // ['publish', 'reject'] + $workflow->can($blogPost, 'publish'); // True + $workflow->getEnabledTransitions($blogPost); // $blogPost can perform transition "publish" or "reject" Learn more ---------- @@ -100,5 +102,3 @@ Learn more :glob: /workflow/* - -.. _Packagist: https://packagist.org/packages/symfony/workflow diff --git a/components/yaml.rst b/components/yaml.rst index c3f355ab1da..93e9f30e0fd 100644 --- a/components/yaml.rst +++ b/components/yaml.rst @@ -294,7 +294,7 @@ Date Handling By default, the YAML parser will convert unquoted strings which look like a date or a date-time into a Unix timestamp; for example ``2016-05-27`` or -``2016-05-27T02:59:43.1Z`` (ISO-8601_):: +``2016-05-27T02:59:43.1Z`` (`ISO-8601`_):: Yaml::parse('2016-05-27'); // 1464307200 @@ -440,7 +440,6 @@ Learn More yaml/* -.. _YAML: http://yaml.org/ -.. _Packagist: https://packagist.org/packages/symfony/yaml +.. _`YAML`: http://yaml.org/ .. _`YAML 1.2 version specification`: http://yaml.org/spec/1.2/spec.html -.. _ISO-8601: http://www.iso.org/iso/iso8601 +.. _`ISO-8601`: http://www.iso.org/iso/iso8601 diff --git a/components/yaml/yaml_format.rst b/components/yaml/yaml_format.rst index b8c97912bf3..c645f52c481 100644 --- a/components/yaml/yaml_format.rst +++ b/components/yaml/yaml_format.rst @@ -94,9 +94,17 @@ where each line break is replaced by a space: > This is a very long sentence - that spans several lines in the YAML - but which will be rendered as a string - without carriage returns. + that spans several lines in the YAML. + + # This will be parsed as follows: (notice the trailing \n) + # "This is a very long sentence that spans several lines in the YAML.\n" + + >- + This is a very long sentence + that spans several lines in the YAML. + + # This will be parsed as follows: (without a trailing \n) + # "This is a very long sentence that spans several lines in the YAML." .. note:: @@ -149,7 +157,7 @@ Booleans in YAML are expressed with ``true`` and ``false``. Dates ~~~~~ -YAML uses the ISO-8601 standard to express dates: +YAML uses the `ISO-8601`_ standard to express dates: .. code-block:: yaml @@ -310,8 +318,6 @@ The YAML specification defines some tags to set the type of any data explicitly: Pz7Y6OjuDg4J+fn5OTk6enp 56enmleECcgggoBADs= -.. _YAML: http://yaml.org/ - Unsupported YAML Features ------------------------- @@ -320,12 +326,13 @@ The following YAML features are not supported by the Symfony Yaml component: * Multi-documents (``---`` and ``...`` markers); * Complex mapping keys and complex values starting with ``?``; * Tagged values as keys; -* The following tags and types: `!!set`, `!!omap`, `!!pairs`, `!!set`, `!!seq`, - `!!bool`, `!!int`, `!!merge`, `!!null`, `!!timestamp`, `!!value`, `!!yaml`; +* The following tags and types: ``!!set``, ``!!omap``, ``!!pairs``, ``!!seq``, + ``!!bool``, ``!!int``, ``!!merge``, ``!!null``, ``!!timestamp``, ``!!value``, ``!!yaml``; * Tags (``TAG`` directive; example: ``%TAG ! tag:example.com,2000:app/``) and tag references (example: ``!``); * Using sequence-like syntax for mapping elements (example: ``{foo, bar}``; use ``{foo: ~, bar: ~}`` instead). +.. _`ISO-8601`: http://www.iso.org/iso/iso8601 .. _`YAML website`: http://yaml.org/ .. _`YAML specification`: http://www.yaml.org/spec/1.2/spec.html diff --git a/configuration.rst b/configuration.rst index b24ae66bb4c..cb5a6a4bd8b 100644 --- a/configuration.rst +++ b/configuration.rst @@ -4,6 +4,9 @@ Configuring Symfony =================== +Configuration Files +------------------- + Symfony applications are configured with the files stored in the ``config/`` directory, which has this default structure: @@ -27,7 +30,7 @@ stores the configuration of every package installed in your application. Packages (also called "bundles" in Symfony and "plugins/modules" in other projects) add ready-to-use features to your projects. -When using :doc:`Symfony Flex `, which is enabled by default in +When using :ref:`Symfony Flex `, which is enabled by default in Symfony applications, packages update the ``bundles.php`` file and create new files in ``config/packages/`` automatically during their installation. For example, this is the default file created by the "API Platform" package: @@ -50,7 +53,7 @@ to change these files after package installation ``config:dump-reference`` command. Configuration Formats ---------------------- +~~~~~~~~~~~~~~~~~~~~~ Unlike other frameworks, Symfony doesn't impose you a specific format to configure your applications. Symfony lets you choose between YAML, XML and PHP @@ -64,18 +67,18 @@ there's not even any performance difference between them. YAML is used by default when installing packages because it's concise and very readable. These are the main advantages and disadvantages of each format: -* :doc:`YAML `: simple, clean and readable, but - requires using a dedicated parser; -* *XML*: supports IDE autocompletion/validation and is parsed natively by PHP, - but sometimes it generates too verbose configuration; -* *PHP*: very powerful and it allows to create dynamic configuration, but the +* **YAML**: simple, clean and readable, but not all IDEs support autocompletion + and validation for it. :doc:`Learn the YAML syntax `; +* **XML**:autocompleted/validated by most IDEs and is parsed natively by PHP, + but sometimes it generates too verbose configuration. `Learn the XML syntax`_; +* **PHP**: very powerful and it allows to create dynamic configuration, but the resulting configuration is less readable than the other formats. Importing Configuration Files ------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Symfony loads configuration files using the :doc:`Config component -`, which provides advanced features such as importing +`, which provides advanced features such as importing other configuration files, even if they use a different format: .. configuration-block:: @@ -126,15 +129,9 @@ configuration files, even if they use a different format: // ... -.. tip:: - - If you use any other configuration format, you have to define your own loader - class extending it from :class:`Symfony\\Component\\DependencyInjection\\Loader\\FileLoader`. - In addition, you can define your own services to load configurations from - databases or web services. - .. _config-parameter-intro: .. _config-parameters-yml: +.. _configuration-parameters: Configuration Parameters ------------------------ @@ -152,9 +149,21 @@ reusable configuration value. By convention, parameters are defined under the parameters: # the parameter name is an arbitrary string (the 'app.' prefix is recommended # to better differentiate your parameters from Symfony parameters). - # the parameter value can be any valid scalar (numbers, strings, booleans, arrays) app.admin_email: 'something@example.com' + # boolean parameters + app.enable_v2_protocol: true + + # array/collection parameters + app.supported_locales: ['en', 'es', 'fr'] + + # binary content parameters (encode the contents with base64_encode()) + app.some_parameter: !!binary VGhpcyBpcyBhIEJlbGwgY2hhciAH + + # PHP constants as parameter values + app.some_constant: !php/const GLOBAL_CONSTANT + app.another_constant: !php/const App\Entity\BlogPost::MAX_ITEMS + # ... .. code-block:: xml @@ -171,9 +180,27 @@ reusable configuration value. By convention, parameters are defined under the + to better differentiate your parameters from Symfony parameters). --> something@example.com + + + true + + true + + + + en + es + fr + + + + VGhpcyBpcyBhIEJlbGwgY2hhciAH + + + GLOBAL_CONSTANT + App\Entity\BlogPost::MAX_ITEMS @@ -184,10 +211,37 @@ reusable configuration value. By convention, parameters are defined under the // config/services.php // the parameter name is an arbitrary string (the 'app.' prefix is recommended // to better differentiate your parameters from Symfony parameters). - // the parameter value can be any valid scalar (numbers, strings, booleans, arrays) $container->setParameter('app.admin_email', 'something@example.com'); + + // boolean parameters + $container->setParameter('app.enable_v2_protocol', true); + + // array/collection parameters + $container->setParameter('app.supported_locales', ['en', 'es', 'fr']); + + // binary content parameters (use the PHP escape sequences) + $container->setParameter('app.some_parameter', 'This is a Bell char: \x07'); + + // PHP constants as parameter values + use App\Entity\BlogPost; + + $container->setParameter('app.some_constant', GLOBAL_CONSTANT); + $container->setParameter('app.another_constant', BlogPost::MAX_ITEMS); + // ... +.. caution:: + + When using XML configuration, the values between ```` tags are + not trimmed. This means that the value of the following parameter will be + ``'\n something@example.com\n'``: + + .. code-block:: xml + + + something@example.com + + Once defined, you can reference this parameter value from any other configuration file using a special syntax: wrap the parameter name in two ``%`` (e.g. ``%app.admin_email%``): @@ -231,6 +285,33 @@ configuration file using a special syntax: wrap the parameter name in two ``%`` // ... ]); +.. note:: + + If some parameter value includes the ``%`` character, you need to escape it + by adding another ``%`` so Symfony doesn't consider it a reference to a + parameter name: + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + # Parsed as 'https://symfony.com/?foo=%s&bar=%d' + url_pattern: 'https://symfony.com/?foo=%%s&bar=%%d' + + .. code-block:: xml + + + + http://symfony.com/?foo=%%s&bar=%%d + + + .. code-block:: php + + // config/services.php + $container->setParameter('url_pattern', 'http://symfony.com/?foo=%%s&bar=%%d'); + .. include:: /components/dependency_injection/_imports-parameters-note.rst.inc Configuration parameters are very common in Symfony applications. Some packages @@ -239,8 +320,8 @@ a new ``locale`` parameter is added to the ``config/services.yaml`` file). .. seealso:: - Read the :ref:`service parameters ` article to - learn about how to use these configuration parameters in services and controllers. + Later in this article you can read how to + ref:`get configuration parameters in controllers and services `. .. index:: single: Environments; Introduction @@ -407,33 +488,12 @@ This example shows how to configure the database connection using an env var: The next step is to define the value of those env vars in your shell, your web server, etc. This is explained in the following sections, but to protect your application from undefined env vars, you can give them a default value using the -``parameters`` key: - -.. configuration-block:: +``.env`` file: - .. code-block:: yaml - - # config/services.yaml - parameters: - env(DATABASE_URL): 'sqlite:///%kernel.project_dir%/var/data.db' - - .. code-block:: xml - - - - - - - sqlite:///%kernel.project_dir%/var/data.db - - - - .. code-block:: php +.. code-block:: bash - // config/services.php - $container->setParameter('env(DATABASE_URL)', 'sqlite:///%kernel.project_dir%/var/data.db'); + # .env + DATABASE_URL=sqlite:///%kernel.project_dir%/var/data.db .. seealso:: @@ -445,6 +505,32 @@ In order to define the actual values of env vars, Symfony proposes different solutions depending if the application is running in production or in your local development machine. +Independent from the way you set environment variables, you may need to run the +``debug:container`` command with the ``--env-vars`` option to verify that they +are defined and have the expected values: + +.. code-block:: terminal + + $ php bin/console debug:container --env-vars + + ---------------- ----------------- --------------------------------------------- + Name Default value Real value + ---------------- ----------------- --------------------------------------------- + APP_SECRET n/a "471a62e2d601a8952deb186e44186cb3" + FOO "[1, "2.5", 3]" n/a + BAR null n/a + ---------------- ----------------- --------------------------------------------- + + # you can also filter the list of env vars by name: + $ php bin/console debug:container --env-vars foo + + # run this command to show all the details for a specific env var: + $ php bin/console debug:container --env-var=FOO + +.. versionadded:: 4.3 + + The option to debug environment variables was introduced in Symfony 4.3. + .. _configuration-env-var-in-dev: .. _config-dot-env: @@ -469,39 +555,7 @@ This is for example the content of the ``.env`` file to define the value of the In addition to your own env vars, this ``.env`` file also contains the env vars defined by the third-party packages installed in your application (they are -added automatically by :doc:`Symfony Flex ` when installing packages). - -Managing Multiple .env Files -............................ - -The ``.env`` file defines the default values for all env vars. However, it's -common to override some of those values depending on the environment (e.g. to -use a different database for tests) or depending on the machine (e.g. to use a -different OAuth token in your local machine while developing). - -That's why you can define multiple ``.env`` files which override the default env -vars. These are the files in priority order (an env var found in one file -overrides the same env var for all the files below): - -* ``.env..local`` (e.g. ``.env.test.local``): defines/overrides - env vars only for some environment and only in your local machine; -* ``.env.`` (e.g. ``.env.test``): defines/overrides env vars for - some environment and for all machines; -* ``.env.local``: defines/overrides env vars for all environments but only in - your local machine; -* ``.env``: defines the default value of env vars. - -The ``.env`` and ``.env.`` files should be committed to the shared -repository because they are the same for all developers. However, the -``.env.local`` and ``.env..local`` and files **should not be -committed** because only you will use them. In fact, the ``.gitignore`` file -that comes with Symfony prevents them from being committed. - -.. caution:: - - Applications created before November 2018 had a slightly different system, - involving a ``.env.dist`` file. For information about upgrading, see: - :doc:`configuration/dot-env-changes`. +added automatically by :ref:`Symfony Flex ` when installing packages). .. _configuration-env-var-in-prod: @@ -511,7 +565,7 @@ Configuring Environment Variables in Production In production, the ``.env`` files are also parsed and loaded on each request so you can override the env vars already defined in the server. In order to improve performance, you can run the ``dump-env`` command (available when using -:doc:`Symfony Flex ` 1.2 or later). +:ref:`Symfony Flex ` 1.2 or later). This command parses all the ``.env`` files once and compiles their contents into a new PHP-optimized file called ``.env.local.php``. From that moment, Symfony @@ -526,6 +580,17 @@ will load the parsed file instead of parsing the ``.env`` files again: Update your deployment tools/workflow to run the ``dump-env`` command after each deploy to improve the application performance. +.. _configuration-env-var-web-server: + +Creating ``.env`` files is the easiest way of using env vars in Symfony +applications. However, you can also configure real env vars in your servers and +operating systems. + +.. tip:: + + SymfonyCloud, the cloud service optimized for Symfony applications, defines + some `utilities to manage env vars`_ in production. + .. caution:: Beware that dumping the contents of the ``$_SERVER`` and ``$_ENV`` variables @@ -537,31 +602,220 @@ will load the parsed file instead of parsing the ``.env`` files again: :doc:`Symfony profiler `. In practice this shouldn't be a problem because the web profiler must **never** be enabled in production. -Defining Environment Variables in the Web Server -................................................ +.. _configuration-multiple-env-files: -Using the ``.env`` files explained earlier is the recommended way of using env -vars in Symfony applications. However, you can also define env vars in the web -server configuration if you prefer that: +Managing Multiple .env Files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``.env`` file defines the default values for all env vars. However, it's +common to override some of those values depending on the environment (e.g. to +use a different database for tests) or depending on the machine (e.g. to use a +different OAuth token on your local machine while developing). + +That's why you can define multiple ``.env`` files to override env vars. The +following list shows the files loaded in all environments. The ``.env`` file is +the only mandatory file and each file content overrides the previous one: + +* ``.env``: defines the default values of the env vars needed by the application; +* ``.env.local``: defines machine-specific overrides for env vars on all + environments. This file is not committed to the repository, so these overrides + only apply to the machine which contains the file (your local computer, + production server, etc.); +* ``.env.`` (e.g. ``.env.test``): overrides env vars only for some + environment but for all machines; +* ``.env..local`` (e.g. ``.env.test.local``): defines machine-specific + env vars overrides only for some environment. It's similar to ``.env.local``, + but the overrides only apply to some particular environment. + +.. note:: + + The real environment variables defined in the server always win over the + env vars created by the ``.env`` files. + +The ``.env`` and ``.env.`` files should be committed to the shared +repository because they are the same for all developers and machines. However, +the env files ending in ``.local`` (``.env.local`` and ``.env..local``) +**should not be committed** because only you will use them. In fact, the +``.gitignore`` file that comes with Symfony prevents them from being committed. + +.. caution:: + + Applications created before November 2018 had a slightly different system, + involving a ``.env.dist`` file. For information about upgrading, see: + :doc:`configuration/dot-env-changes`. + +.. _configuration-accessing-parameters: + +Accessing Configuration Parameters +---------------------------------- + +Controllers and services can access all the configuration parameters. This +includes both the :ref:`parameters defined by yourself ` +and the parameters created by packages/bundles. Run the following command to see +all the parameters that exist in your application: + +.. code-block:: terminal + + $ php bin/console debug:container --parameters + +In controllers extending from the :ref:`AbstractController `, +use the ``getParameter()`` helper:: + + // src/Controller/UserController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + + class UserController extends AbstractController + { + // ... + + public function index() + { + $projectDir = $this->getParameter('kernel.project_dir'); + $adminEmail = $this->getParameter('app.admin_email'); + + // ... + } + } + +In services and controllers not extending from ``AbstractController``, inject +the parameters as arguments of their constructors. You must inject them +explicitly because :doc:`service autowiring ` +doesn't work for parameters: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + app.contents_dir: '...' + + services: + App\Service\MessageGenerator: + arguments: + $contentsDir: '%app.contents_dir%' + + .. code-block:: xml + + + + + + + ... + + + + + %app.contents_dir% + + + + + .. code-block:: php + + // config/services.php + use App\Service\MessageGenerator; + use Symfony\Component\DependencyInjection\Reference; + + $container->setParameter('app.contents_dir', '...'); + + $container->getDefinition(MessageGenerator::class) + ->setArgument('$contentsDir', '%app.contents_dir%'); + +If you inject the same parameters over and over again, use instead the +``services._defaults.bind`` option. The arguments defined in that option are +injected automatically whenever a service constructor or controller action +define an argument with that exact name. For example, to inject the value of the +:ref:`kernel.project_dir parameter ` +whenever a service/controller defines a ``$projectDir`` argument, use this: .. configuration-block:: - .. code-block:: apache + .. code-block:: yaml + + # config/services.yaml + services: + _defaults: + bind: + # pass this value to any $projectDir argument for any service + # that's created in this file (including controller arguments) + $projectDir: '%kernel.project_dir%' - # ... - SetEnv DATABASE_URL "mysql://db_user:db_password@127.0.0.1:3306/db_name" - + .. code-block:: xml + + + + + + + + + %kernel.project_dir% + - .. code-block:: nginx + + + - fastcgi_param DATABASE_URL "mysql://db_user:db_password@127.0.0.1:3306/db_name"; + .. code-block:: php -.. tip:: + // config/services.php + use App\Controller\LuckyController; + use Psr\Log\LoggerInterface; + use Symfony\Component\DependencyInjection\Reference; + + $container->register(LuckyController::class) + ->setPublic(true) + ->setBindings([ + // pass this value to any $projectDir argument for any service + // that's created in this file (including controller arguments) + '$projectDir' => '%kernel.project_dir%', + ]) + ; - SymfonyCloud, the cloud service optimized for Symfony applications, defines - some `utilities to manage env vars`_ in production. +.. seealso:: + + Read the article about :ref:`binding arguments by name and/or type ` + to learn more about this powerful feature. + +Finally, if some service needs to access to lots of parameters, instead of +injecting each of them individually, you can inject all the application +parameters at once by type-hinting any of its constructor arguments with the +:class:`Symfony\\Component\\DependencyInjection\\ParameterBag\\ContainerBagInterface`:: + + // src/Service/MessageGenerator.php + // ... + + use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface; + + class MessageGenerator + { + private $params; + + public function __construct(ContainerBagInterface $params) + { + $this->params = $params; + } + + public function someMethod() + { + // get any container parameter from $this->params, which stores all of them + $sender = $this->params->get('mailer_sender'); + // ... + } + } Keep Going! ----------- @@ -576,10 +830,7 @@ part of Symfony individually by following the guides. Check out: * :doc:`/email` * :doc:`/logging` -And the many other topics. - -Learn more ----------- +And all the other topics related to configuration: .. toctree:: :maxdepth: 1 @@ -587,6 +838,7 @@ Learn more configuration/* +.. _`Learn the XML syntax`: https://en.wikipedia.org/wiki/XML .. _`environment variables`: https://en.wikipedia.org/wiki/Environment_variable .. _`symbolic links`: https://en.wikipedia.org/wiki/Symbolic_link .. _`utilities to manage env vars`: https://symfony.com/doc/master/cloud/cookbooks/env.html diff --git a/configuration/dot-env-changes.rst b/configuration/dot-env-changes.rst index 596f206853e..280a40c8d63 100644 --- a/configuration/dot-env-changes.rst +++ b/configuration/dot-env-changes.rst @@ -18,7 +18,7 @@ important changes: * A) The ``.env.dist`` file no longer exists. Its contents should be moved to your ``.env`` file (see the next point). -* B) The ``.env`` file **is** now commited to your repository. It was previously ignored +* B) The ``.env`` file **is** now committed to your repository. It was previously ignored via the ``.gitignore`` file (the updated recipe does not ignore this file). Because this file is committed, it should contain non-sensitive, default values. Basically, the ``.env.dist`` file was moved to ``.env``. @@ -79,8 +79,8 @@ changes can be made to any Symfony 3.4 or higher app: $ git mv .env.dist .env # Windows - $ mv .env .env.local - $ git mv .env.dist .env + C:\> move .env .env.local + C:\> git mv .env.dist .env You can also update the `comment on the top of .env`_ to reflect the new changes. diff --git a/configuration/env_var_processors.rst b/configuration/env_var_processors.rst index fb54a11ecce..67d9bca422e 100644 --- a/configuration/env_var_processors.rst +++ b/configuration/env_var_processors.rst @@ -1,12 +1,13 @@ .. index:: single: Environment Variable Processors; env vars +.. _env-var-processors: + Environment Variable Processors =============================== :ref:`Using env vars to configure Symfony applications ` is a -common practice to hide sensitive configuration (e.g. database credentials) and -to make your applications truly dynamic. +common practice to make your applications truly dynamic. The main issue of env vars is that their values can only be strings and your application may need other data types (integer, boolean, etc.). Symfony solves @@ -324,6 +325,97 @@ Symfony provides the following env var processors: 'auth' => '%env(file:AUTH_FILE)%', ]); +``env(require:FOO)`` + ``require()`` the PHP file whose path is the value of the ``FOO`` + env var and return the value returned from it. + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + parameters: + env(PHP_FILE): '../config/.runtime-evaluated.php' + app: + auth: '%env(require:PHP_FILE)%' + + .. code-block:: xml + + + + + + + ../config/.runtime-evaluated.php + + + + + + .. code-block:: php + + // config/packages/framework.php + $container->setParameter('env(PHP_FILE)', '../config/.runtime-evaluated.php'); + $container->loadFromExtension('app', [ + 'auth' => '%env(require:AUTH_FILE)%', + ]); + + .. versionadded:: 4.3 + + The ``require`` processor was introduced in Symfony 4.3. + +``env(trim:FOO)`` + Trims the content of ``FOO`` env var, removing whitespaces from the beginning + and end of the string. This is especially useful in combination with the + ``file`` processor, as it'll remove newlines at the end of a file. + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + parameters: + env(AUTH_FILE): '../config/auth.json' + google: + auth: '%env(trim:file:AUTH_FILE)%' + + .. code-block:: xml + + + + + + + ../config/auth.json + + + + + + .. code-block:: php + + // config/packages/framework.php + $container->setParameter('env(AUTH_FILE)', '../config/auth.json'); + $container->loadFromExtension('google', [ + 'auth' => '%env(trim:file:AUTH_FILE)%', + ]); + + .. versionadded:: 4.3 + + The ``trim`` processor was introduced in Symfony 4.3. + ``env(key:FOO:BAR)`` Retrieves the value associated with the key ``FOO`` from the array whose contents are stored in the ``BAR`` env var: @@ -362,6 +454,176 @@ Symfony provides the following env var processors: $container->setParameter('env(SECRETS_FILE)', '/opt/application/.secrets.json'); $container->setParameter('database_password', '%env(key:database_password:json:file:SECRETS_FILE)%'); +``env(default:fallback_param:BAR)`` + Retrieves the value of the parameter ``fallback_param`` when the ``BAR`` env + var is not available: + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + # if PRIVATE_KEY is not a valid file path, the content of raw_key is returned + private_key: '%env(default:raw_key:file:PRIVATE_KEY)%' + raw_key: '%env(PRIVATE_KEY)%' + + .. code-block:: xml + + + + + + + %env(default:raw_key:file:PRIVATE_KEY)% + %env(PRIVATE_KEY)% + + + + .. code-block:: php + + // config/services.php + + // if PRIVATE_KEY is not a valid file path, the content of raw_key is returned + $container->setParameter('private_key', '%env(default:raw_key:file:PRIVATE_KEY)%'); + $container->setParameter('raw_key', '%env(PRIVATE_KEY)%'); + + When the fallback parameter is omitted (e.g. ``env(default::API_KEY)``), the + value returned is ``null``. + + .. versionadded:: 4.3 + + The ``default`` processor was introduced in Symfony 4.3. + +``env(url:FOO)`` + Parses an absolute URL and returns its components as an associative array. + + .. code-block:: bash + + # .env + MONGODB_URL="mongodb://db_user:db_password@127.0.0.1:27017/db_name" + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/mongodb.yaml + mongo_db_bundle: + clients: + default: + hosts: + - { host: '%env(key:host:url:MONGODB_URL)%', port: '%env(key:port:url:MONGODB_URL)%' } + username: '%env(key:user:url:MONGODB_URL)%' + password: '%env(key:pass:url:MONGODB_URL)%' + connections: + default: + database_name: '%env(key:path:url:MONGODB_URL)%' + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/mongodb.php + $container->loadFromExtension('mongodb', [ + 'clients' => [ + 'default' => [ + 'hosts' => [ + [ + 'host' => '%env(key:host:url:MONGODB_URL)%', + 'port' => '%env(key:port:url:MONGODB_URL)%', + ], + ], + 'username' => '%env(key:user:url:MONGODB_URL)%', + 'password' => '%env(key:pass:url:MONGODB_URL)%', + ], + ], + 'connections' => [ + 'default' => [ + 'database_name' => '%env(key:path:url:MONGODB_URL)%', + ], + ], + ]); + + .. caution:: + + In order to ease extraction of the resource from the URL, the leading + ``/`` is trimmed from the ``path`` component. + + .. versionadded:: 4.3 + + The ``url`` processor was introduced in Symfony 4.3. + +``env(query_string:FOO)`` + Parses the query string part of the given URL and returns its components as + an associative array. + + .. code-block:: bash + + # .env + MONGODB_URL="mongodb://db_user:db_password@127.0.0.1:27017/db_name?timeout=3000" + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/mongodb.yaml + mongo_db_bundle: + clients: + default: + # ... + connectTimeoutMS: '%env(int:key:timeout:query_string:MONGODB_URL)%' + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/mongodb.php + $container->loadFromExtension('mongodb', [ + 'clients' => [ + 'default' => [ + // ... + 'connectTimeoutMS' => '%env(int:key:timeout:query_string:MONGODB_URL)%', + ], + ], + ]); + + .. versionadded:: 4.3 + + The ``query_string`` processor was introduced in Symfony 4.3. + It is also possible to combine any number of processors: .. code-block:: yaml diff --git a/configuration/front_controllers_and_kernel.rst b/configuration/front_controllers_and_kernel.rst index d986d7471b7..bdeac5ed93d 100644 --- a/configuration/front_controllers_and_kernel.rst +++ b/configuration/front_controllers_and_kernel.rst @@ -123,6 +123,8 @@ new kernel. .. index:: single: Configuration; Debug mode +.. _debug-mode: + Debug Mode ~~~~~~~~~~ diff --git a/console.rst b/console.rst index 7e8e65c0f1c..fcad944ca21 100644 --- a/console.rst +++ b/console.rst @@ -322,7 +322,6 @@ console:: // tests/Command/CreateUserCommandTest.php namespace App\Tests\Command; - use App\Command\CreateUserCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Console\Tester\CommandTester; diff --git a/console/coloring.rst b/console/coloring.rst index c9faff798e4..774a2ab96fa 100644 --- a/console/coloring.rst +++ b/console/coloring.rst @@ -74,8 +74,26 @@ You can also set these colors and options directly inside the tag name:: or use the :method:`Symfony\\Component\\Console\\Formatter\\OutputFormatter::escape` method to escape all the tags included in the given string. +Displaying Clickable Links +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 4.3 + + The feature to display clickable links was introduced in Symfony 4.3. + +Commands can use the special ```` tag to display links similar to the +```` elements of web pages:: + + $output->writeln('Symfony Homepage'); + +If your terminal belongs to the `list of terminal emulators that support links`_ +you can click on the *"Symfony Homepage"* text to open its URL in your default +browser. Otherwise, you'll see *"Symfony Homepage"* as regular text and the URL +will be lost. + .. _Cmder: https://cmder.net/ .. _ConEmu: https://conemu.github.io/ .. _ANSICON: https://github.com/adoxa/ansicon/releases .. _Mintty: https://mintty.github.io/ .. _Hyper: https://hyper.is/ +.. _`list of terminal emulators that support links`: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda diff --git a/console/command_in_controller.rst b/console/command_in_controller.rst index d738484c6b8..6559ae15c0a 100644 --- a/console/command_in_controller.rst +++ b/console/command_in_controller.rst @@ -21,7 +21,7 @@ their code. Instead, you can execute the command directly. overhead. Imagine you want to send spooled Swift Mailer messages by -:doc:`using the swiftmailer:spool:send command `. +:doc:`using the swiftmailer:spool:send command `. Run this command from inside your controller via:: // src/Controller/SpoolController.php diff --git a/console/request_context.rst b/console/request_context.rst deleted file mode 100644 index f2fbb505cf2..00000000000 --- a/console/request_context.rst +++ /dev/null @@ -1,99 +0,0 @@ -.. index:: - single: Console; Generating URLs - -How to Generate URLs from the Console -===================================== - -Unfortunately, the command line context does not know about your VirtualHost -or domain name. This means that if you generate absolute URLs within a -console command you'll probably end up with something like ``http://localhost/foo/bar`` -which is not very useful. - -To fix this, you need to configure the "request context", which is a fancy -way of saying that you need to configure your environment so that it knows -what URL it should use when generating URLs. - -There are two ways of configuring the request context: at the application level -and per Command. - -Configuring the Request Context Globally ----------------------------------------- - -To configure the Request Context - which is used by the URL Generator - you can -redefine the parameters it uses as default values to change the default host -(``localhost``) and scheme (``http``). You can also configure the base path (both for -the URL generator and the assets) if Symfony is not running in the root directory. - -Note that this does not impact URLs generated via normal web requests, since those -will override the defaults. - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - parameters: - router.request_context.host: 'example.org' - router.request_context.scheme: 'https' - router.request_context.base_url: 'my/path' - asset.request_context.base_path: '%router.request_context.base_url%' - asset.request_context.secure: true - - .. code-block:: xml - - - - - - - example.org - https - my/path - %router.request_context.base_url% - true - - - - - .. code-block:: php - - // config/services.php - $container->setParameter('router.request_context.host', 'example.org'); - $container->setParameter('router.request_context.scheme', 'https'); - $container->setParameter('router.request_context.base_url', 'my/path'); - $container->setParameter('asset.request_context.base_path', $container->getParameter('router.request_context.base_url')); - $container->setParameter('asset.request_context.secure', true); - -Configuring the Request Context per Command -------------------------------------------- - -To change it only in one command you need to fetch the Request Context -from the ``router`` service and override its settings:: - - // src/Command/DemoCommand.php - use Symfony\Component\Routing\RouterInterface; - // ... - - class DemoCommand extends Command - { - private $router; - - public function __construct(RouterInterface $router) - { - parent::__construct(); - - $this->router = $router; - } - - protected function execute(InputInterface $input, OutputInterface $output) - { - $context = $this->router->getContext(); - $context->setHost('example.com'); - $context->setScheme('https'); - $context->setBaseUrl('my/path'); - - $url = $this->router->generate('route-name', ['param-name' => 'param-value']); - // ... - } - } diff --git a/contributing/code/bc.rst b/contributing/code/bc.rst index 30cffd8eae8..a631073afec 100644 --- a/contributing/code/bc.rst +++ b/contributing/code/bc.rst @@ -445,9 +445,4 @@ Turn static into non static No .. [9] Allowed for the ``void`` return type. -.. _Semantic Versioning: https://semver.org/ -.. _scalar type: https://php.net/manual/en/function.is-scalar.php -.. _boolean values: https://php.net/manual/en/function.boolval.php -.. _string values: https://php.net/manual/en/function.strval.php -.. _integer values: https://php.net/manual/en/function.intval.php -.. _float values: https://php.net/manual/en/function.floatval.php +.. _`Semantic Versioning`: https://semver.org/ diff --git a/contributing/code/bugs.rst b/contributing/code/bugs.rst index 197f36312b4..3b024945271 100644 --- a/contributing/code/bugs.rst +++ b/contributing/code/bugs.rst @@ -48,5 +48,4 @@ If your problem definitely looks like a bug, report it using the official bug .. _IRC channel: https://symfony.com/irc .. _the Symfony Slack: https://symfony.com/slack-invite .. _tracker: https://github.com/symfony/symfony/issues -.. _Symfony Standard Edition: https://github.com/symfony/symfony-standard/ .. _
HTML tag: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details diff --git a/contributing/code/conventions.rst b/contributing/code/conventions.rst index ba5ddb31b1c..d3622b57ade 100644 --- a/contributing/code/conventions.rst +++ b/contributing/code/conventions.rst @@ -79,30 +79,46 @@ must be used instead (where ``XXX`` is the name of the related thing): .. _contributing-code-conventions-deprecations: -Deprecations ------------- +Deprecating Code +---------------- From time to time, some classes and/or methods are deprecated in the framework; that happens when a feature implementation cannot be changed because of backward compatibility issues, but we still want to propose a "better" alternative. In that case, the old implementation can be **deprecated**. +Deprecations must only be introduced on the next minor version of the impacted +component (or bundle, or bridge, or contract). +They can exceptionally be introduced on previous supported versions if they are critical. + +A new class (or interface, or trait) cannot be introduced as deprecated, or +contain deprecated methods. + +A new method cannot be introduced as deprecated. + A feature is marked as deprecated by adding a ``@deprecated`` phpdoc to relevant classes, methods, properties, ...:: /** - * @deprecated since vendor-name/package-name 2.8, to be removed in 3.0. Use XXX instead. + * @deprecated since Symfony 2.8. + */ + +The deprecation message must indicate the version in which the feature was deprecated, +and whenever possible, how it was replaced:: + + /** + * @deprecated since Symfony 2.8, use Replacement instead. */ -The deprecation message should indicate the version when the class/method was -deprecated, the version when it will be removed, and whenever possible, how -the feature was replaced. +When the replacement is in another namespace than the deprecated class, its FQCN must be used:: + + /** + * @deprecated since Symfony 2.8, use A\B\Replacement instead. + */ -A PHP ``E_USER_DEPRECATED`` error must also be triggered to help people with -the migration starting one or two minor versions before the version where the -feature will be removed (depending on the criticality of the removal):: +A PHP ``E_USER_DEPRECATED`` error must also be triggered to help people with the migration:: - @trigger_error('XXX() is deprecated since vendor-name/package-name 2.8 and will be removed in 3.0. Use XXX instead.', E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s" class is deprecated since Symfony 2.8, use "%s" instead.', Deprecated::class, Replacement::class), E_USER_DEPRECATED); Without the `@-silencing operator`_, users would need to opt-out from deprecation notices. Silencing swaps this behavior and allows users to opt-in when they are @@ -113,19 +129,58 @@ the Web Debug Toolbar or by the PHPUnit bridge). When deprecating a whole class the ``trigger_error()`` call should be placed between the namespace and the use declarations, like in this example from -`ArrayParserCache`_:: +`ServiceRouterLoader`_:: - namespace Symfony\Component\ExpressionLanguage\ParserCache; + namespace Symfony\Component\Routing\Loader\DependencyInjection; - @trigger_error('The '.__NAMESPACE__.'\ArrayParserCache class is deprecated since version 3.2 and will be removed in 4.0. Use the Symfony\Component\Cache\Adapter\ArrayAdapter class instead.', E_USER_DEPRECATED); + use Symfony\Component\Routing\Loader\ContainerLoader; - use Symfony\Component\ExpressionLanguage\ParsedExpression; + @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ServiceRouterLoader::class, ContainerLoader::class), E_USER_DEPRECATED); /** - * @author Adrien Brault - * - * @deprecated since Symfony 3.2, to be removed in 4.0. Use the Symfony\Component\Cache\Adapter\ArrayAdapter class instead. + * @deprecated since Symfony 4.4, use Symfony\Component\Routing\Loader\ContainerLoader instead. */ - class ArrayParserCache implements ParserCacheInterface + class ServiceRouterLoader extends ObjectRouteLoader + +.. _`ServiceRouterLoader`: https://github.com/symfony/symfony/blob/4.4/src/Symfony/Component/Routing/Loader/DependencyInjection/ServiceRouterLoader.php + +The deprecation must be added to the ``CHANGELOG.md`` file of the impacted component:: + + 4.4.0 + ----- + + * Deprecated the `Deprecated` class, use `Replacement` instead. + +It must also be added to the ``UPGRADE.md`` file of the targeted minor version +(``UPGRADE-4.4.md`` in our example):: + + DependencyInjection + ------------------- + + * Deprecated the `Deprecated` class, use `Replacement` instead. + +Finally, its consequences must be added to the ``UPGRADE.md`` file of the next major version +(``UPGRADE-5.0.md`` in our example):: + + DependencyInjection + ------------------- + + * Removed the `Deprecated` class, use `Replacement` instead. + +All these tasks are mandatory and must be done in the same pull request. + +Removing Deprecated Code +------------------------ + +Removing deprecated code can only be done once every 2 years, on the next major version of the +impacted component (``master`` branch). + +When removing deprecated code, the consequences of the deprecation must be added to the ``CHANGELOG.md`` file +of the impacted component:: + + 5.0.0 + ----- + + * Removed the `Deprecated` class, use `Replacement` instead. -.. _`ArrayParserCache`: https://github.com/symfony/symfony/blob/3.2/src/Symfony/Component/ExpressionLanguage/ParserCache/ArrayParserCache.php +This task is mandatory and must be done in the same pull request. diff --git a/contributing/code/core_team.rst b/contributing/code/core_team.rst index 8c5a63321e4..5a9596fc80f 100644 --- a/contributing/code/core_team.rst +++ b/contributing/code/core_team.rst @@ -29,12 +29,7 @@ The Symfony Core groups, in descending order of priority, are as follows: 2. **Mergers Team** -* Merge pull requests for the component or components on which they have been - granted privileges. - -3. **Deciders Team** - -* Decide to merge or reject a pull request. +* Merge pull requests on the main Symfony repository. In addition, there are other groups created to manage specific topics: @@ -43,6 +38,10 @@ In addition, there are other groups created to manage specific topics: * Manage the whole security process (triaging reported vulnerabilities, fixing the reported issues, coordinating the release of security fixes, etc.) +**Recipes Team** + +* Manage the recipes in the main and contrib recipe repositories. + **Documentation Team** * Manage the whole `symfony-docs repository`_. @@ -56,55 +55,30 @@ Active Core Members * **Mergers Team** (``@symfony/mergers`` on GitHub): - * **Tobias Schultze** (`Tobion`_) can merge into the Routing_, - OptionsResolver_ and PropertyAccess_ components; - - * **Nicolas Grekas** (`nicolas-grekas`_) can merge into all components, - bridges and bundles; - - * **Christophe Coevoet** (`stof`_) can merge into all components, bridges and - bundles; - - * **Kévin Dunglas** (`dunglas`_) can merge into the PropertyInfo_, the Serializer_ - and the WebLink_ components; - - * **Jakub Zalas** (`jakzal`_) can merge into the DomCrawler_ and Intl_ - components; - - * **Christian Flothmann** (`xabbuh`_) can merge into the Yaml_, and Form_ - components; - - * **Javier Eguiluz** (`javiereguiluz`_) can merge into the WebProfilerBundle_; - - * **Grégoire Pineau** (`lyrixx`_) can merge into the Workflow_ component; - - * **Ryan Weaver** (`weaverryan`_) can merge into the Security_ component and - the SecurityBundle_; - - * **Robin Chalas** (`chalasr`_) can merge into the Console_ and Security_ - components and the SecurityBundle_; - - * **Maxime Steinhausser** (`ogizanagi`_) can merge into Config_, Console_, - Form_, Serializer_, DependencyInjection_, and HttpKernel_ components; - - * **Tobias Nyholm** (`Nyholm`_) manages the official and contrib recipes - repositories; - - * **Samuel Rozé** (`sroze`_) can merge into the Messenger_ component. - - * **Yonel Ceruto** (`yceruto`_) can merge into the ErrorRenderer, - OptionsResolver_, and Form_ components and TwigBundle_. - -* **Deciders Team** (``@symfony/deciders`` on GitHub): - - * **Jordi Boggiano** (`seldaek`_); - * **Lukas Kahwe Smith** (`lsmith77`_). + * **Nicolas Grekas** (`nicolas-grekas`_); + * **Christophe Coevoet** (`stof`_); + * **Christian Flothmann** (`xabbuh`_); + * **Tobias Schultze** (`Tobion`_); + * **Kévin Dunglas** (`dunglas`_); + * **Jakub Zalas** (`jakzal`_); + * **Javier Eguiluz** (`javiereguiluz`_); + * **Grégoire Pineau** (`lyrixx`_); + * **Ryan Weaver** (`weaverryan`_); + * **Robin Chalas** (`chalasr`_); + * **Maxime Steinhausser** (`ogizanagi`_); + * **Samuel Rozé** (`sroze`_); + * **Yonel Ceruto** (`yceruto`_). * **Security Team** (``@symfony/security`` on GitHub): * **Fabien Potencier** (`fabpot`_); * **Michael Cullum** (`michaelcullum`_). +* **Recipes Team**: + + * **Fabien Potencier** (`fabpot`_); + * **Tobias Nyholm** (`Nyholm`_). + * **Documentation Team** (``@symfony/team-symfony-docs`` on GitHub): * **Fabien Potencier** (`fabpot`_); @@ -123,7 +97,8 @@ Symfony contributions: * **Bernhard Schussek** (`webmozart`_); * **Abdellatif AitBoudad** (`aitboudad`_); -* **Romain Neutron**. +* **Romain Neutron**; +* **Jordi Boggiano** (`Seldaek`_). Core Membership Application ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -170,11 +145,11 @@ A pull request **can be merged** if: * It is a minor change [1]_; -* Enough time was given for peer reviews (at least 2 days for "regular" - pull requests, and 4 days for pull requests with "a significant impact"); +* Enough time was given for peer reviews; -* At least the component's **Merger** or two other Core members voted ``+1`` - and no Core member voted ``-1``. +* At least two **Merger Team** members voted ``+1`` (only one if the submitter + is part of the Merger team) and no Core member voted ``-1`` (via Github + reviews or as comments). Pull Request Merging Process ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -199,56 +174,15 @@ discretion of the **Project Leader**. .. [1] Minor changes comprise typos, DocBlock fixes, code standards violations, and minor CSS, JavaScript and HTML modifications. -.. _PhpUnitBridge: https://github.com/symfony/phpunit-bridge -.. _BrowserKit: https://github.com/symfony/browser-kit -.. _Cache: https://github.com/symfony/cache -.. _Config: https://github.com/symfony/config -.. _Console: https://github.com/symfony/console -.. _Debug: https://github.com/symfony/debug -.. _DebugBundle: https://github.com/symfony/debug-bundle -.. _DependencyInjection: https://github.com/symfony/dependency-injection -.. _DoctrineBridge: https://github.com/symfony/doctrine-bridge -.. _EventDispatcher: https://github.com/symfony/event-dispatcher -.. _DomCrawler: https://github.com/symfony/dom-crawler -.. _ErrorRenderer: https://github.com/symfony/error-renderer -.. _Form: https://github.com/symfony/form -.. _HttpFoundation: https://github.com/symfony/http-foundation -.. _HttpKernel: https://github.com/symfony/http-kernel -.. _Icu: https://github.com/symfony/icu -.. _Intl: https://github.com/symfony/intl -.. _LDAP: https://github.com/symfony/ldap -.. _Locale: https://github.com/symfony/locale -.. _Messenger: https://github.com/symfony/messenger -.. _MonologBridge: https://github.com/symfony/monolog-bridge -.. _OptionsResolver: https://github.com/symfony/options-resolver -.. _Process: https://github.com/symfony/process -.. _PropertyAccess: https://github.com/symfony/property-access -.. _PropertyInfo: https://github.com/symfony/property-info -.. _Routing: https://github.com/symfony/routing -.. _Serializer: https://github.com/symfony/serializer -.. _Translation: https://github.com/symfony/translation -.. _Security: https://github.com/symfony/security -.. _SecurityBundle: https://github.com/symfony/security-bundle -.. _Stopwatch: https://github.com/symfony/stopwatch -.. _TwigBridge: https://github.com/symfony/twig-bridge -.. _TwigBundle: https://github.com/symfony/twig-bundle -.. _Validator: https://github.com/symfony/validator -.. _VarDumper: https://github.com/symfony/var-dumper -.. _Workflow: https://github.com/symfony/workflow -.. _Yaml: https://github.com/symfony/yaml -.. _WebProfilerBundle: https://github.com/symfony/web-profiler-bundle -.. _WebLink: https://github.com/symfony/web-link .. _`symfony-docs repository`: https://github.com/symfony/symfony-docs .. _`fabpot`: https://github.com/fabpot/ .. _`webmozart`: https://github.com/webmozart/ .. _`Tobion`: https://github.com/Tobion/ -.. _`romainneutron`: https://github.com/romainneutron/ .. _`nicolas-grekas`: https://github.com/nicolas-grekas/ .. _`stof`: https://github.com/stof/ .. _`dunglas`: https://github.com/dunglas/ .. _`jakzal`: https://github.com/jakzal/ .. _`Seldaek`: https://github.com/Seldaek/ -.. _`lsmith77`: https://github.com/lsmith77/ .. _`weaverryan`: https://github.com/weaverryan/ .. _`aitboudad`: https://github.com/aitboudad/ .. _`xabbuh`: https://github.com/xabbuh/ diff --git a/contributing/code/pull_requests.rst b/contributing/code/pull_requests.rst index bf1d07a4cc9..6957c5d043d 100644 --- a/contributing/code/pull_requests.rst +++ b/contributing/code/pull_requests.rst @@ -225,7 +225,7 @@ in mind the following: as defined in `PSR-1`_ and `PSR-2`_. A status is posted below the pull request description with a summary - of any problems it detects or any Travis CI build failures. + of any problems it detects or any `Travis-CI`_ build failures. .. tip:: @@ -415,12 +415,8 @@ before merging. .. _ProGit: https://git-scm.com/book .. _GitHub: https://github.com/join -.. _`GitHub's Documentation`: https://help.github.com/articles/ignoring-files +.. _`GitHub's documentation`: https://help.github.com/articles/ignoring-files .. _Symfony repository: https://github.com/symfony/symfony -.. _dev mailing-list: https://groups.google.com/group/symfony-devs -.. _travis-ci.org: https://travis-ci.org/ -.. _`travis-ci.org status icon`: https://about.travis-ci.com/docs/user/status-images/ -.. _`travis-ci.org Getting Started Guide`: https://about.travis-ci.com/docs/user/getting-started/ .. _`documentation repository`: https://github.com/symfony/symfony-docs .. _`fabbot`: https://fabbot.io .. _`PSR-1`: https://www.php-fig.org/psr/psr-1/ @@ -429,4 +425,3 @@ before merging. .. _`Symfony Slack`: https://symfony.com/slack-invite .. _`Travis-CI`: https://travis-ci.org/symfony/symfony .. _`draft status`: https://help.github.com/en/articles/about-pull-requests#draft-pull-requests -.. _`Symfony Roadmap`: https://symfony.com/roadmap diff --git a/contributing/code/security.rst b/contributing/code/security.rst index 344661b4958..e9f4dcdabf1 100644 --- a/contributing/code/security.rst +++ b/contributing/code/security.rst @@ -1,9 +1,9 @@ Security Issues =============== -This document explains how Symfony security issues are handled by the Symfony -core team (Symfony being the code hosted on the main ``symfony/symfony`` `Git -repository`_). +This document explains how Symfony security issues are handled by the +Symfony core team (Symfony being the code hosted on the main ``symfony/symfony`` +`Git repository`_). Reporting a Security Issue -------------------------- @@ -38,7 +38,8 @@ confirmed, the core team works on a solution following these steps: #. Publish the post on the official Symfony `blog`_ (it must also be added to the "`Security Advisories`_" category); #. Update the public `security advisories database`_ maintained by the - FriendsOfPHP organization and which is used by the ``security:check`` command. + FriendsOfPHP organization and which is used by + :ref:`the check:security command `. .. note:: @@ -169,16 +170,14 @@ Security Advisories .. tip:: You can check your Symfony application for known security vulnerabilities - using the ``security:check`` command provided by the - :ref:`Symfony security checker `. + using :ref:`the check:security command `. Check the `Security Advisories`_ blog category for a list of all security vulnerabilities that were fixed in Symfony releases, starting from Symfony 1.0.0. -.. _Git repository: https://github.com/symfony/symfony +.. _`Git repository`: https://github.com/symfony/symfony .. _blog: https://symfony.com/blog/ -.. _Security Advisories: https://symfony.com/blog/category/security-advisories .. _`security advisories database`: https://github.com/FriendsOfPHP/security-advisories .. _`mitre.org`: https://cveform.mitre.org/ .. _`Security Advisories`: https://symfony.com/blog/category/security-advisories diff --git a/contributing/community/reviews.rst b/contributing/community/reviews.rst index e730ebeced7..06cc404d821 100644 --- a/contributing/community/reviews.rst +++ b/contributing/community/reviews.rst @@ -45,7 +45,7 @@ next step. Create a GitHub Account ----------------------- -Symfony uses GitHub_ to manage bug reports and pull requests. If you want to +Symfony uses `GitHub`_ to manage bug reports and pull requests. If you want to do reviews, you need to `create a GitHub account`_ and log in. The Bug Report Review Process @@ -214,11 +214,8 @@ Pick a pull request from the `PRs in need of review`_ and follow these steps: .. _`Symfony skeleton`: https://github.com/symfony/skeleton .. _`Symfony website skeleton`: https://github.com/symfony/website-skeleton .. _create a GitHub account: https://help.github.com/articles/signing-up-for-a-new-github-account/ -.. _forking: https://help.github.com/articles/fork-a-repo/ .. _bug reports in need of review: https://github.com/symfony/symfony/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3A%22Bug%22+label%3A%22Status%3A+Needs+Review%22+ .. _PRs in need of review: https://github.com/symfony/symfony/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Apr+label%3A%22Status%3A+Needs+Review%22+ -.. _Contribution Guidelines: https://github.com/symfony/symfony/blob/master/CONTRIBUTING.md -.. _Symfony's Release Schedule: https://symfony.com/doc/current/contributing/community/releases.html#schedule .. _Symfony Roadmap: https://symfony.com/roadmap .. _Carson Bot: https://github.com/carsonbot/carsonbot .. _`Needs Review`: https://github.com/symfony/symfony/labels/Status%3A%20Needs%20Review diff --git a/contributing/documentation/format.rst b/contributing/documentation/format.rst index fe9aa7e3853..c4c7c659446 100644 --- a/contributing/documentation/format.rst +++ b/contributing/documentation/format.rst @@ -131,7 +131,7 @@ If you want to modify that title, use this alternative syntax: .. code-block:: rst - :doc:`Spooling Email ` + :doc:`Doctrine Associations ` .. note:: @@ -148,12 +148,10 @@ If you want to modify that title, use this alternative syntax: :doc:`environments` **Links to the API** follow a different syntax, where you must specify the type -of the linked resource (``namespace``, ``class`` or ``method``): +of the linked resource (``class`` or ``method``): .. code-block:: rst - :namespace:`Symfony\\Component\\BrowserKit` - :class:`Symfony\\Component\\Routing\\Matcher\\ApacheUrlMatcher` :method:`Symfony\\Component\\HttpKernel\\Bundle\\Bundle::build` @@ -194,7 +192,7 @@ describe how the behavior has changed: Support for ICU MessageFormat was introduced in Symfony 4.2. Prior to this, pluralization was managed by the ``transChoice`` method. -For a deprecation use the ``.. deprecated:: 4.X`` directive: +For a deprecation use the ``.. deprecated:: 4.x`` directive: .. code-block:: rst diff --git a/contributing/documentation/overview.rst b/contributing/documentation/overview.rst index bd0c9564ab4..19dd6d030ce 100644 --- a/contributing/documentation/overview.rst +++ b/contributing/documentation/overview.rst @@ -65,9 +65,9 @@ Let's imagine that you want to improve the Setup guide. In order to make your changes, follow these steps: **Step 1.** Go to the official Symfony documentation repository located at -`github.com/symfony/symfony-docs`_ and click on the **Fork** button to `fork the -repository`_ to your personal account. This is only needed the first time you -contribute to Symfony. +`github.com/symfony/symfony-docs`_ and click on the **Fork** button to +`fork the repository`_ to your personal account. This is only needed the first +time you contribute to Symfony. **Step 2.** **Clone** the forked repository to your local machine (this example uses the ``projects/symfony-docs/`` directory to store the documentation; change @@ -339,7 +339,6 @@ definitely don't want you to waste your time! .. _`Symfony Documentation Contributors`: https://symfony.com/contributors/doc .. _`SymfonyConnect`: https://connect.symfony.com/ .. _`Symfony Documentation Badge`: https://connect.symfony.com/badge/36/symfony-documentation-contributor -.. _`sync your fork`: https://help.github.com/articles/syncing-a-fork .. _`SymfonyCloud`: https://symfony.com/cloud .. _`roadmap`: https://symfony.com/roadmap .. _`pip`: https://pip.pypa.io/en/stable/ diff --git a/controller.rst b/controller.rst index 35dc6358238..879c984bfc8 100644 --- a/controller.rst +++ b/controller.rst @@ -125,6 +125,8 @@ method is just a helper method that generates the URL for a given route:: $url = $this->generateUrl('app_lucky_number', ['max' => 10]); +.. _controller-redirect: + Redirecting ~~~~~~~~~~~ @@ -177,7 +179,7 @@ object for you:: return $this->render('lucky/number.html.twig', ['number' => $number]); Templating and Twig are explained more in the -:doc:`Creating and Using Templates article `. +:doc:`Creating and Using Templates article `. .. index:: single: Controller; Accessing services @@ -289,6 +291,7 @@ new controller class: $ php bin/console make:controller BrandNewController created: src/Controller/BrandNewController.php + created: templates/brandnew/index.html.twig If you want to generate an entire CRUD from a Doctrine :doc:`entity `, use: @@ -297,6 +300,15 @@ use: $ php bin/console make:crud Product + created: src/Controller/ProductController.php + created: src/Form/ProductType.php + created: templates/product/_delete_form.html.twig + created: templates/product/_form.html.twig + created: templates/product/edit.html.twig + created: templates/product/index.html.twig + created: templates/product/new.html.twig + created: templates/product/show.html.twig + .. versionadded:: 1.2 The ``make:crud`` command was introduced in MakerBundle 1.2. @@ -449,7 +461,8 @@ and then redirects. The message key (``notice`` in this example) can be anything you'll use this key to retrieve the message. In the template of the next page (or even better, in your base layout template), -read any flash messages from the session using ``app.flashes()``: +read any flash messages from the session using the ``flashes()`` method provided +by the :ref:`Twig global app variable `: .. code-block:: html+twig @@ -553,6 +566,19 @@ response types. Some of these are mentioned below. To learn more about the ``Request`` and ``Response`` (and different ``Response`` classes), see the :ref:`HttpFoundation component documentation `. +Accessing Configuration Values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To get the value of any :ref:`configuration parameter ` +from a controller, use the ``getParameter()`` helper method:: + + // ... + public function index() + { + $contentsDir = $this->getParameter('kernel.project_dir').'/contents'; + // ... + } + Returning JSON Response ~~~~~~~~~~~~~~~~~~~~~~~ @@ -625,7 +651,7 @@ handle caching and more. Keep Going! ----------- -Next, learn all about :doc:`rendering templates with Twig `. +Next, learn all about :doc:`rendering templates with Twig `. Learn more about Controllers ---------------------------- @@ -633,7 +659,7 @@ Learn more about Controllers .. toctree:: :hidden: - templating + templates .. toctree:: :maxdepth: 1 diff --git a/controller/error_pages.rst b/controller/error_pages.rst index a17d2b2529f..a4026b75a66 100644 --- a/controller/error_pages.rst +++ b/controller/error_pages.rst @@ -150,7 +150,7 @@ Fortunately, the default ``ExceptionController`` allows you to preview your *error* pages during development. To use this feature, you need to load some special routes provided by TwigBundle -(if the application uses :doc:`Symfony Flex ` they are loaded +(if the application uses :ref:`Symfony Flex ` they are loaded automatically when installing Twig support): .. configuration-block:: @@ -339,7 +339,7 @@ error pages. .. note:: If your listener calls ``setResponse()`` on the - :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent`, + :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent`, event, propagation will be stopped and the response will be sent to the client. @@ -357,6 +357,3 @@ time and again, you can have just one (or several) listeners deal with them. out and other things. .. _`TwigBundle`: https://github.com/symfony/twig-bundle -.. _`WebfactoryExceptionsBundle`: https://github.com/webfactory/exceptions-bundle -.. _`Symfony Standard Edition`: https://github.com/symfony/symfony-standard/ -.. _`ExceptionListener`: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php diff --git a/controller/service.rst b/controller/service.rst index 247b5737c45..a9098e16b86 100644 --- a/controller/service.rst +++ b/controller/service.rst @@ -184,7 +184,6 @@ If you want to know what type-hints to use for each service, see the ``getSubscribedServices()`` method in `AbstractController`_. .. _`Controller class source code`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php -.. _`base Controller class`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php .. _`ControllerTrait`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php .. _`AbstractController`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php .. _`ADR pattern`: https://en.wikipedia.org/wiki/Action%E2%80%93domain%E2%80%93responder diff --git a/controller/upload_file.rst b/controller/upload_file.rst index ce63d2bd449..b90c199bab3 100644 --- a/controller/upload_file.rst +++ b/controller/upload_file.rst @@ -353,9 +353,8 @@ Using a Doctrine Listener ------------------------- The previous versions of this article explained how to handle file uploads using -:doc:`Doctrine listeners `. However, this -is no longer recommended, because Doctrine events shouldn't be used for your -domain logic. +:ref:`Doctrine listeners `. However, this is no longer +recommended, because Doctrine events shouldn't be used for your domain logic. Moreover, Doctrine listeners are often dependent on internal Doctrine behavior which may change in future versions. Also, they can introduce performance issues diff --git a/create_framework/dependency_injection.rst b/create_framework/dependency_injection.rst index 180c7303d31..27991a50915 100644 --- a/create_framework/dependency_injection.rst +++ b/create_framework/dependency_injection.rst @@ -253,4 +253,3 @@ internally. Have fun! .. _`Pimple`: https://github.com/silexphp/Pimple -.. _`Application`: https://github.com/silexphp/Silex/blob/master/src/Silex/Application.php diff --git a/create_framework/event_dispatcher.rst b/create_framework/event_dispatcher.rst index 5d8c6d303ca..fd655a93ebf 100644 --- a/create_framework/event_dispatcher.rst +++ b/create_framework/event_dispatcher.rst @@ -76,7 +76,7 @@ the Response instance:: } // dispatch a response event - $this->dispatcher->dispatch('response', new ResponseEvent($response, $request)); + $this->dispatcher->dispatch(new ResponseEvent($response, $request), 'response'); return $response; } @@ -88,9 +88,9 @@ now dispatched:: // example.com/src/Simplex/ResponseEvent.php namespace Simplex; - use Symfony\Component\EventDispatcher\Event; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; + use Symfony\Contracts\EventDispatcher\Event; class ResponseEvent extends Event { diff --git a/create_framework/http_foundation.rst b/create_framework/http_foundation.rst index db8021a1877..b86ac80ada5 100644 --- a/create_framework/http_foundation.rst +++ b/create_framework/http_foundation.rst @@ -270,7 +270,7 @@ cases by yourself. Why not using a technology that already works? .. note:: If you want to learn more about the HttpFoundation component, you can have - a look at the :namespace:`Symfony\\Component\\HttpFoundation` API or read + a look at the ``Symfony\Component\HttpFoundation`` API or read its dedicated :doc:`documentation `. Believe or not but we have our first framework. You can stop now if you want. @@ -284,8 +284,8 @@ the wheel. I've almost forgot to talk about one added benefit: using the HttpFoundation component is the start of better interoperability between all frameworks and -`applications using it`_ (like `Symfony`_, `Drupal 8`_, `phpBB 3`_, `ezPublish -5`_, `Laravel`_ and `more`_). +`applications using it`_ (like `Symfony`_, `Drupal 8`_, `phpBB 3`_, `Laravel`_ +and `ezPublish 5`_, and `more`_). .. _`Twig`: https://twig.symfony.com/ .. _`HTTP specification`: https://tools.ietf.org/wg/httpbis/ @@ -296,8 +296,6 @@ component is the start of better interoperability between all frameworks and .. _`phpBB 3`: https://www.phpbb.com/ .. _`ezPublish 5`: https://ez.no/ .. _`Laravel`: https://laravel.com/ -.. _`Midgard CMS`: http://www.midgard-project.org/ -.. _`Zikula`: https://zikula.org/ .. _`autoloaded`: https://php.net/autoload .. _`PSR-4`: https://www.php-fig.org/psr/psr-4/ .. _`more`: https://symfony.com/components/HttpFoundation diff --git a/create_framework/http_kernel_controller_resolver.rst b/create_framework/http_kernel_controller_resolver.rst index 4afcd9feb27..5b998ed2dab 100644 --- a/create_framework/http_kernel_controller_resolver.rst +++ b/create_framework/http_kernel_controller_resolver.rst @@ -203,4 +203,3 @@ Think about it once more: our framework is more robust and more flexible than ever and it still has less than 50 lines of code. .. _`reflection`: https://php.net/reflection -.. _`FrameworkExtraBundle`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html diff --git a/create_framework/http_kernel_httpkernel_class.rst b/create_framework/http_kernel_httpkernel_class.rst index 43dd45b7182..f9f8f16932f 100644 --- a/create_framework/http_kernel_httpkernel_class.rst +++ b/create_framework/http_kernel_httpkernel_class.rst @@ -154,11 +154,11 @@ only if needed:: use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; + use Symfony\Component\HttpKernel\Event\ViewEvent; class StringResponseListener implements EventSubscriberInterface { - public function onView(GetResponseForControllerResultEvent $event) + public function onView(ViewEvent $event) { $response = $event->getControllerResult(); diff --git a/deployment.rst b/deployment.rst index 27a76b78165..b0ab569c87f 100644 --- a/deployment.rst +++ b/deployment.rst @@ -118,8 +118,8 @@ you'll need to do: A) Check Requirements ~~~~~~~~~~~~~~~~~~~~~ -Use the :doc:`Symfony Requirements Checker ` to check -if your server meets the technical requirements to run Symfony applications. +Use the ``check:requirements`` command to check if your server meets the +:ref:`technical requirements for running Symfony applications `. .. _b-configure-your-app-config-parameters-yml-file: @@ -170,7 +170,7 @@ as you normally do: If you get a "class not found" error during this step, you may need to run ``export APP_ENV=prod`` (or ``export SYMFONY_ENV=prod`` if you're not - using :doc:`Symfony Flex `) before running this command so + using :ref:`Symfony Flex `) before running this command so that the ``post-install-cmd`` scripts run in the ``prod`` environment. D) Clear your Symfony Cache @@ -248,7 +248,7 @@ Learn More .. _`Deployer`: http://deployer.org/ .. _`Git Tagging`: https://git-scm.com/book/en/v2/Git-Basics-Tagging .. _`Heroku`: https://devcenter.heroku.com/articles/getting-started-with-symfony -.. _`platform.sh`: https://docs.platform.sh/frameworks/symfony.html +.. _`Platform.sh`: https://docs.platform.sh/frameworks/symfony.html .. _`Azure`: https://azure.microsoft.com/en-us/develop/php/ .. _`fortrabbit`: https://help.fortrabbit.com/install-symfony .. _`EasyDeployBundle`: https://github.com/EasyCorp/easy-deploy-bundle diff --git a/deployment/heroku.rst b/deployment/heroku.rst index a7527805bd2..1e2e1635c55 100644 --- a/deployment/heroku.rst +++ b/deployment/heroku.rst @@ -7,6 +7,6 @@ Deploying to Heroku =================== To deploy to Heroku, see their official documentation: -`Getting Started with Symfony on Heroku`_. +`Deploying Symfony 4 Apps on Heroku`_. -.. _`Getting Started with Symfony on Heroku`: https://devcenter.heroku.com/articles/getting-started-with-symfony +.. _`Deploying Symfony 4 Apps on Heroku`: https://devcenter.heroku.com/articles/deploying-symfony4 diff --git a/deployment/proxies.rst b/deployment/proxies.rst index 2dd0ac06eb6..6e54ce5b612 100644 --- a/deployment/proxies.rst +++ b/deployment/proxies.rst @@ -76,5 +76,21 @@ That's it! It's critical that you prevent traffic from all non-trusted sources. If you allow outside traffic, they could "spoof" their true IP address and other information. +Custom Headers When Using a Reverse Proxy +----------------------------------------- + +Some reverse proxies (like `CloudFront`_ with ``CloudFront-Forwarded-Proto``) may force you to use a custom header. +For instance you have ``Custom-Forwarded-Proto`` instead of ``X-Forwarded-Proto``. + +In this case, you'll need to set the header ``X-Forwarded-Proto`` with the value of +``Custom-Forwarded-Proto`` early enough in your application, i.e. before handling the request:: + + // public/index.php + + // ... + $_SERVER['HEADER_X_FORWARDED_PROTO'] = $_SERVER['HEADER_CUSTOM_FORWARDED_PROTO']; + // ... + $response = $kernel->handle($request); + .. _`security groups`: http://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-groups.html -.. _`RFC 7239`: http://tools.ietf.org/html/rfc7239 +.. _`CloudFront`: https://en.wikipedia.org/wiki/Amazon_CloudFront diff --git a/doctrine.rst b/doctrine.rst index 433ee746e00..485254f7bba 100644 --- a/doctrine.rst +++ b/doctrine.rst @@ -9,22 +9,24 @@ Databases and the Doctrine ORM Do you prefer video tutorials? Check out the `Doctrine screencast series`_. -Symfony doesn't provide a component to work with the database, but it *does* provide -tight integration with a third-party library called `Doctrine`_. +Symfony provides all the tools you need to use databases in your applications +thanks to `Doctrine`_, the best set of PHP libraries to work with databases. +These tools support relational databases like MySQL and PostgreSQL and also +NoSQL databases like MongoDB. -.. note:: - - This article is all about using the Doctrine ORM. If you prefer to use raw - database queries, see the ":doc:`/doctrine/dbal`" article instead. +Databases are a broad topic, so the documentation is divided in three articles: - You can also persist data to `MongoDB`_ using Doctrine ODM library. See the - "`DoctrineMongoDBBundle`_" documentation. +* This article explains the recommended way to work with **relational databases** + in Symfony applications; +* Read :doc:`this other article ` if you need **low-level access** + to perform raw SQL queries to relational databases (similar to PHP's `PDO`_); +* Read `DoctrineMongoDBBundle docs`_ if you are working with **MongoDB databases**. Installing Doctrine ------------------- -First, install Doctrine support via the ORM pack, as well as the MakerBundle, -which will help generate some code: +First, install Doctrine support via the ``orm`` :ref:`Symfony pack `, +as well as the MakerBundle, which will help generate some code: .. code-block:: terminal @@ -353,12 +355,12 @@ and save it:: class ProductController extends AbstractController { /** - * @Route("/product", name="product") + * @Route("/product", name="create_product") */ - public function index() + public function createProduct(): Response { // you can fetch the EntityManager via $this->getDoctrine() - // or you can add an argument to your action: index(EntityManagerInterface $entityManager) + // or you can add an argument to the action: createProduct(EntityManagerInterface $entityManager) $entityManager = $this->getDoctrine()->getManager(); $product = new Product(); @@ -418,6 +420,78 @@ Take a look at the previous example in more detail: Whether you're creating or updating objects, the workflow is always the same: Doctrine is smart enough to know if it should INSERT or UPDATE your entity. +.. _automatic_object_validation: + +Validating Objects +------------------ + +:doc:`The Symfony validator ` reuses Doctrine metadata to perform +some basic validation tasks:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use App\Entity\Product; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Validator\Validator\ValidatorInterface; + // ... + + class ProductController extends AbstractController + { + /** + * @Route("/product", name="create_product") + */ + public function createProduct(ValidatorInterface $validator): Response + { + $product = new Product(); + // This will trigger an error: the column isn't nullable in the database + $product->setName(null); + // This will trigger a type mismatch error: an integer is expected + $product->setPrice('1999'); + + // ... + + $errors = $validator->validate($product); + if (count($errors) > 0) { + return new Response((string) $errors, 400); + } + + // ... + } + } + +Although the ``Product`` entity doesn't define any explicit +:doc:`validation configuration `, Symfony introspects the Doctrine +mapping configuration to infer some validation rules. For example, given that +the ``name`` property can't be ``null`` in the database, a +:doc:`NotNull constraint ` is added automatically +to the property (if it doesn't contain that constraint already). + +The following table summarizes the mapping between Doctrine metadata and +the corresponding validation constraints added automatically by Symfony: + +================== ========================================================= ===== +Doctrine attribute Validation constraint Notes +================== ========================================================= ===== +``nullable=false`` :doc:`NotNull ` Requires installing the :doc:`PropertyInfo component ` +``type`` :doc:`Type ` Requires installing the :doc:`PropertyInfo component ` +``unique=true`` :doc:`UniqueEntity ` +``length`` :doc:`Length ` +================== ========================================================= ===== + +Because :doc:`the Form component ` as well as `API Platform`_ internally +use the Validator component, all your forms and web APIs will also automatically +benefit from these automatic validation constraints. + +This automatic validation is a nice feature to improve your productivity, but it +doesn't replace the validation configuration entirely. You still need to add +some :doc:`validation constraints ` to ensure that data +provided by the user is correct. + +.. versionadded:: 4.3 + + The automatic validation has been added in Symfony 4.3. + Fetching Objects from the Database ---------------------------------- @@ -496,8 +570,8 @@ the :ref:`doctrine-queries` section. If the number of database queries is too high, the icon will turn yellow to indicate that something may not be correct. Click on the icon to open the Symfony Profiler and see the exact queries that were executed. If you don't - see the web debug toolbar, try running ``composer require --dev symfony/profiler-pack`` - to install it. + see the web debug toolbar, install the ``profiler`` :ref:`Symfony pack ` + by running this command: ``composer require --dev symfony/profiler-pack``. Automatically Fetching Objects (ParamConverter) ----------------------------------------------- @@ -599,11 +673,11 @@ But what if you need a more complex query? When you generated your entity with use App\Entity\Product; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; - use Symfony\Bridge\Doctrine\RegistryInterface; + use Doctrine\Common\Persistence\ManagerRegistry; class ProductRepository extends ServiceEntityRepository { - public function __construct(RegistryInterface $registry) + public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Product::class); } @@ -621,7 +695,7 @@ a new method for this to your repository:: // ... class ProductRepository extends ServiceEntityRepository { - public function __construct(RegistryInterface $registry) + public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Product::class); } @@ -725,58 +799,10 @@ relationships. For info, see :doc:`/doctrine/associations`. -.. _doctrine-fixtures: - -Dummy Data Fixtures -------------------- - -Doctrine provides a library that allows you to programmatically load testing -data into your project (i.e. "fixture data"). Install it with: - -.. code-block:: terminal - - $ composer require --dev doctrine/doctrine-fixtures-bundle - -Then, use the ``make:fixtures`` command to generate an empty fixture class: - -.. code-block:: terminal - - $ php bin/console make:fixtures - - The class name of the fixtures to create (e.g. AppFixtures): - > ProductFixture - -Customize the new class to load ``Product`` objects into Doctrine:: - - // src/DataFixtures/ProductFixture.php - namespace App\DataFixtures; +Database Testing +---------------- - use Doctrine\Bundle\FixturesBundle\Fixture; - use Doctrine\Common\Persistence\ObjectManager; - - class ProductFixture extends Fixture - { - public function load(ObjectManager $manager) - { - $product = new Product(); - $product->setName('Priceless widget!'); - $product->setPrice(14.50); - $product->setDescription('Ok, I guess it *does* have a price'); - $manager->persist($product); - - // add more products - - $manager->flush(); - } - } - -Empty the database and reload *all* the fixture classes with: - -.. code-block:: terminal - - $ php bin/console doctrine:fixtures:load - -For information, see the "`DoctrineFixturesBundle`_" documentation. +Read the article about :doc:`testing code that interacts with the database `. Learn more ---------- @@ -786,8 +812,7 @@ Learn more doctrine/associations doctrine/common_extensions - doctrine/lifecycle_callbacks - doctrine/event_listeners_subscribers + doctrine/events doctrine/registration_form doctrine/custom_dql_functions doctrine/dbal @@ -796,19 +821,15 @@ Learn more doctrine/mongodb_session_storage doctrine/resolve_target_entity doctrine/reverse_engineering - -* `DoctrineFixturesBundle`_ + testing/database .. _`Doctrine`: http://www.doctrine-project.org/ .. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt -.. _`MongoDB`: https://www.mongodb.org/ .. _`Doctrine's Mapping Types documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html .. _`Query Builder`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/query-builder.html .. _`Doctrine Query Language`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html -.. _`Mapping Types Documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#property-mapping .. _`Reserved SQL keywords documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#quoting-reserved-words -.. _`DoctrineMongoDBBundle`: https://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html -.. _`DoctrineFixturesBundle`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html +.. _`DoctrineMongoDBBundle docs`: https://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html .. _`Transactions and Concurrency`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/transactions-and-concurrency.html .. _`DoctrineMigrationsBundle`: https://github.com/doctrine/DoctrineMigrationsBundle .. _`NativeQuery`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/native-sql.html @@ -816,3 +837,5 @@ Learn more .. _`ParamConverter`: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html .. _`limit of 767 bytes for the index key prefix`: https://dev.mysql.com/doc/refman/5.6/en/innodb-restrictions.html .. _`Doctrine screencast series`: https://symfonycasts.com/screencast/symfony-doctrine +.. _`API Platform`: https://api-platform.com/docs/core/validation/ +.. _`PDO`: https://php.net/pdo diff --git a/doctrine/common_extensions.rst b/doctrine/common_extensions.rst index 3a3f559fc81..c4c27461046 100644 --- a/doctrine/common_extensions.rst +++ b/doctrine/common_extensions.rst @@ -14,8 +14,7 @@ functionality for `Sluggable`_, `Translatable`_, `Timestampable`_, `Loggable`_, The usage for each of these extensions is explained in that repository. However, to install/activate each extension you must register and activate an -:doc:`Event Listener `. -To do this, you have two options: +:ref:`Doctrine event Listener `. To do this, you have two options: #. Use the `StofDoctrineExtensionsBundle`_, which integrates the above library. diff --git a/doctrine/dbal.rst b/doctrine/dbal.rst index b584ab479e3..ab1fa4c69e7 100644 --- a/doctrine/dbal.rst +++ b/doctrine/dbal.rst @@ -22,7 +22,7 @@ data manipulations. Read the official Doctrine `DBAL Documentation`_ to learn all the details and capabilities of Doctrine's DBAL library. -First, install the Doctrine ORM pack: +First, install the Doctrine ``orm`` :ref:`Symfony pack `: .. code-block:: terminal diff --git a/doctrine/event_listeners_subscribers.rst b/doctrine/event_listeners_subscribers.rst deleted file mode 100644 index de27feecb26..00000000000 --- a/doctrine/event_listeners_subscribers.rst +++ /dev/null @@ -1,267 +0,0 @@ -.. index:: - single: Doctrine; Event listeners and subscribers - -.. _doctrine-event-config: -.. _how-to-register-event-listeners-and-subscribers: - -Doctrine Event Listeners and Subscribers -======================================== - -Doctrine packages have a rich event system that fires events when almost anything -happens inside the system. For you, this means that you can create arbitrary -:doc:`services ` and tell Doctrine to notify those -objects whenever a certain action (e.g. ``prePersist()``) happens within Doctrine. -This could be useful, for example, to create an independent search index -whenever an object in your database is saved. - -Doctrine defines two types of objects that can listen to Doctrine events: -listeners and subscribers. Both are very similar, but listeners are a bit -more straightforward. For more, see `The Event System`_ on Doctrine's documentation. - -Before using them, keep in mind that Doctrine events are intended for -persistence hooks (i.e. *"save also this when saving that"*). They should not be -used for domain logic, such as logging changes, setting ``updatedAt`` and -``createdAt`` properties, etc. - -.. seealso:: - - This article covers listeners and subscribers for Doctrine ORM. If you are - using ODM for MongoDB, read the `DoctrineMongoDBBundle documentation`_. - -Configuring the Listener/Subscriber ------------------------------------ - -To register a service to act as an event listener or subscriber you have -to :doc:`tag ` it with the appropriate name. Depending -on your use-case, you can hook a listener into every DBAL connection and ORM -entity manager or just into one specific DBAL connection and all the entity -managers that use this connection. - -.. configuration-block:: - - .. code-block:: yaml - - services: - # ... - - App\EventListener\SearchIndexer: - tags: - - { name: doctrine.event_listener, event: postPersist } - App\EventListener\SearchIndexer2: - tags: - - { name: doctrine.event_listener, event: postPersist, connection: default } - App\EventListener\SearchIndexerSubscriber: - tags: - - { name: doctrine.event_subscriber, connection: default } - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - .. code-block:: php - - use App\EventListener\SearchIndexer; - use App\EventListener\SearchIndexer2; - use App\EventListener\SearchIndexerSubscriber; - - $container->autowire(SearchIndexer::class) - ->addTag('doctrine.event_listener', ['event' => 'postPersist']) - ; - $container->autowire(SearchIndexer2::class) - ->addTag('doctrine.event_listener', [ - 'event' => 'postPersist', - 'connection' => 'default', - ]) - ; - $container->autowire(SearchIndexerSubscriber::class) - ->addTag('doctrine.event_subscriber', ['connection' => 'default']) - ; - -Creating the Listener Class ---------------------------- - -In the previous example, a ``SearchIndexer`` service was configured as a Doctrine -listener on the event ``postPersist``. The class behind that service must have -a ``postPersist()`` method, which will be called when the event is dispatched:: - - // src/EventListener/SearchIndexer.php - namespace App\EventListener; - - use App\Entity\Product; - // for Doctrine < 2.4: use Doctrine\ORM\Event\LifecycleEventArgs; - use Doctrine\Common\Persistence\Event\LifecycleEventArgs; - - class SearchIndexer - { - public function postPersist(LifecycleEventArgs $args) - { - $entity = $args->getObject(); - - // only act on some "Product" entity - if (!$entity instanceof Product) { - return; - } - - $entityManager = $args->getObjectManager(); - // ... do something with the Product - } - } - -In each event, you have access to a ``LifecycleEventArgs`` object, which -gives you access to both the entity object of the event and the entity manager -itself. - -One important thing to notice is that a listener will be listening for *all* -entities in your application. So, if you're interested in only handling a -specific type of entity (e.g. a ``Product`` entity but not a ``BlogPost`` -entity), you should check for the entity's class type in your method -(as shown above). - -.. tip:: - - In Doctrine 2.4, a feature called Entity Listeners was introduced. - It is a lifecycle listener class used for an entity. You can read - about it in `the DoctrineBundle documentation`_. - -Creating the Subscriber Class ------------------------------ - -A Doctrine event subscriber must implement the ``Doctrine\Common\EventSubscriber`` -interface and have an event method for each event it subscribes to:: - - // src/EventListener/SearchIndexerSubscriber.php - namespace App\EventListener; - - use App\Entity\Product; - use Doctrine\Common\EventSubscriber; - // for Doctrine < 2.4: use Doctrine\ORM\Event\LifecycleEventArgs; - use Doctrine\Common\Persistence\Event\LifecycleEventArgs; - use Doctrine\ORM\Events; - - class SearchIndexerSubscriber implements EventSubscriber - { - public function getSubscribedEvents() - { - return [ - Events::postPersist, - Events::postUpdate, - ]; - } - - public function postUpdate(LifecycleEventArgs $args) - { - $this->index($args); - } - - public function postPersist(LifecycleEventArgs $args) - { - $this->index($args); - } - - public function index(LifecycleEventArgs $args) - { - $entity = $args->getObject(); - - // perhaps you only want to act on some "Product" entity - if ($entity instanceof Product) { - $entityManager = $args->getObjectManager(); - // ... do something with the Product - } - } - } - -.. tip:: - - Doctrine event subscribers cannot return a flexible array of methods to - call for the events like the :ref:`Symfony event subscriber ` - can. Doctrine event subscribers must return a simple array of the event - names they subscribe to. Doctrine will then expect methods on the subscriber - with the same name as each subscribed event, just as when using an event listener. - -For a full reference, see chapter `The Event System`_ in the Doctrine documentation. - -Performance Considerations --------------------------- - -One important difference between listeners and subscribers is that Symfony loads -entity listeners lazily. This means that the listener classes are only fetched -from the service container (and instantiated) if the related event is actually -fired. - -That's why it is preferable to use entity listeners instead of subscribers -whenever possible. - -Priorities for Event Listeners ------------------------------- - -In case you have multiple listeners for the same event you can control the order -in which they are invoked using the ``priority`` attribute on the tag. Priorities -are defined with positive or negative integers (they default to ``0``). Higher -numbers mean that listeners are invoked earlier. - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - App\EventListener\MyHighPriorityListener: - tags: - - { name: doctrine.event_listener, event: postPersist, priority: 10 } - - App\EventListener\MyLowPriorityListener: - tags: - - { name: doctrine.event_listener, event: postPersist, priority: 1 } - - .. code-block:: xml - - - - - - - - - - - - - - - - .. code-block:: php - - // config/services.php - use App\EventListener\MyHighPriorityListener; - use App\EventListener\MyLowPriorityListener; - - $container - ->autowire(MyHighPriorityListener::class) - ->addTag('doctrine.event_listener', ['event' => 'postPersist', 'priority' => 10]) - ; - - $container - ->autowire(MyLowPriorityListener::class) - ->addTag('doctrine.event_listener', ['event' => 'postPersist', 'priority' => 1]) - ; - -.. _`The Event System`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html -.. _`the DoctrineBundle documentation`: https://symfony.com/doc/current/bundles/DoctrineBundle/entity-listeners.html -.. _`DoctrineMongoDBBundle documentation`: https://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html diff --git a/doctrine/events.rst b/doctrine/events.rst new file mode 100644 index 00000000000..3df6784bc42 --- /dev/null +++ b/doctrine/events.rst @@ -0,0 +1,445 @@ +.. index:: + single: Doctrine; Lifecycle Callbacks; Doctrine Events + +Doctrine Events +=============== + +`Doctrine`_, the set of PHP libraries used by Symfony to work with databases, +provides a lightweight event system to update entities during the application +execution. These events, called `lifecycle events`_, allow to perform tasks such +as *"update the createdAt property automatically just before persisting entities +of this type"*. + +Doctrine triggers events before/after performing the most common entity +operations (e.g. ``prePersist/postPersist``, ``preUpdate/postUpdate``) and also +on other common tasks (e.g. ``loadClassMetadata``, ``onClear``). + +There are different ways to listen to these Doctrine events: + +* **Lifecycle callbacks**, they are defined as methods on the entity classes and + they are called when the events are triggered; +* **Lifecycle listeners and subscribers**, they are classes with callback + methods for one or more events and they are called for all entities; +* **Entity listeners**, they are similar to lifecycle listeners, but they are + called only for the entities of a certain class. + +These are the **drawbacks and advantages** of each one: + +* Callbacks have better performance because they only apply to a single entity + class, but you can't reuse the logic for different entities and they can't + access to :doc:`Symfony services `; +* Lifecycle listeners and subscribers can reuse logic among different entities + and can access Symfony services but their performance is worse because they + are called for all entities; +* Entity listeners have the same advantages of lifecycle listeners and they have + better performance because they only apply to a single entity class. + +This article only explains the basics about Doctrine events when using them +inside a Symfony application. Read the `official docs about Doctrine events`_ +to learn everything about them. + +.. seealso:: + + This article covers listeners and subscribers for Doctrine ORM. If you are + using ODM for MongoDB, read the `DoctrineMongoDBBundle documentation`_. + +Doctrine Lifecycle Callbacks +---------------------------- + +Lifecycle callbacks are defined as methods inside the entity you want to modify. +For example, suppose you want to set a ``createdAt`` date column to the current +date, but only when the entity is first persisted (i.e. inserted). To do so, +define a callback for the ``prePersist`` Doctrine event: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Entity/Product.php + use Doctrine\ORM\Mapping as ORM; + + // When using annotations, don't forget to add @ORM\HasLifecycleCallbacks() + // to the class of the entity where you define the callback + + /** + * @ORM\Entity() + * @ORM\HasLifecycleCallbacks() + */ + class Product + { + // ... + + /** + * @ORM\PrePersist + */ + public function setCreatedAtValue() + { + $this->createdAt = new \DateTime(); + } + } + + .. code-block:: yaml + + # config/doctrine/Product.orm.yml + App\Entity\Product: + type: entity + # ... + lifecycleCallbacks: + prePersist: ['setCreatedAtValue'] + + .. code-block:: xml + + + + + + + + + + + + + +.. note:: + + Some lifecycle callbacks receive an argument that provides access to + useful information such as the current entity manager (e.g. the ``preUpdate`` + callback receives a ``PreUpdateEventArgs $event`` argument). + +.. _doctrine-lifecycle-listener: + +Doctrine Lifecycle Listeners +---------------------------- + +Lifecycle listeners are defined as PHP classes that listen to a single Doctrine +event on all the application entities. For example, suppose that you want to +update some search index whenever a new entity is persisted in the database. To +do so, define a listener for the ``postPersist`` Doctrine event:: + + // src/EventListener/SearchIndexer.php + namespace App\EventListener; + + use App\Entity\Product; + use Doctrine\Common\Persistence\Event\LifecycleEventArgs; + + class SearchIndexer + { + // the listener methods receive an argument which gives you access to + // both the entity object of the event and the entity manager itself + public function postPersist(LifecycleEventArgs $args) + { + $entity = $args->getObject(); + + // if this listener only applies to certain entity types, + // add some code to check the entity type as early as possible + if (!$entity instanceof Product) { + return; + } + + $entityManager = $args->getObjectManager(); + // ... do something with the Product entity + } + } + +The next step is to enable the Doctrine listener in the Symfony application by +creating a new service for it and :doc:`tagging it ` +with the ``doctrine.event_listener`` tag: + +.. configuration-block:: + + .. code-block:: yaml + + services: + # ... + + App\EventListener\SearchIndexer: + tags: + - + name: 'doctrine.event_listener' + # this is the only required option for the lifecycle listener tag + event: 'postPersist' + + # listeners can define their priority in case multiple listeners are associated + # to the same event (default priority = 0; higher numbers = listener is run earlier) + priority: 500 + + # you can also restrict listeners to a specific Doctrine connection + connection: 'default' + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + use App\EventListener\SearchIndexer; + + // listeners are applied by default to all Doctrine connections + $container->autowire(SearchIndexer::class) + ->addTag('doctrine.event_listener', [ + // this is the only required option for the lifecycle listener tag + 'event' => 'postPersist', + + // listeners can define their priority in case multiple listeners are associated + // to the same event (default priority = 0; higher numbers = listener is run earlier) + 'priority' => 500, + + # you can also restrict listeners to a specific Doctrine connection + 'connection' => 'default', + ]) + ; + +.. tip:: + + Symfony loads (and instantiates) Doctrine listeners only when the related + Doctrine event is actually fired; whereas Doctrine subscribers are always + loaded (and instantiated) by Symfony, making them less performant. + +Doctrine Entity Listeners +------------------------- + +Entity listeners are defined as PHP classes that listen to a single Doctrine +event on a single entity class. For example, suppose that you want to send some +notifications whenever a ``User`` entity is modified in the database. To do so, +define a listener for the ``postUpdate`` Doctrine event:: + + // src/EventListener/UserChangedNotifier.php + namespace App\EventListener; + + use App\Entity\User; + use Doctrine\ORM\Event\PreUpdateEventArgs; + + class UserChangedNotifier + { + // the entity listener methods receive two arguments: + // the entity instance and the lifecycle event + public function postUpdate(User $user, PreUpdateEventArgs $event) + { + // ... do something to notify the changes + } + } + +The next step is to enable the Doctrine listener in the Symfony application by +creating a new service for it and :doc:`tagging it ` +with the ``doctrine.orm.entity_listener`` tag: + +.. configuration-block:: + + .. code-block:: yaml + + services: + # ... + + App\EventListener\UserChangedNotifier: + tags: + - + # these are the basic options that define the entity listener + name: 'doctrine.orm.entity_listener' + event: 'postUpdate' + entity: 'App\Entity\User' + + # set the 'lazy' option to TRUE to only instantiate listeners when they are used + lazy: true + + # you can also associate an entity listener to a specific entity manager + entity_manager: 'custom' + + # by default, Symfony looks for a method called after the event (e.g. postUpdate()) + # if it doesn't exist, it tries to execute the '__invoke()' method, but you can + # configure a custom method name with the 'method' option + method: 'checkUserChanges' + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + use App\Entity\User; + use App\EventListener\UserChangedNotifier; + + $container->autowire(UserChangedNotifier::class) + ->addTag('doctrine.orm.event_listener', [ + // these are the basic options that define the entity listener + 'event' => 'postUpdate', + 'entity' => User::class, + + // set the 'lazy' option to TRUE to only instantiate listeners when they are used + 'lazy' => true, + + // you can also associate an entity listener to a specific entity manager + 'entity_manager' => 'custom', + + // by default, Symfony looks for a method called after the event (e.g. postUpdate()) + // if it doesn't exist, it tries to execute the '__invoke()' method, but you can + // configure a custom method name with the 'method' option + 'method' => 'checkUserChanges', + ]) + ; + +Doctrine Lifecycle Subscribers +------------------------------ + +Lifecycle subscribers are defined as PHP classes that implement the +``Doctrine\Common\EventSubscriber`` interface and which listen to one or more +Doctrine events on all the application entities. For example, suppose that you +want to log all the database activity. To do so, define a listener for the +``postPersist``, ``postRemove`` and ``postUpdate`` Doctrine events:: + + // src/EventListener/DatabaseActivitySubscriber.php + namespace App\EventListener; + + use App\Entity\Product; + use Doctrine\Common\EventSubscriber; + use Doctrine\Common\Persistence\Event\LifecycleEventArgs; + use Doctrine\ORM\Events; + + class DatabaseActivitySubscriber implements EventSubscriber + { + // this method can only return the event names; you cannot define a + // custom method name to execute when each event triggers + public function getSubscribedEvents() + { + return [ + Events::postPersist, + Events::postRemove, + Events::postUpdate, + ]; + } + + // callback methods must be called exactly like the events they listen to; + // they receive an argument of type LifecycleEventArgs, which gives you access + // to both the entity object of the event and the entity manager itself + public function postPersist(LifecycleEventArgs $args) + { + $this->logActivity('persist', $args); + } + + public function postRemove(LifecycleEventArgs $args) + { + $this->logActivity('remove', $args); + } + + public function postUpdate(LifecycleEventArgs $args) + { + $this->logActivity('update', $args); + } + + private function logActivity(string $action, LifecycleEventArgs $args) + { + $entity = $args->getObject(); + + // if this subscriber only applies to certain entity types, + // add some code to check the entity type as early as possible + if (!$entity instanceof Product) { + return; + } + + // ... get the entity information and log it somehow + } + } + +If you're using the :ref:`default services.yaml configuration `, +Symfony will register the Doctrine subscriber automatically thanks to the +:ref:`autoconfigure ` and +:doc:`autowiring ` features. However, if you need +to associate the subscriber with a specific Doctrine connection, you must define +a service for it and :doc:`tag it ` with the +``doctrine.event_subscriber`` tag: + +.. configuration-block:: + + .. code-block:: yaml + + services: + # ... + + # in most applications you don't need to define a service for your + # subscriber (this is only needed when using a custom Doctrine connection) + App\EventListener\DatabaseActivitySubscriber: + tags: + - { name: 'doctrine.event_subscriber', connection: 'default' } + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + use App\EventListener\DatabaseActivitySubscriber; + + // in most applications you don't need to define a service for your + // subscriber (this is only needed when using a custom Doctrine connection) + $container->autowire(DatabaseActivitySubscriber::class) + ->addTag('doctrine.event_subscriber', ['connection' => 'default']) + ; + +.. tip:: + + Symfony loads (and instantiates) Doctrine subscribers whenever the + application executes; whereas Doctrine listeners are only loaded when the + related event is actually fired, making them more performant. + +.. _`Doctrine`: https://www.doctrine-project.org/ +.. _`lifecycle events`: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/events.html#lifecycle-events +.. _`official docs about Doctrine events`: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/events.html +.. _`DoctrineMongoDBBundle documentation`: https://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html diff --git a/doctrine/lifecycle_callbacks.rst b/doctrine/lifecycle_callbacks.rst deleted file mode 100644 index 9b62ae16f74..00000000000 --- a/doctrine/lifecycle_callbacks.rst +++ /dev/null @@ -1,100 +0,0 @@ -.. index:: - single: Doctrine; Lifecycle Callbacks - -How to Work with Lifecycle Callbacks -==================================== - -Sometimes, you need to perform an action right before or after an entity -is inserted, updated, or deleted. These types of actions are known as "lifecycle" -callbacks, as they're callback methods that you need to execute during different -stages of the lifecycle of an entity (e.g. the entity is inserted, updated, -deleted, etc). - -If you're using annotations for your metadata, start by enabling the lifecycle -callbacks. This is not necessary if you're using YAML or XML for your mapping. - -.. code-block:: php-annotations - - // src/Entity/Product.php - use Doctrine\ORM\Mapping as ORM; - - /** - * @ORM\Entity() - * @ORM\HasLifecycleCallbacks() - */ - class Product - { - // ... - } - -Now, you can tell Doctrine to execute a method on any of the available lifecycle -events. For example, suppose you want to set a ``createdAt`` date column to -the current date, only when the entity is first persisted (i.e. inserted): - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Entity/Product.php - use Doctrine\ORM\Mapping as ORM; - - /** - * @ORM\PrePersist - */ - public function setCreatedAtValue() - { - $this->createdAt = new \DateTime(); - } - - .. code-block:: yaml - - # config/doctrine/Product.orm.yml - App\Entity\Product: - type: entity - # ... - lifecycleCallbacks: - prePersist: [setCreatedAtValue] - - .. code-block:: xml - - - - - - - - - - - - - -.. note:: - - The above example assumes that you've created and mapped a ``createdAt`` - property (not shown here). - -Now, right before the entity is first persisted, Doctrine will automatically -call this method and the ``createdAt`` field will be set to the current date. - -There are several other lifecycle events that you can hook into. For more -information on other lifecycle events and lifecycle callbacks in general, see -Doctrine's `Lifecycle Events documentation`_. - -.. sidebar:: Lifecycle Callbacks and Event Listeners - - Notice that the ``setCreatedAtValue()`` method receives no arguments. This - is always the case for lifecycle callbacks and is intentional: lifecycle - callbacks should be simple methods that are concerned with internally - transforming data in the entity (e.g. setting a created/updated field, - generating a slug value). - - If you need to do some heavier lifting - like performing logging or sending - an email - you should register an external class as an event listener - or subscriber and give it access to whatever resources you need. For - more information, see :doc:`/doctrine/event_listeners_subscribers`. - -.. _`Lifecycle Events documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-events diff --git a/doctrine/pdo_session_storage.rst b/doctrine/pdo_session_storage.rst index 81fbbd2fead..ee6efc80de9 100644 --- a/doctrine/pdo_session_storage.rst +++ b/doctrine/pdo_session_storage.rst @@ -219,7 +219,7 @@ MySQL `sess_data` BLOB NOT NULL, `sess_time` INTEGER UNSIGNED NOT NULL, `sess_lifetime` MEDIUMINT NOT NULL - ) COLLATE utf8_bin, ENGINE = InnoDB; + ) COLLATE utf8mb4_bin, ENGINE = InnoDB; .. note:: diff --git a/doctrine/registration_form.rst b/doctrine/registration_form.rst index ff3ee4d4be2..d999eda77e9 100644 --- a/doctrine/registration_form.rst +++ b/doctrine/registration_form.rst @@ -6,242 +6,15 @@ How to Implement a Registration Form ==================================== -The basics of creating a registration form are the same as any normal form. After -all, you are creating an object with it (a user). However, since this is related -to security, there are some additional aspects. This article explains it all. - -Before you get Started ----------------------- - -To create the registration form, make sure you have these 3 things ready: - -**1) Install MakerBundle** - -Make sure MakerBundle is installed: - -.. code-block:: terminal - - $ composer require --dev symfony/maker-bundle - -If you need any other dependencies, MakerBundle will tell you when you run each -command. - -**2) Create a User Class** - -If you already have a :ref:`User class `, great! If not, you -can generate one by running: - -.. code-block:: terminal - - $ php bin/console make:user - -For more info, see :ref:`create-user-class`. - -**3) (Optional) Create a Guard Authenticator** - -If you want to automatically authenticate your user after registration, create -a Guard authenticator before generating your registration form. For details, see -the :ref:`firewalls-authentication` section on the main security page. - -Adding the Registration System ------------------------------- - -To easiest way to build your registration form is by using the ``make:registration-form`` -command: - -.. versionadded:: 1.11 - - The ``make:registration-form`` was introduced in MakerBundle 1.11.0. - -.. code-block:: terminal - - $ php bin/console make:registration-form - -This command needs to know several things - like your ``User`` class and information -about the properties on that class. The questions will vary based on your setup, -because the command will guess as much as possible. - -When the command is done, congratulations! You have a functional registration form -system that's ready for you to customize. The generated files will look something -like what you see below. - -RegistrationFormType -~~~~~~~~~~~~~~~~~~~~ - -The form class for the registration form will look something like this:: - - namespace App\Form; - - use App\Entity\User; - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\Extension\Core\Type\PasswordType; - use Symfony\Component\Form\FormBuilderInterface; - use Symfony\Component\OptionsResolver\OptionsResolver; - use Symfony\Component\Validator\Constraints\Length; - use Symfony\Component\Validator\Constraints\NotBlank; - - class RegistrationFormType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('email') - ->add('plainPassword', PasswordType::class, [ - // instead of being set onto the object directly, - // this is read and encoded in the controller - 'mapped' => false, - 'constraints' => [ - new NotBlank([ - 'message' => 'Please enter a password', - ]), - new Length([ - 'min' => 6, - 'minMessage' => 'Your password should be at least {{ limit }} characters', - 'max' => 4096, - ]), - ], - ]) - ; - } - - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults([ - 'data_class' => User::class, - ]); - } - } - -.. _registration-password-max: - -.. sidebar:: Why the 4096 Password Limit? - - Notice that the ``plainPassword`` field has a max length of 4096 characters. - For security purposes (`CVE-2013-5750`_), Symfony limits the plain password - length to 4096 characters when encoding it. Adding this constraint makes - sure that your form will give a validation error if anyone tries a super-long - password. - - You'll need to add this constraint anywhere in your application where - your user submits a plaintext password (e.g. change password form). The - only place where you don't need to worry about this is your login form, - since Symfony's Security component handles this for you. - -RegistrationController -~~~~~~~~~~~~~~~~~~~~~~ - -The controller builds the form and, on submit, encodes the plain password and -saves the user:: - - namespace App\Controller; - - use App\Entity\User; - use App\Form\RegistrationFormType; - use App\Security\StubAuthenticator; - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; - use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; - use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; - - class RegistrationController extends AbstractController - { - /** - * @Route("/register", name="app_register") - */ - public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder): Response - { - $user = new User(); - $form = $this->createForm(RegistrationFormType::class, $user); - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - // encode the plain password - $user->setPassword( - $passwordEncoder->encodePassword( - $user, - $form->get('plainPassword')->getData() - ) - ); - - $entityManager = $this->getDoctrine()->getManager(); - $entityManager->persist($user); - $entityManager->flush(); - - // do anything else you need here, like send an email - - return $this->redirectToRoute('app_homepage'); - } - - return $this->render('registration/register.html.twig', [ - 'registrationForm' => $form->createView(), - ]); - } - } - -register.html.twig -~~~~~~~~~~~~~~~~~~ - -The template renders the form: - -.. code-block:: html+twig - - {% extends 'base.html.twig' %} - - {% block title %}Register{% endblock %} - - {% block body %} -

Register

- - {{ form_start(registrationForm) }} - {{ form_row(registrationForm.email) }} - {{ form_row(registrationForm.plainPassword) }} - - - {{ form_end(registrationForm) }} - {% endblock %} - -Adding a "accept terms" Checkbox --------------------------------- - -Sometimes, you want a "Do you accept the terms and conditions" checkbox on your -registration form. The only trick is that you want to add this field to your form -without adding an unnecessary new ``termsAccepted`` property to your ``User`` entity -that you'll never need. - -To do this, add a ``termsAccepted`` field to your form, but set its -:ref:`mapped ` option to ``false``:: - - // src/Form/UserType.php - // ... - use Symfony\Component\Form\Extension\Core\Type\CheckboxType; - use Symfony\Component\Form\Extension\Core\Type\EmailType; - use Symfony\Component\Validator\Constraints\IsTrue; - - class UserType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('email', EmailType::class) - // ... - ->add('termsAccepted', CheckboxType::class, [ - 'mapped' => false, - 'constraints' => new IsTrue(), - ]) - ; - } - } - -The :ref:`constraints ` option is also used, which allows -us to add validation, even though there is no ``termsAccepted`` property on ``User``. - -Manually Authenticating after Success -------------------------------------- - -If you're using Guard authentication, you can :ref:`automatically authenticate ` -after registration is successful. The generator may have already configured your -controller to take advantage of this. - -.. _`CVE-2013-5750`: https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form +This article has been removed because it only explained things that are +already explained in other articles. Specifically, to implement a registration +form you must: + +#. :ref:`Define a class to represent users `; +#. :doc:`Create a form ` to ask for the registration information (you can + generate this with the ``make:registration-form`` command provided by the `MakerBundle`_); +#. Create :doc:`a controller ` to :ref:`process the form `; +#. :ref:`Protect some parts of your application ` so + only registered users can access to them. + +.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html diff --git a/email.rst b/email.rst index 7446ba3a3b2..82c07d06f36 100644 --- a/email.rst +++ b/email.rst @@ -1,8 +1,13 @@ .. index:: single: Emails -How to Send an Email -==================== +Swift Mailer +============ + +.. note:: + + In Symfony 4.3, the :doc:`Mailer ` component was introduced and can + be used instead of Swift Mailer. Symfony provides a mailer feature based on the popular `Swift Mailer`_ library via the `SwiftMailerBundle`_. This mailer supports sending messages with your @@ -12,7 +17,7 @@ own mail servers as well as using popular email providers like `Mandrill`_, Installation ------------ -In applications using :doc:`Symfony Flex `, run this command to +In applications using :ref:`Symfony Flex `, run this command to install the Swift Mailer based mailer before using it: .. code-block:: terminal @@ -40,7 +45,7 @@ environment variable in the ``.env`` file: MAILER_URL=null://localhost # use this to configure a traditional SMTP server - MAILER_URL=smtp://localhost:25?encryption=ssl&auth_mode=login&username=&password= + MAILER_URL=smtp://localhost:465?encryption=ssl&auth_mode=login&username=&password= .. caution:: @@ -77,6 +82,7 @@ sending an email is pretty straightforward:: // you can remove the following code if you don't define a text version for your emails ->addPart( $this->renderView( + // templates/emails/registration.txt.twig 'emails/registration.txt.twig', ['name' => $name] ), @@ -157,16 +163,499 @@ file. For example, for `Amazon SES`_ (Simple Email Service): Use the same technique for other mail services, as most of the time there is nothing more to it than configuring an SMTP endpoint. -Learn more ----------- +How to Work with Emails during Development +------------------------------------------ + +When developing an application which sends email, you will often +not want to actually send the email to the specified recipient during +development. If you are using the SwiftmailerBundle with Symfony, you +can achieve this through configuration settings without having to make +any changes to your application's code at all. There are two main choices +when it comes to handling email during development: (a) disabling the +sending of email altogether or (b) sending all email to a specific +address (with optional exceptions). + +Disabling Sending +~~~~~~~~~~~~~~~~~ + +You can disable sending email by setting the ``disable_delivery`` option to +``true``, which is the default value used by Symfony in the ``test`` environment +(email messages will continue to be sent in the other environments): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/test/swiftmailer.yaml + swiftmailer: + disable_delivery: true + + .. code-block:: xml + + + + + + + + + .. code-block:: php + + // config/packages/test/swiftmailer.php + $container->loadFromExtension('swiftmailer', [ + 'disable_delivery' => "true", + ]); + +.. _sending-to-a-specified-address: + +Sending to a Specified Address(es) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can also choose to have all email sent to a specific address or a list of addresses, instead +of the address actually specified when sending the message. This can be done +via the ``delivery_addresses`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/dev/swiftmailer.yaml + swiftmailer: + delivery_addresses: ['dev@example.com'] + + .. code-block:: xml + + + + + + + dev@example.com + + + + .. code-block:: php + + // config/packages/dev/swiftmailer.php + $container->loadFromExtension('swiftmailer', [ + 'delivery_addresses' => ['dev@example.com'], + ]); + +Now, suppose you're sending an email to ``recipient@example.com`` in a controller:: + + public function index($name, \Swift_Mailer $mailer) + { + $message = (new \Swift_Message('Hello Email')) + ->setFrom('send@example.com') + ->setTo('recipient@example.com') + ->setBody( + $this->renderView( + // templates/hello/email.txt.twig + 'hello/email.txt.twig', + ['name' => $name] + ) + ) + ; + $mailer->send($message); + + return $this->render(...); + } + +In the ``dev`` environment, the email will instead be sent to ``dev@example.com``. +Swift Mailer will add an extra header to the email, ``X-Swift-To``, containing +the replaced address, so you can still see who it would have been sent to. + +.. note:: + + In addition to the ``to`` addresses, this will also stop the email being + sent to any ``CC`` and ``BCC`` addresses set for it. Swift Mailer will add + additional headers to the email with the overridden addresses in them. + These are ``X-Swift-Cc`` and ``X-Swift-Bcc`` for the ``CC`` and ``BCC`` + addresses respectively. + +.. _sending-to-a-specified-address-but-with-exceptions: + +Sending to a Specified Address but with Exceptions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Suppose you want to have all email redirected to a specific address, +(like in the above scenario to ``dev@example.com``). But then you may want +email sent to some specific email addresses to go through after all, and +not be redirected (even if it is in the dev environment). This can be done +by adding the ``delivery_whitelist`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/dev/swiftmailer.yaml + swiftmailer: + delivery_addresses: ['dev@example.com'] + delivery_whitelist: + # all email addresses matching these regexes will be delivered + # like normal, as well as being sent to dev@example.com + - '/@specialdomain\.com$/' + - '/^admin@mydomain\.com$/' + + .. code-block:: xml + + + + + + + + /@specialdomain\.com$/ + /^admin@mydomain\.com$/ + dev@example.com + + + + .. code-block:: php + + // config/packages/dev/swiftmailer.php + $container->loadFromExtension('swiftmailer', [ + 'delivery_addresses' => ["dev@example.com"], + 'delivery_whitelist' => [ + // all email addresses matching these regexes will be delivered + // like normal, as well as being sent to dev@example.com + '/@specialdomain\.com$/', + '/^admin@mydomain\.com$/', + ], + ]); + +In the above example all email messages will be redirected to ``dev@example.com`` +and messages sent to the ``admin@mydomain.com`` address or to any email address +belonging to the domain ``specialdomain.com`` will also be delivered as normal. + +.. caution:: + + The ``delivery_whitelist`` option is ignored unless the ``delivery_addresses`` option is defined. + +Viewing from the Web Debug Toolbar +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can view any email sent during a single response when you are in the +``dev`` environment using the web debug toolbar. The email icon in the toolbar +will show how many emails were sent. If you click it, a report will open +showing the details of the sent emails. + +If you're sending an email and then immediately redirecting to another page, +the web debug toolbar will not display an email icon or a report on the next +page. + +Instead, you can set the ``intercept_redirects`` option to ``true`` in the +``dev`` environment, which will cause the redirect to stop and allow you to open +the report with details of the sent emails. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/dev/web_profiler.yaml + web_profiler: + intercept_redirects: true + + .. code-block:: xml + + + + + + + + + .. code-block:: php + + // config/packages/dev/web_profiler.php + $container->loadFromExtension('web_profiler', [ + 'intercept_redirects' => 'true', + ]); + +.. tip:: + + Alternatively, you can open the profiler after the redirect and search + by the submit URL used on the previous request (e.g. ``/contact/handle``). + The profiler's search feature allows you to load the profiler information + for any past requests. + +.. tip:: + + In addition to the features provided by Symfony, there are applications that + can help you test emails during application development, like `MailCatcher`_ + and `MailHog`_. + +How to Spool Emails +------------------- + +The default behavior of the Symfony mailer is to send the email messages +immediately. You may, however, want to avoid the performance hit of the +communication to the email server, which could cause the user to wait for the +next page to load while the email is sending. This can be avoided by choosing to +"spool" the emails instead of sending them directly. + +This makes the mailer to not attempt to send the email message but instead save +it somewhere such as a file. Another process can then read from the spool and +take care of sending the emails in the spool. Currently only spooling to file or +memory is supported. + +.. _email-spool-memory: + +Spool Using Memory +~~~~~~~~~~~~~~~~~~ + +When you use spooling to store the emails to memory, they will get sent right +before the kernel terminates. This means the email only gets sent if the whole +request got executed without any unhandled exception or any errors. To configure +this spool, use the following configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/swiftmailer.yaml + swiftmailer: + # ... + spool: { type: memory } + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/swiftmailer.php + $container->loadFromExtension('swiftmailer', [ + // ... + 'spool' => ['type' => 'memory'], + ]); + +.. _spool-using-a-file: + +Spool Using Files +~~~~~~~~~~~~~~~~~ + +When you use the filesystem for spooling, Symfony creates a folder in the given +path for each mail service (e.g. "default" for the default service). This folder +will contain files for each email in the spool. So make sure this directory is +writable by Symfony (or your webserver/php)! + +In order to use the spool with files, use the following configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/swiftmailer.yaml + swiftmailer: + # ... + spool: + type: file + path: /path/to/spooldir + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/swiftmailer.php + $container->loadFromExtension('swiftmailer', [ + // ... + + 'spool' => [ + 'type' => 'file', + 'path' => '/path/to/spooldir', + ], + ]); + +.. tip:: + + If you want to store the spool somewhere with your project directory, + remember that you can use the ``%kernel.project_dir%`` parameter to reference + the project's root: + + .. code-block:: yaml + + path: '%kernel.project_dir%/var/spool' + +Now, when your app sends an email, it will not actually be sent but instead +added to the spool. Sending the messages from the spool is done separately. +There is a console command to send the messages in the spool: + +.. code-block:: terminal + + $ APP_ENV=prod php bin/console swiftmailer:spool:send + +It has an option to limit the number of messages to be sent: + +.. code-block:: terminal + + $ APP_ENV=prod php bin/console swiftmailer:spool:send --message-limit=10 + +You can also set the time limit in seconds: + +.. code-block:: terminal + + $ APP_ENV=prod php bin/console swiftmailer:spool:send --time-limit=10 + +In practice you will not want to run this manually. Instead, the console command +should be triggered by a cron job or scheduled task and run at a regular +interval. + +.. caution:: + + When you create a message with SwiftMailer, it generates a ``Swift_Message`` + class. If the ``swiftmailer`` service is lazy loaded, it generates instead a + proxy class named ``Swift_Message_``. + + If you use the memory spool, this change is transparent and has no impact. + But when using the filesystem spool, the message class is serialized in + a file with the randomized class name. The problem is that this random + class name changes on every cache clear. + + So if you send a mail and then you clear the cache, on the next execution of + ``swiftmailer:spool:send`` an error will raise because the class + ``Swift_Message_`` doesn't exist (anymore). + + The solutions are either to use the memory spool or to load the + ``swiftmailer`` service without the ``lazy`` option (see :doc:`/service_container/lazy_services`). + +How to Test that an Email is Sent in a Functional Test +------------------------------------------------------ + +Sending emails with Symfony is pretty straightforward thanks to the +SwiftmailerBundle, which leverages the power of the `Swift Mailer`_ library. + +To functionally test that an email was sent, and even assert the email subject, +content or any other headers, you can use :doc:`the Symfony Profiler `. + +Start with a controller action that sends an email:: + + public function sendEmail($name, \Swift_Mailer $mailer) + { + $message = (new \Swift_Message('Hello Email')) + ->setFrom('send@example.com') + ->setTo('recipient@example.com') + ->setBody('You should see me from the profiler!') + ; + + $mailer->send($message); + + // ... + } + +In your functional test, use the ``swiftmailer`` collector on the profiler +to get information about the messages sent on the previous request:: + + // tests/Controller/MailControllerTest.php + namespace App\Tests\Controller; + + use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + + class MailControllerTest extends WebTestCase + { + public function testMailIsSentAndContentIsOk() + { + $client = static::createClient(); + + // enables the profiler for the next request (it does nothing if the profiler is not available) + $client->enableProfiler(); + + $crawler = $client->request('POST', '/path/to/above/action'); + + $mailCollector = $client->getProfile()->getCollector('swiftmailer'); + + // checks that an email was sent + $this->assertSame(1, $mailCollector->getMessageCount()); + + $collectedMessages = $mailCollector->getMessages(); + $message = $collectedMessages[0]; + + // Asserting email data + $this->assertInstanceOf('Swift_Message', $message); + $this->assertSame('Hello Email', $message->getSubject()); + $this->assertSame('send@example.com', key($message->getFrom())); + $this->assertSame('recipient@example.com', key($message->getTo())); + $this->assertSame( + 'You should see me from the profiler!', + $message->getBody() + ); + } + } + +Troubleshooting +~~~~~~~~~~~~~~~ + +Problem: The Collector Object Is ``null`` +......................................... + +The email collector is only available when the profiler is enabled and collects +information, as explained in :doc:`/testing/profiling`. -.. toctree:: - :maxdepth: 1 +Problem: The Collector Doesn't Contain the Email +................................................ - email/dev_environment - email/spool - email/testing +If a redirection is performed after sending the email (for example when you send +an email after a form is processed and before redirecting to another page), make +sure that the test client doesn't follow the redirects, as explained in +:doc:`/testing`. Otherwise, the collector will contain the information of the +redirected page and the email won't be accessible. +.. _`MailCatcher`: https://github.com/sj26/mailcatcher +.. _`MailHog`: https://github.com/mailhog/MailHog .. _`Swift Mailer`: http://swiftmailer.org/ .. _`SwiftMailerBundle`: https://github.com/symfony/swiftmailer-bundle .. _`Creating Messages`: https://swiftmailer.symfony.com/docs/messages.html diff --git a/email/dev_environment.rst b/email/dev_environment.rst deleted file mode 100644 index 2116618a7d0..00000000000 --- a/email/dev_environment.rst +++ /dev/null @@ -1,252 +0,0 @@ -.. index:: - single: Emails; In development - -How to Work with Emails during Development -========================================== - -When developing an application which sends email, you will often -not want to actually send the email to the specified recipient during -development. If you are using the SwiftmailerBundle with Symfony, you -can achieve this through configuration settings without having to make -any changes to your application's code at all. There are two main choices -when it comes to handling email during development: (a) disabling the -sending of email altogether or (b) sending all email to a specific -address (with optional exceptions). - -Disabling Sending ------------------ - -You can disable sending email by setting the ``disable_delivery`` option to -``true``, which is the default value used by Symfony in the ``test`` environment -(email messages will continue to be sent in the other environments): - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/test/swiftmailer.yaml - swiftmailer: - disable_delivery: true - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // config/packages/test/swiftmailer.php - $container->loadFromExtension('swiftmailer', [ - 'disable_delivery' => "true", - ]); - -.. _sending-to-a-specified-address: - -Sending to a Specified Address(es) ----------------------------------- - -You can also choose to have all email sent to a specific address or a list of addresses, instead -of the address actually specified when sending the message. This can be done -via the ``delivery_addresses`` option: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/dev/swiftmailer.yaml - swiftmailer: - delivery_addresses: ['dev@example.com'] - - .. code-block:: xml - - - - - - - dev@example.com - - - - .. code-block:: php - - // config/packages/dev/swiftmailer.php - $container->loadFromExtension('swiftmailer', [ - 'delivery_addresses' => ['dev@example.com'], - ]); - -Now, suppose you're sending an email to ``recipient@example.com`` in a controller:: - - public function index($name, \Swift_Mailer $mailer) - { - $message = (new \Swift_Message('Hello Email')) - ->setFrom('send@example.com') - ->setTo('recipient@example.com') - ->setBody( - $this->renderView( - 'HelloBundle:Hello:email.txt.twig', - ['name' => $name] - ) - ) - ; - $mailer->send($message); - - return $this->render(...); - } - -In the ``dev`` environment, the email will instead be sent to ``dev@example.com``. -Swift Mailer will add an extra header to the email, ``X-Swift-To``, containing -the replaced address, so you can still see who it would have been sent to. - -.. note:: - - In addition to the ``to`` addresses, this will also stop the email being - sent to any ``CC`` and ``BCC`` addresses set for it. Swift Mailer will add - additional headers to the email with the overridden addresses in them. - These are ``X-Swift-Cc`` and ``X-Swift-Bcc`` for the ``CC`` and ``BCC`` - addresses respectively. - -.. _sending-to-a-specified-address-but-with-exceptions: - -Sending to a Specified Address but with Exceptions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Suppose you want to have all email redirected to a specific address, -(like in the above scenario to ``dev@example.com``). But then you may want -email sent to some specific email addresses to go through after all, and -not be redirected (even if it is in the dev environment). This can be done -by adding the ``delivery_whitelist`` option: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/dev/swiftmailer.yaml - swiftmailer: - delivery_addresses: ['dev@example.com'] - delivery_whitelist: - # all email addresses matching these regexes will be delivered - # like normal, as well as being sent to dev@example.com - - '/@specialdomain\.com$/' - - '/^admin@mydomain\.com$/' - - .. code-block:: xml - - - - - - - - /@specialdomain\.com$/ - /^admin@mydomain\.com$/ - dev@example.com - - - - .. code-block:: php - - // config/packages/dev/swiftmailer.php - $container->loadFromExtension('swiftmailer', [ - 'delivery_addresses' => ["dev@example.com"], - 'delivery_whitelist' => [ - // all email addresses matching these regexes will be delivered - // like normal, as well as being sent to dev@example.com - '/@specialdomain\.com$/', - '/^admin@mydomain\.com$/', - ], - ]); - -In the above example all email messages will be redirected to ``dev@example.com`` -and messages sent to the ``admin@mydomain.com`` address or to any email address -belonging to the domain ``specialdomain.com`` will also be delivered as normal. - -.. caution:: - - The ``delivery_whitelist`` option is ignored unless the ``delivery_addresses`` option is defined. - -Viewing from the Web Debug Toolbar ----------------------------------- - -You can view any email sent during a single response when you are in the -``dev`` environment using the web debug toolbar. The email icon in the toolbar -will show how many emails were sent. If you click it, a report will open -showing the details of the sent emails. - -If you're sending an email and then immediately redirecting to another page, -the web debug toolbar will not display an email icon or a report on the next -page. - -Instead, you can set the ``intercept_redirects`` option to ``true`` in the -``dev`` environment, which will cause the redirect to stop and allow you to open -the report with details of the sent emails. - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/dev/web_profiler.yaml - web_profiler: - intercept_redirects: true - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // config/packages/dev/web_profiler.php - $container->loadFromExtension('web_profiler', [ - 'intercept_redirects' => 'true', - ]); - -.. tip:: - - Alternatively, you can open the profiler after the redirect and search - by the submit URL used on the previous request (e.g. ``/contact/handle``). - The profiler's search feature allows you to load the profiler information - for any past requests. - -.. tip:: - - In addition to the features provided by Symfony, there are applications that - can help you test emails during application development, like `MailCatcher`_ - and `MailHog`_. - -.. _`MailCatcher`: https://github.com/sj26/mailcatcher -.. _`MailHog`: https://github.com/mailhog/MailHog diff --git a/email/spool.rst b/email/spool.rst deleted file mode 100644 index 47d96c00cf4..00000000000 --- a/email/spool.rst +++ /dev/null @@ -1,164 +0,0 @@ -.. index:: - single: Emails; Spooling - -How to Spool Emails -=================== - -The default behavior of the Symfony mailer is to send the email messages -immediately. You may, however, want to avoid the performance hit of the -communication to the email server, which could cause the user to wait for the -next page to load while the email is sending. This can be avoided by choosing to -"spool" the emails instead of sending them directly. - -This makes the mailer to not attempt to send the email message but instead save -it somewhere such as a file. Another process can then read from the spool and -take care of sending the emails in the spool. Currently only spooling to file or -memory is supported. - -.. _email-spool-memory: - -Spool Using Memory ------------------- - -When you use spooling to store the emails to memory, they will get sent right -before the kernel terminates. This means the email only gets sent if the whole -request got executed without any unhandled exception or any errors. To configure -this spool, use the following configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/swiftmailer.yaml - swiftmailer: - # ... - spool: { type: memory } - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // config/packages/swiftmailer.php - $container->loadFromExtension('swiftmailer', [ - // ... - 'spool' => ['type' => 'memory'], - ]); - -.. _spool-using-a-file: - -Spool Using Files ------------------- - -When you use the filesystem for spooling, Symfony creates a folder in the given -path for each mail service (e.g. "default" for the default service). This folder -will contain files for each email in the spool. So make sure this directory is -writable by Symfony (or your webserver/php)! - -In order to use the spool with files, use the following configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/swiftmailer.yaml - swiftmailer: - # ... - spool: - type: file - path: /path/to/spooldir - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // config/packages/swiftmailer.php - $container->loadFromExtension('swiftmailer', [ - // ... - - 'spool' => [ - 'type' => 'file', - 'path' => '/path/to/spooldir', - ], - ]); - -.. tip:: - - If you want to store the spool somewhere with your project directory, - remember that you can use the ``%kernel.project_dir%`` parameter to reference - the project's root: - - .. code-block:: yaml - - path: '%kernel.project_dir%/var/spool' - -Now, when your app sends an email, it will not actually be sent but instead -added to the spool. Sending the messages from the spool is done separately. -There is a console command to send the messages in the spool: - -.. code-block:: terminal - - $ APP_ENV=prod php bin/console swiftmailer:spool:send - -It has an option to limit the number of messages to be sent: - -.. code-block:: terminal - - $ APP_ENV=prod php bin/console swiftmailer:spool:send --message-limit=10 - -You can also set the time limit in seconds: - -.. code-block:: terminal - - $ APP_ENV=prod php bin/console swiftmailer:spool:send --time-limit=10 - -In practice you will not want to run this manually. Instead, the console command -should be triggered by a cron job or scheduled task and run at a regular -interval. - -.. caution:: - - When you create a message with SwiftMailer, it generates a ``Swift_Message`` - class. If the ``swiftmailer`` service is lazy loaded, it generates instead a - proxy class named ``Swift_Message_``. - - If you use the memory spool, this change is transparent and has no impact. - But when using the filesystem spool, the message class is serialized in - a file with the randomized class name. The problem is that this random - class name changes on every cache clear. So if you send a mail and then you - clear the cache, the message will not be unserializable. - - On the next execution of ``swiftmailer:spool:send`` an error will raise because - the class ``Swift_Message_`` doesn't exist (anymore). - - The solutions are either to use the memory spool or to load the - ``swiftmailer`` service without the ``lazy`` option (see :doc:`/service_container/lazy_services`). diff --git a/email/testing.rst b/email/testing.rst deleted file mode 100644 index 8ca65047d9e..00000000000 --- a/email/testing.rst +++ /dev/null @@ -1,85 +0,0 @@ -.. index:: - single: Emails; Testing - -How to Test that an Email is Sent in a Functional Test -====================================================== - -Sending emails with Symfony is pretty straightforward thanks to the -SwiftmailerBundle, which leverages the power of the `Swift Mailer`_ library. - -To functionally test that an email was sent, and even assert the email subject, -content or any other headers, you can use :doc:`the Symfony Profiler `. - -Start with a controller action that sends an email:: - - public function sendEmail($name, \Swift_Mailer $mailer) - { - $message = (new \Swift_Message('Hello Email')) - ->setFrom('send@example.com') - ->setTo('recipient@example.com') - ->setBody('You should see me from the profiler!') - ; - - $mailer->send($message); - - // ... - } - -In your functional test, use the ``swiftmailer`` collector on the profiler -to get information about the messages sent on the previous request:: - - // tests/Controller/MailControllerTest.php - namespace App\Tests\Controller; - - use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - - class MailControllerTest extends WebTestCase - { - public function testMailIsSentAndContentIsOk() - { - $client = static::createClient(); - - // enables the profiler for the next request (it does nothing if the profiler is not available) - $client->enableProfiler(); - - $crawler = $client->request('POST', '/path/to/above/action'); - - $mailCollector = $client->getProfile()->getCollector('swiftmailer'); - - // checks that an email was sent - $this->assertSame(1, $mailCollector->getMessageCount()); - - $collectedMessages = $mailCollector->getMessages(); - $message = $collectedMessages[0]; - - // Asserting email data - $this->assertInstanceOf('Swift_Message', $message); - $this->assertSame('Hello Email', $message->getSubject()); - $this->assertSame('send@example.com', key($message->getFrom())); - $this->assertSame('recipient@example.com', key($message->getTo())); - $this->assertSame( - 'You should see me from the profiler!', - $message->getBody() - ); - } - } - -Troubleshooting ---------------- - -Problem: The Collector Object Is ``null`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The email collector is only available when the profiler is enabled and collects -information, as explained in :doc:`/testing/profiling`. - -Problem: The Collector Doesn't Contain the Email -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If a redirection is performed after sending the email (for example when you send -an email after a form is processed and before redirecting to another page), make -sure that the test client doesn't follow the redirects, as explained in -:doc:`/testing`. Otherwise, the collector will contain the information of the -redirected page and the email won't be accessible. - -.. _`Swift Mailer`: http://swiftmailer.org/ diff --git a/event_dispatcher.rst b/event_dispatcher.rst index 653feb11f4b..c1c7d1c6f95 100644 --- a/event_dispatcher.rst +++ b/event_dispatcher.rst @@ -27,12 +27,12 @@ The most common way to listen to an event is to register an **event listener**:: namespace App\EventListener; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; + use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; class ExceptionListener { - public function onKernelException(GetResponseForExceptionEvent $event) + public function onKernelException(ExceptionEvent $event) { // You get the exception object from the received event $exception = $event->getException(); @@ -63,10 +63,16 @@ The most common way to listen to an event is to register an **event listener**:: .. tip:: Each event receives a slightly different type of ``$event`` object. For - the ``kernel.exception`` event, it is :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent`. + the ``kernel.exception`` event, it is :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent`. Check out the :doc:`Symfony events reference ` to see what type of object each event provides. +.. versionadded:: 4.3 + + The :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` class was + introduced in Symfony 4.3. In previous versions it was called + ``Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent``. + Now that the class is created, you need to register it as a service and notify Symfony that it is a "listener" on the ``kernel.exception`` event by using a special "tag": @@ -151,7 +157,7 @@ listen to the same ``kernel.exception`` event:: namespace App\EventSubscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; + use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\KernelEvents; class ExceptionSubscriber implements EventSubscriberInterface @@ -168,17 +174,17 @@ listen to the same ``kernel.exception`` event:: ]; } - public function processException(GetResponseForExceptionEvent $event) + public function processException(ExceptionEvent $event) { // ... } - public function logException(GetResponseForExceptionEvent $event) + public function logException(ExceptionEvent $event) { // ... } - public function notifyException(GetResponseForExceptionEvent $event) + public function notifyException(ExceptionEvent $event) { // ... } @@ -200,18 +206,18 @@ Request Events, Checking Types ------------------------------ A single page can make several requests (one master request, and then multiple -sub-requests - typically by :doc:`/templating/embedding_controllers`). For the core -Symfony events, you might need to check to see if the event is for a "master" request -or a "sub request":: +sub-requests - typically when :ref:`embedding controllers in templates `). +For the core Symfony events, you might need to check to see if the event is for +a "master" request or a "sub request":: // src/EventListener/RequestListener.php namespace App\EventListener; - use Symfony\Component\HttpKernel\Event\GetResponseEvent; + use Symfony\Component\HttpKernel\Event\RequestEvent; class RequestListener { - public function onKernelRequest(GetResponseEvent $event) + public function onKernelRequest(RequestEvent $event) { if (!$event->isMasterRequest()) { // don't do anything if it's not the master request diff --git a/event_dispatcher/before_after_filters.rst b/event_dispatcher/before_after_filters.rst index 99ca428f5ee..03b47565377 100644 --- a/event_dispatcher/before_after_filters.rst +++ b/event_dispatcher/before_after_filters.rst @@ -115,7 +115,7 @@ event subscribers, you can learn more about them at :doc:`/event_dispatcher`:: use App\Controller\TokenAuthenticatedController; use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\FilterControllerEvent; + use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\KernelEvents; @@ -128,20 +128,17 @@ event subscribers, you can learn more about them at :doc:`/event_dispatcher`:: $this->tokens = $tokens; } - public function onKernelController(FilterControllerEvent $event) + public function onKernelController(ControllerEvent $event) { $controller = $event->getController(); - /* - * $controller passed can be either a class or a Closure. - * This is not usual in Symfony but it may happen. - * If it is a class, it comes in array format - */ - if (!is_array($controller)) { - return; + // when a controller class defines multiple action methods, the controller + // is returned as [$controllerInstance, 'methodName'] + if (is_array($controller)) { + $controller = $controller[0]; } - if ($controller[0] instanceof TokenAuthenticatedController) { + if ($controller instanceof TokenAuthenticatedController) { $token = $event->getRequest()->query->get('token'); if (!in_array($token, $this->tokens)) { throw new AccessDeniedHttpException('This action needs a valid token!'); @@ -188,7 +185,7 @@ For example, take the ``TokenSubscriber`` from the previous example and first record the authentication token inside the request attributes. This will serve as a basic flag that this request underwent token authentication:: - public function onKernelController(FilterControllerEvent $event) + public function onKernelController(ControllerEvent $event) { // ... @@ -208,9 +205,9 @@ This will look for the ``auth_token`` flag on the request object and set a custo header on the response if it's found:: // add the new use statement at the top of your file - use Symfony\Component\HttpKernel\Event\FilterResponseEvent; + use Symfony\Component\HttpKernel\Event\ResponseEvent; - public function onKernelResponse(FilterResponseEvent $event) + public function onKernelResponse(ResponseEvent $event) { // check to see if onKernelController marked this as a token "auth'ed" request if (!$token = $event->getRequest()->attributes->get('auth_token')) { diff --git a/event_dispatcher/method_behavior.rst b/event_dispatcher/method_behavior.rst index aaa2abfd746..7d93d074353 100644 --- a/event_dispatcher/method_behavior.rst +++ b/event_dispatcher/method_behavior.rst @@ -19,7 +19,7 @@ method:: { // dispatch an event before the method $event = new BeforeSendMailEvent($subject, $message); - $this->dispatcher->dispatch('mailer.pre_send', $event); + $this->dispatcher->dispatch($event, 'mailer.pre_send'); // get $foo and $bar from the event, they may have been modified $subject = $event->getSubject(); @@ -30,7 +30,7 @@ method:: // do something after the method $event = new AfterSendMailEvent($returnValue); - $this->dispatcher->dispatch('mailer.post_send', $event); + $this->dispatcher->dispatch($event, 'mailer.post_send'); return $event->getReturnValue(); } diff --git a/form/action_method.rst b/form/action_method.rst deleted file mode 100644 index f0b40db24e7..00000000000 --- a/form/action_method.rst +++ /dev/null @@ -1,132 +0,0 @@ -.. index:: - single: Forms; Changing the action and method - -How to Change the Action and Method of a Form -============================================= - -By default, a form will be submitted via an HTTP POST request to the same -URL under which the form was rendered. Sometimes you want to change these -parameters. You can do so in a few different ways. - -If you use the :class:`Symfony\\Component\\Form\\FormBuilder` to build your -form, you can use ``setAction()`` and ``setMethod()``: - -.. configuration-block:: - - .. code-block:: php-symfony - - // src/Controller/DefaultController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Form\Extension\Core\Type\DateType; - use Symfony\Component\Form\Extension\Core\Type\SubmitType; - use Symfony\Component\Form\Extension\Core\Type\TextType; - - class DefaultController extends AbstractController - { - public function new() - { - // ... - - $form = $this->createFormBuilder($task) - ->setAction($this->generateUrl('target_route')) - ->setMethod('GET') - ->add('task', TextType::class) - ->add('dueDate', DateType::class) - ->add('save', SubmitType::class) - ->getForm(); - - // ... - } - } - - .. code-block:: php-standalone - - use Symfony\Component\Form\Forms; - use Symfony\Component\Form\Extension\Core\Type\DateType; - use Symfony\Component\Form\Extension\Core\Type\FormType; - use Symfony\Component\Form\Extension\Core\Type\SubmitType; - use Symfony\Component\Form\Extension\Core\Type\TextType; - - // ... - - $formFactoryBuilder = Forms::createFormFactoryBuilder(); - - // Form factory builder configuration ... - - $formFactory = $formFactoryBuilder->getFormFactory(); - - $form = $formFactory->createBuilder(FormType::class, $task) - ->setAction('...') - ->setMethod('GET') - ->add('task', TextType::class) - ->add('dueDate', DateType::class) - ->add('save', SubmitType::class) - ->getForm(); - -.. note:: - - This example assumes that you've created a route called ``target_route`` - that points to the controller that processes the form. - -When using a form type class, you can pass the action and method as form -options: - -.. configuration-block:: - - .. code-block:: php-symfony - - // src/Controller/DefaultController.php - namespace App\Controller; - - use App\Form\TaskType; - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - - class DefaultController extends AbstractController - { - public function new() - { - // ... - - $form = $this->createForm(TaskType::class, $task, [ - 'action' => $this->generateUrl('target_route'), - 'method' => 'GET', - ]); - - // ... - } - } - - .. code-block:: php-standalone - - use App\Form\TaskType; - use Symfony\Component\Form\Forms; - - $formFactoryBuilder = Forms::createFormFactoryBuilder(); - - // Form factory builder configuration ... - - $formFactory = $formFactoryBuilder->getFormFactory(); - - $form = $formFactory->create(TaskType::class, $task, [ - 'action' => '...', - 'method' => 'GET', - ]); - -Finally, you can override the action and method in the template by passing them -to the ``form()`` or the ``form_start()`` helper functions: - -.. code-block:: twig - - {# templates/default/new.html.twig #} - {{ form_start(form, {'action': path('target_route'), 'method': 'GET'}) }} - -.. note:: - - If the form's method is not GET or POST, but PUT, PATCH or DELETE, Symfony - will insert a hidden field with the name ``_method`` that stores this method. - The form will be submitted in a normal POST request, but Symfony's router - is capable of detecting the ``_method`` parameter and will interpret it as - a PUT, PATCH or DELETE request. See the :ref:`configuration-framework-http_method_override` - option. diff --git a/form/bootstrap4.rst b/form/bootstrap4.rst index 3def8baae47..330ed2a165e 100644 --- a/form/bootstrap4.rst +++ b/form/bootstrap4.rst @@ -111,6 +111,5 @@ Form errors are rendered **inside** the ``
`` element and it's the theme used by default in Symfony applications unless you configure @@ -182,6 +183,8 @@ of form themes: {# ... #} +.. _create-your-own-form-theme: + Creating your Own Form Theme ---------------------------- @@ -275,6 +278,32 @@ form. You can also define this value explicitly with the ``block_name`` option:: In this example, the fragment name will be ``_product_custom_name_widget`` instead of the default ``_product_name_widget``. +.. _form-fragment-custom-naming: + +Custom Fragment Naming for Individual Fields +............................................ + +The ``block_prefix`` option allows form fields to define their own custom +fragment name. This is mostly useful to customize some instances of the same +field without having to :doc:`create a custom form type `:: + + use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\Form\FormBuilderInterface; + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->add('name', TextType::class, [ + 'block_prefix' => 'wrapped_text', + ]); + } + +.. versionadded:: 4.3 + + The ``block_prefix`` option was introduced in Symfony 4.3. + +Now you can use ``wrapped_text_row``, ``wrapped_text_widget``, etc. as the block +names. + .. _form-custom-prototype: Fragment Naming for Collections @@ -565,9 +594,6 @@ is a collection of fields (e.g. a whole form), and not just an individual field: {% endif %} {% endblock form_errors %} -.. _`form_div_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig -.. _`Twig Bridge`: https://github.com/symfony/symfony/tree/master/src/Symfony/Bridge/Twig -.. _`view on GitHub`: https://github.com/symfony/symfony/tree/master/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form .. _`form_div_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig .. _`form_table_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig .. _`bootstrap_3_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig diff --git a/form/multiple_buttons.rst b/form/multiple_buttons.rst index 1965e941f62..74d430b4bb8 100644 --- a/form/multiple_buttons.rst +++ b/form/multiple_buttons.rst @@ -35,3 +35,9 @@ Or you can get the button's name by using the if ($form->getClickedButton() && 'saveAndAdd' === $form->getClickedButton()->getName()) { // ... } + + // when using nested forms, two or more buttons can have the same name; + // in those cases, compare the button objects instead of the button names + if ($form->getClickedButton() === $form->get('saveAndAdd')){ + // ... + } diff --git a/form/unit_testing.rst b/form/unit_testing.rst index 5a94152892b..470de3455dd 100644 --- a/form/unit_testing.rst +++ b/form/unit_testing.rst @@ -4,6 +4,13 @@ How to Unit Test your Forms =========================== +.. caution:: + + This article is intended for developers who create + :doc:`custom form types `. If you are using + the :doc:`built-in Symfony form types ` or the form + types provided by third-party bundles, you don't need to unit test them. + The Form component consists of 3 core objects: a form type (implementing :class:`Symfony\\Component\\Form\\FormTypeInterface`), the :class:`Symfony\\Component\\Form\\Form` and the diff --git a/form/validation_groups.rst b/form/validation_groups.rst index 2b8d87e43e5..09b420bceaf 100644 --- a/form/validation_groups.rst +++ b/form/validation_groups.rst @@ -8,21 +8,22 @@ Validation Groups ----------------- If your object takes advantage of :doc:`validation groups `, -you'll need to specify which validation group(s) your form should use:: +you'll need to specify which validation group(s) your form should use. Pass +this as an option when :ref:`creating forms in controllers `:: $form = $this->createFormBuilder($user, [ 'validation_groups' => ['registration'], ])->add(...); -If you're creating :ref:`form classes ` (a good -practice), then you'll need to add the following to the ``configureOptions()`` -method:: +When :ref:`creating forms in classes `, add the +following to the ``configureOptions()`` method:: use Symfony\Component\OptionsResolver\OptionsResolver; public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ + // ... 'validation_groups' => ['registration'], ]); } diff --git a/forms.rst b/forms.rst index 4fccc340c43..b902e53ba3e 100644 --- a/forms.rst +++ b/forms.rst @@ -9,37 +9,37 @@ Forms Do you prefer video tutorials? Check out the `Symfony Forms screencast series`_. -Dealing with HTML forms is one of the most common - and challenging - tasks for -a web developer. Symfony integrates a Form component that helps you dealing -with forms. In this article, you'll build a complex form from the ground up, -learning the most important features of the form library along the way. +Creating and processing HTML forms is hard and repetitive. You need to deal with +rendering HTML form fields, validating submitted data, mapping the form data +into objects and a lot more. Symfony includes a powerful form feature that +provides all these features and many more for truly complex scenarios. Installation ------------ -In applications using :doc:`Symfony Flex `, run this command to +In applications using :ref:`Symfony Flex `, run this command to install the form feature before using it: .. code-block:: terminal $ composer require symfony/form -.. note:: +Usage +----- - The Symfony Form component is a standalone library that can be used outside - of Symfony projects. For more information, see the - :doc:`Form component documentation ` on GitHub. +The recommended workflow when working with Symfony forms is the following: -.. index:: - single: Forms; Create a simple form +#. **Build the form** in a Symfony controller or using a dedicated form class; +#. **Render the form** in a template so the user can edit and submit it; +#. **Process the form** to validate the submitted data, transform it into PHP + data and do something with it (e.g. persist it in a database). -Creating a Simple Form ----------------------- +Each of these steps is explained in detail in the next sections. To make +examples easier to follow, all of them assume that you're building a simple Todo +list application that displays "tasks". -Suppose you're building a simple todo list application that will need to -display "tasks". Because your users will need to edit and create tasks, you're -going to need to build a form. But before you begin, first focus on the generic -``Task`` class that represents and stores the data for a single task:: +Users create and edit tasks using Symfony forms. Each task is an instance of the +following ``Task`` class:: // src/Entity/Task.php namespace App\Entity; @@ -73,20 +73,45 @@ going to need to build a form. But before you begin, first focus on the generic This class is a "plain-old-PHP-object" because, so far, it has nothing to do with Symfony or any other library. It's a normal PHP object that directly solves a problem inside *your* application (i.e. the need to represent a task in your -application). By the end of this article, you'll be able to submit data to a -``Task`` instance (via an HTML form), validate its data and persist it to the -database. +application). But you can also edit :doc:`Doctrine entities ` in the +same way. -.. index:: - single: Forms; Create a form in a controller +.. _form-types: -Building the Form -~~~~~~~~~~~~~~~~~ +Form Types +~~~~~~~~~~ + +Before creating your first Symfony form, it's important to understand the +concept of "form type". In other projects, it's common to differentiate between +"forms" and "form fields". In Symfony, all of them are "form types": + +* a single ```` form field is a "form type" (e.g. ``TextType``); +* a group of several HTML fields used to input a postal address is a "form type" + (e.g. ``PostalAddressType``); +* an entire ``
`` with multiple fields to edit a user profile is a + "form type" (e.g. ``UserProfileType``). + +This may be confusing at first, but it will feel natural to you soon enough. +Besides, it simplifies code and makes "composing" and "embedding" form fields +much easier to implement. + +There are tens of :doc:`form types provided by Symfony ` +and you can also :doc:`create your own form types
`. + +Building Forms +-------------- + +Symfony provides a "form builder" object which allows you to describe the form +fields using a fluent interface. Later, this builder creates the actual form +object used to render and process contents. -Now that you've created a ``Task`` class, the next step is to create and -render the actual HTML form. In Symfony, this is done by building a form -object and then rendering it in a template. For now, this can all be done -from inside a controller:: +.. _creating-forms-in-controllers: + +Creating Forms in Controllers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your controller extends from the :ref:`AbstractController `, +use the ``createFormBuilder()`` helper:: // src/Controller/TaskController.php namespace App\Controller; @@ -102,7 +127,7 @@ from inside a controller:: { public function new(Request $request) { - // creates a task and gives it some dummy data for this example + // creates a task object and initializes some data for this example $task = new Task(); $task->setTask('Write a blog post'); $task->setDueDate(new \DateTime('tomorrow')); @@ -113,119 +138,249 @@ from inside a controller:: ->add('save', SubmitType::class, ['label' => 'Create Task']) ->getForm(); - return $this->render('task/new.html.twig', [ - 'form' => $form->createView(), - ]); + // ... + } + } + +If your controller does not extend from ``AbstractController``, you'll need to +:ref:`fetch services in your controller ` and +use the ``createBuilder()`` method of the ``form.factory`` service. + +In this example, you've added two fields to your form - ``task`` and ``dueDate`` +- corresponding to the ``task`` and ``dueDate`` properties of the ``Task`` +class. You've also assigned each a :ref:`form type ` (e.g. ``TextType`` +and ``DateType``), represented by its fully qualified class name. Finally, you +added a submit button with a custom label for submitting the form to the server. + +.. _creating-forms-in-classes: + +Creating Form Classes +~~~~~~~~~~~~~~~~~~~~~ + +Symfony recommends to :doc:`create thin controllers `. +That's why it's better to move complex forms to dedicated classes instead of +defining them in controller actions. Besides, forms defined in classes can be +reused in multiple actions and services. + +Form classes are :ref:`form types ` that implement +:class:`Symfony\\Component\\Form\\FormTypeInterface`. However, it's better to +extend from :class:`Symfony\\Component\\Form\\AbstractType`, which already +implements the interface and provides some utilities:: + + // src/Form/Type/TaskType.php + namespace App\Form\Type; + + use Symfony\Component\Form\AbstractType; + use Symfony\Component\Form\Extension\Core\Type\DateType; + use Symfony\Component\Form\Extension\Core\Type\SubmitType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\Form\FormBuilderInterface; + + class TaskType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('task', TextType::class) + ->add('dueDate', DateType::class) + ->add('save', SubmitType::class) + ; } } .. tip:: - This example shows you how to build your form directly in the controller. - Later, in the ":ref:`form-creating-form-classes`" section, you'll learn - how to build your form in a standalone class, which is recommended as - your form becomes reusable. + Install the `MakerBundle`_ in your project to generate form classes using + the ``make:form`` and ``make:registration-form`` commands. + +The form class contains all the directions needed to create the task form. In +controllers extending from the :ref:`AbstractController `, +use the ``createForm()`` helper (otherwise, use the ``create()`` method of the +``form.factory`` service):: + + // src/Controller/TaskController.php + use App\Form\Type\TaskType; + // ... + + class TaskController extends AbstractController + { + public function new() + { + // creates a task object and initializes some data for this example + $task = new Task(); + $task->setTask('Write a blog post'); + $task->setDueDate(new \DateTime('tomorrow')); -Creating a form requires relatively little code because Symfony form objects -are built with a "form builder". The form builder's purpose is to allow you -to write simple form "recipes" and have it do all the heavy-lifting of actually -building the form. + $form = $this->createForm(TaskType::class, $task); -In this example, you've added two fields to your form - ``task`` and ``dueDate`` - -corresponding to the ``task`` and ``dueDate`` properties of the ``Task`` class. -You've also assigned each a "type" (e.g. ``TextType`` and ``DateType``), -represented by its fully qualified class name. Among other things, it determines -which HTML form tag(s) is rendered for that field. + // ... + } + } -Finally, you added a submit button with a custom label for submitting the form to -the server. +.. _form-data-class: -Symfony comes with many built-in types that will be discussed shortly -(see :ref:`forms-type-reference`). +Every form needs to know the name of the class that holds the underlying data +(e.g. ``App\Entity\Task``). Usually, this is just guessed based off of the +object passed to the second argument to ``createForm()`` (i.e. ``$task``). +Later, when you begin :doc:`embedding forms `, this will no +longer be sufficient. -.. index:: - single: Forms; Basic template rendering +So, while not always necessary, it's generally a good idea to explicitly specify +the ``data_class`` option by adding the following to your form type class:: -Rendering the Form -~~~~~~~~~~~~~~~~~~ + // src/Form/Type/TaskType.php + use App\Entity\Task; + use Symfony\Component\OptionsResolver\OptionsResolver; + // ... + + class TaskType extends AbstractType + { + // ... + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Task::class, + ]); + } + } + +Rendering Forms +--------------- -Now that the form has been created, the next step is to render it. This is -done by passing a special form "view" object to your template (notice the -``$form->createView()`` in the controller above) and using a set of -:ref:`form helper functions `: +Now that the form has been created, the next step is to render it. Instead of +passing the entire form object to the template, use the ``createView()`` method +to build another object with the visual representation of the form:: + + // src/Controller/TaskController.php + namespace App\Controller; + + use App\Entity\Task; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Form\Extension\Core\Type\DateType; + use Symfony\Component\Form\Extension\Core\Type\SubmitType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\HttpFoundation\Request; + + class TaskController extends AbstractController + { + public function new(Request $request) + { + $task = new Task(); + // ... + + $form = $this->createForm(TaskType::class, $task); + + return $this->render('task/new.html.twig', [ + 'form' => $form->createView(), + ]); + } + } + +Then, use some :ref:`form helper functions ` to +render the form contents: .. code-block:: twig {# templates/task/new.html.twig #} {{ form(form) }} -.. image:: /_images/form/simple-form.png - :align: center - That's it! The :ref:`form() function ` renders all fields *and* the ``
`` start and end tags. By default, the form method is -``POST`` and the target URL is the same that displayed the form. - -As short as this is, it's not very flexible. Usually, you'll need more control -about how the entire form or some of its fields look. Symfony provides several -ways of doing that: - -* If your app uses a CSS framework such as Bootstrap or Foundation, use any of - the :ref:`built-in form themes ` to make all your forms - match the style of the rest of your app; -* If you want to customize only a few fields or a few forms of your app, read - the :doc:`How to Customize Form Rendering ` article; -* If you want to customize all your forms in the same way, create a - :doc:`Symfony form theme ` (based on any of the built-in - themes or from scratch). - -Before moving on, notice how the rendered ``task`` input field has the value -of the ``task`` property from the ``$task`` object (i.e. "Write a blog post"). -This is the first job of a form: to take data from an object and translate -it into a format that's suitable for being rendered in an HTML form. +``POST`` and the target URL is the same that displayed the form, but +:ref:`you can change both `. + +Notice how the rendered ``task`` input field has the value of the ``task`` +property from the ``$task`` object (i.e. "Write a blog post"). This is the first +job of a form: to take data from an object and translate it into a format that's +suitable for being rendered in an HTML form. .. tip:: The form system is smart enough to access the value of the protected ``task`` property via the ``getTask()`` and ``setTask()`` methods on the ``Task`` class. Unless a property is public, it *must* have a "getter" and - "setter" method so that the Form component can get and put data onto the - property. For a boolean property, you can use an "isser" or "hasser" method - (e.g. ``isPublished()`` or ``hasReminder()``) instead of a getter (e.g. + "setter" method so that Symfony can get and put data onto the property. For + a boolean property, you can use an "isser" or "hasser" method (e.g. + ``isPublished()`` or ``hasReminder()``) instead of a getter (e.g. ``getPublished()`` or ``getReminder()``). -.. index:: - single: Forms; Handling form submissions +As short as this rendering is, it's not very flexible. Usually, you'll need more +control about how the entire form or some of its fields look. For example, thanks +to the :doc:`Bootstrap 4 integration with Symfony forms ` you +can set this option to generate forms compatible with the Bootstrap 4 CSS framework: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/twig.yaml + twig: + form_themes: ['bootstrap_4_layout.html.twig'] + + .. code-block:: xml + + + + + + + bootstrap_4_layout.html.twig + + + + + .. code-block:: php + + // config/packages/twig.php + $container->loadFromExtension('twig', [ + 'form_themes' => [ + 'bootstrap_4_layout.html.twig', + ], + + // ... + ]); + +The :ref:`built-in Symfony form themes ` include +Bootstrap 3 and 4 and Foundation 5. You can also +:ref:`create your own Symfony form theme `. + +In addition to form themes, Symfony allows you to +:doc:`customize the way fields are rendered ` with +multiple functions to render each field part separately (widgets, labels, +errors, help messages, etc.) -.. _form-handling-form-submissions: +.. _processing-forms: -Handling Form Submissions -~~~~~~~~~~~~~~~~~~~~~~~~~ +Processing Forms +---------------- -By default, the form will submit a POST request back to the same controller that -renders it. +The :ref:`recommended way of processing forms ` is to +use a single action for both rendering the form and handling the form submit. +You can use separate actions, but using one action simplifies everything while +keeping the code concise and maintainable. -Here, the second job of a form is to translate user-submitted data back to the -properties of an object. To make this happen, the submitted data from the -user must be written into the Form object. Add the following functionality to -your controller:: +Processing a form means to translate user-submitted data back to the properties +of an object. To make this happen, the submitted data from the user must be +written into the form object:: // ... use Symfony\Component\HttpFoundation\Request; public function new(Request $request) { - // just setup a fresh $task object (remove the dummy data) + // just setup a fresh $task object (remove the example data) $task = new Task(); - $form = $this->createFormBuilder($task) - ->add('task', TextType::class) - ->add('dueDate', DateType::class) - ->add('save', SubmitType::class, ['label' => 'Create Task']) - ->getForm(); + $form = $this->createForm(TaskType::class, $task); $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { // $form->getData() holds the submitted values // but, the original `$task` variable has also been updated @@ -245,25 +400,17 @@ your controller:: ]); } -.. caution:: - - Be aware that the ``createView()`` method should be called *after* ``handleRequest()`` - is called. Otherwise, changes done in the ``*_SUBMIT`` events aren't applied to the - view (like validation errors). - This controller follows a common pattern for handling forms and has three possible paths: -#. When initially loading the page in a browser, the form is created and - rendered. :method:`Symfony\\Component\\Form\\FormInterface::handleRequest` - recognizes that the form was not submitted and does nothing. - :method:`Symfony\\Component\\Form\\FormInterface::isSubmitted` returns ``false`` - if the form was not submitted. +#. When initially loading the page in a browser, the form hasn't been submitted + yet and ``$form->isSubmitted()`` returns ``false``. So, the form is created + and rendered; #. When the user submits the form, :method:`Symfony\\Component\\Form\\FormInterface::handleRequest` recognizes this and immediately writes the submitted data back into the ``task`` and ``dueDate`` properties of the ``$task`` object. Then this object - is validated. If it is invalid (validation is covered in the next section), + is validated (validation is explained in the next section). If it is invalid, :method:`Symfony\\Component\\Form\\FormInterface::isValid` returns ``false`` and the form is rendered again, but now with validation errors; @@ -271,34 +418,36 @@ possible paths: written into the form, but this time :method:`Symfony\\Component\\Form\\FormInterface::isValid` returns ``true``. Now you have the opportunity to perform some actions using the ``$task`` object (e.g. persisting it to the database) before redirecting - the user to some other page (e.g. a "thank you" or "success" page). + the user to some other page (e.g. a "thank you" or "success" page); - .. note:: +.. note:: + + Redirecting a user after a successful form submission is a best practice + that prevents the user from being able to hit the "Refresh" button of + their browser and re-post the data. - Redirecting a user after a successful form submission prevents the user - from being able to hit the "Refresh" button of their browser and re-post - the data. +.. caution:: + + The ``createView()`` method should be called *after* ``handleRequest()`` is + called. Otherwise, when using :doc:`form events `, changes done + in the ``*_SUBMIT`` events won't be applied to the view (like validation errors). .. seealso:: If you need more control over exactly when your form is submitted or which - data is passed to it, you can use the :method:`Symfony\\Component\\Form\\FormInterface::submit` - method. Read more about it :ref:`form-call-submit-directly`. - -.. index:: - single: Forms; Validation + data is passed to it, you can + :doc:`use the submit() method to handle form submissions `. -.. _forms-form-validation: +.. _validating-forms: -Form Validation ---------------- +Validating Forms +---------------- In the previous section, you learned how a form can be submitted with valid -or invalid data. In Symfony, validation is applied to the underlying object -(e.g. ``Task``). In other words, the question isn't whether the "form" is -valid, but whether or not the ``$task`` object is valid after the form has -applied the submitted data to it. Calling ``$form->isValid()`` is a shortcut -that asks the ``$task`` object whether or not it has valid data. +or invalid data. In Symfony, the question isn't whether the "form" is valid, but +whether or not the underlying object (``$task`` in this example) is valid after +the form has applied the submitted data to it. Calling ``$form->isValid()`` is a +shortcut that asks the ``$task`` object whether or not it has valid data. Before using validation, add support for it in your application: @@ -389,215 +538,286 @@ object. } That's it! If you re-submit the form with invalid data, you'll see the -corresponding errors printed out with the form. +corresponding errors printed out with the form. Read the +:doc:`Symfony validation documentation ` to learn more about this +powerful feature. -Validation is a very powerful feature of Symfony and has its own -:doc:`dedicated article `. +Other Common Form Features +-------------------------- -.. _forms-html5-validation-disable: +Passing Options to Forms +~~~~~~~~~~~~~~~~~~~~~~~~ -.. sidebar:: HTML5 Validation +If you :ref:`create forms in classes `, when building +the form in the controller you can pass custom options to it as the third optional +argument of ``createForm()``:: - Thanks to HTML5, many browsers can natively enforce certain validation constraints - on the client side. The most common validation is activated by rendering - a ``required`` attribute on fields that are required. For browsers that - support HTML5, this will result in a native browser message being displayed - if the user tries to submit the form with that field blank. + // src/Controller/TaskController.php + use App\Form\Type\TaskType; + // ... - Generated forms take full advantage of this new feature by adding sensible - HTML attributes that trigger the validation. The client-side validation, - however, can be disabled by adding the ``novalidate`` attribute to the - ``form`` tag or ``formnovalidate`` to the submit tag. This is especially - useful when you want to test your server-side validation constraints, - but are being prevented by your browser from, for example, submitting - blank fields. + class TaskController extends AbstractController + { + public function new() + { + $task = new Task(); + // use some PHP logic to decide if this form field is required or not + $dueDateIsRequired = ... - .. code-block:: twig + $form = $this->createForm(TaskType::class, $task, [ + 'require_due_date' => $dueDateIsRequired, + ]); - {# templates/task/new.html.twig #} - {{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }} - {{ form_widget(form) }} - {{ form_end(form) }} + // ... + } + } -.. index:: - single: Forms; Built-in field types +If you try to use the form now, you'll see an error message: *The option +"require_due_date" does not exist.* That's because forms must declare all the +options they accept using the ``configureOptions()`` method:: -.. _forms-type-reference: + // src/Form/Type/TaskType.php + use Symfony\Component\OptionsResolver\OptionsResolver; + // ... -Built-in Field Types --------------------- + class TaskType extends AbstractType + { + // ... -Symfony comes standard with a large group of field types that cover all of -the common form fields and data types you'll encounter: + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + // ..., + 'require_due_date' => false, + ]); -.. include:: /reference/forms/types/map.rst.inc + // you can also define the allowed types, allowed values and + // any other feature supported by the OptionsResolver component + $resolver->setAllowedTypes('require_due_date', 'bool'); + } + } -You can also create your own custom field types. See -:doc:`/form/create_custom_field_type` for info. +Now you can use this new form option inside the ``buildForm()`` method:: -.. index:: - single: Forms; Field type options + // src/Form/Type/TaskType.php + namespace App\Form\Type; -Field Type Options -~~~~~~~~~~~~~~~~~~ + use Symfony\Component\Form\AbstractType; + use Symfony\Component\Form\Extension\Core\Type\DateType; + use Symfony\Component\Form\FormBuilderInterface; -Each field type has a number of options that can be used to configure it. -For example, the ``dueDate`` field is currently being rendered as 3 select -boxes. However, the :doc:`DateType ` can be -configured to be rendered as a single text box (where the user would enter -the date as a string in the box):: + class TaskType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + // ... + ->add('dueDate', DateType::class, [ + 'required' => $options['require_due_date'], + ]) + ; + } - ->add('dueDate', DateType::class, ['widget' => 'single_text']) + // ... + } -.. image:: /_images/form/simple-form-2.png - :align: center +Form Type Options +~~~~~~~~~~~~~~~~~ -Each field type has a number of different options that can be passed to it. -Many of these are specific to the field type and details can be found in -the documentation for each type. +Each :ref:`form type ` has a number of options to configure it, as +explained in the :doc:`Symfony form types reference `. +Two commonly used options are ``required`` and ``label``. -.. sidebar:: The ``required`` Option +The ``required`` Option +....................... - The most common option is the ``required`` option, which can be applied to - any field. By default, the ``required`` option is set to ``true``, meaning - that HTML5-ready browsers will apply client-side validation if the field - is left blank. If you don't want this behavior, either - :ref:`disable HTML5 validation ` - or set the ``required`` option on your field to ``false``:: +The most common option is the ``required`` option, which can be applied to any +field. By default, this option is set to ``true``, meaning that HTML5-ready +browsers will require to fill in all fields before submitting the form. - ->add('dueDate', DateType::class, [ - 'widget' => 'single_text', - 'required' => false - ]) +If you don't want this behavior, either +:ref:`disable client-side validation ` for the +entire form or set the ``required`` option to ``false`` on one or more fields:: - Also note that setting the ``required`` option to ``true`` will **not** - result in server-side validation to be applied. In other words, if a - user submits a blank value for the field (either with an old browser - or web service, for example), it will be accepted as a valid value unless - you use Symfony's ``NotBlank`` or ``NotNull`` validation constraint. + ->add('dueDate', DateType::class, [ + 'required' => false, + ]) - In other words, the ``required`` option is "nice", but true server-side - validation should *always* be used. +The ``required`` option does not perform any server-side validation. If a user +submits a blank value for the field (either with an old browser or a web +service, for example), it will be accepted as a valid value unless you also use +Symfony's ``NotBlank`` or ``NotNull`` validation constraints. -.. sidebar:: The ``label`` Option +The ``label`` Option +.................... - The label for the form field can be set using the ``label`` option, - which can be applied to any field:: +By default, the label of form fields are the *humanized* version of the +property name (``user`` -> ``User``; ``postalAddress`` -> ``Postal Address``). +Set the ``label`` option on fields to define their labels explicitly:: - ->add('dueDate', DateType::class, [ - 'widget' => 'single_text', - 'label' => 'Due Date', - ]) + ->add('dueDate', DateType::class, [ + // set it to FALSE to not display the label for this field + 'label' => 'To Be Completed Before', + ]) - The label for a field can also be set in the template rendering the - form, see below. If you don't need a label associated to your input, - you can disable it by setting its value to ``false``. +.. tip:: - .. tip:: + By default, ``
`` name and the field names are generated from the type class name +(e.g. ```` and ````). + If you create your forms with :doc:`Symfony Forms ` this is done + automatically for you. -Routes can be localized to provide unique paths per :doc:`locale `. -Symfony provides a handy way to declare localized routes without duplication. +Matching Expressions +~~~~~~~~~~~~~~~~~~~~ + +Use the ``condition`` option if you need some route to match based on some +arbitrary matching logic: .. configuration-block:: .. code-block:: php-annotations - // src/Controller/CompanyController.php + // src/Controller/DefaultController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; - class CompanyController extends AbstractController + class DefaultController extends AbstractController { /** - * @Route({ - * "nl": "/over-ons", - * "en": "/about-us" - * }, name="about_us") + * @Route( + * "/contact", + * name="contact", + * condition="context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'" + * ) + * + * expressions can also include config parameters: + * condition: "request.headers.get('User-Agent') matches '%app.allowed_browsers%'" */ - public function about() + public function contact() { // ... } @@ -195,11 +266,12 @@ Symfony provides a handy way to declare localized routes without duplication. .. code-block:: yaml # config/routes.yaml - about_us: - path: - nl: /over-ons - en: /about-us - controller: App\Controller\CompanyController::about + contact: + path: /contact + controller: 'App\Controller\DefaultController::contact' + condition: "context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'" + # expressions can also include config parameters: + # condition: "request.headers.get('User-Agent') matches '%app.allowed_browsers%'" .. code-block:: xml @@ -210,103 +282,191 @@ Symfony provides a handy way to declare localized routes without duplication. xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd"> - - /over-ons - /about-us + + context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i' + + .. code-block:: php // config/routes.php - use App\Controller\CompanyController; + use App\Controller\DefaultController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; return function (RoutingConfigurator $routes) { - $routes->add('about_us', [ - 'nl' => '/over-ons', - 'en' => '/about-us', - ]) - ->controller([CompanyController::class, 'about']) + $routes->add('contact', '') + ->controller([DefaultController::class, 'contact']) + ->condition('context.getMethod() in ["GET", "HEAD"] and request.headers.get("User-Agent") matches "/firefox/i"') + // expressions can also include config parameters: + // 'request.headers.get("User-Agent") matches "%app.allowed_browsers%"' ; }; -When a localized route is matched Symfony automatically knows which locale -should be used during the request. Defining routes this way also eliminated the -need for duplicate registration of routes which minimizes the risk for any bugs -caused by definition inconsistency. +The value of the ``condition`` option is any valid +:doc:`ExpressionLanguage expression ` +and can use any of these variables created by Symfony: -.. tip:: +``context`` + An instance of :class:`Symfony\\Component\\Routing\\RequestContext`, + which holds the most fundamental information about the route being matched. - If the application uses full language + territory locales (e.g. ``fr_FR``, - ``fr_BE``), you can use the language part only in your routes (e.g. ``fr``). - This prevents having to define multiple paths when you want to use the same - route path for locales that share the same language. +``request`` + The :ref:`Symfony Request ` object that + represents the current request. -A common requirement for internationalized applications is to prefix all routes -with a locale. This can be done by defining a different prefix for each locale -(and setting an empty prefix for your default locale if you prefer it): +Behind the scenes, expressions are compiled down to raw PHP. Because of this, +using the ``condition`` key causes no extra overhead beyond the time it takes +for the underlying PHP to execute. + +.. caution:: + + Conditions are *not* taken into account when generating URLs (which is + explained later in this article). + +Debugging Routes +~~~~~~~~~~~~~~~~ + +As your application grows, you'll eventually have a *lot* of routes. Symfony +includes some commands to help you debug routing issues. First, the ``debug:router`` +command lists all your application routes in the same order in which Symfony +evaluates them: + +.. code-block:: terminal + + $ php bin/console debug:router + + ---------------- ------- ------- ----- -------------------------------------------- + Name Method Scheme Host Path + ---------------- ------- ------- ----- -------------------------------------------- + homepage ANY ANY ANY / + contact GET ANY ANY /contact + contact_process POST ANY ANY /contact + article_show ANY ANY ANY /articles/{_locale}/{year}/{title}.{_format} + blog ANY ANY ANY /blog/{page} + blog_show ANY ANY ANY /blog/{slug} + ---------------- ------- ------- ----- -------------------------------------------- + +Pass the name (or part of the name) of some route to this argument to print the +route details: + +.. code-block:: terminal + + $ php bin/console debug:router app_lucky_number + + +-------------+---------------------------------------------------------+ + | Property | Value | + +-------------+---------------------------------------------------------+ + | Route Name | app_lucky_number | + | Path | /lucky/number/{max} | + | ... | ... | + | Options | compiler_class: Symfony\Component\Routing\RouteCompiler | + | | utf8: true | + +-------------+---------------------------------------------------------+ + +The other command is called ``router:match`` and it shows which route will match +the given URL. It's useful to find out why some URL is not executing the +controller action that you expect: + +.. code-block:: terminal + + $ php bin/console router:match /lucky/number/8 + + [OK] Route "app_lucky_number" matches + +Route Parameters +---------------- + +The previous examples defined routes where the URL never changes (e.g. ``/blog``). +However, it's common to define routes where some parts are variable. For example, +the URL to display some blog post will probably include the title or slug +(e.g. ``/blog/my-first-post`` or ``/blog/all-about-symfony``). + +In Symfony routes, variable parts are wrapped in ``{ ... }`` and they must have +a unique name. For example, the route to display the blog post contents is +defined as ``/blog/{slug}``: .. configuration-block:: + .. code-block:: php-annotations + + // src/Controller/BlogController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class BlogController extends AbstractController + { + // ... + + /** + * @Route("/blog/{slug}", name="blog_show") + */ + public function show(string $slug) + { + // $slug will equal the dynamic part of the URL + // e.g. at /blog/yay-routing, then $slug='yay-routing' + + // ... + } + } + .. code-block:: yaml - # config/routes/annotations.yaml - controllers: - resource: '../../src/Controller/' - type: annotation - prefix: - en: '' # don't prefix URLs for English, the default locale - nl: '/nl' + # config/routes.yaml + blog_show: + path: /blog/{slug} + controller: App\Controller\BlogController::show .. code-block:: xml - + - - - - /nl - + + .. code-block:: php - // config/routes/annotations.php + // config/routes.php + use App\Controller\BlogController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; return function (RoutingConfigurator $routes) { - $routes->import('../src/Controller/', 'annotation') - ->prefix([ - // don't prefix URLs for English, the default locale - 'en' => '', - 'nl' => '/nl' - ]) + $routes->add('blog_show', '/blog/{slug}') + ->controller([BlogController::class, 'show']) ; }; -.. _routing-requirements: +The name of the variable part (``{slug}`` in this example) is used to create a +PHP variable where that route content is stored and passed to the controller. +If a user visits the ``/blog/my-first-post`` URL, Symfony executes the ``show()`` +method in the ``BlogController`` class and passes a ``$slug = 'my-first-post'`` +argument to the ``show()`` method. -Adding {wildcard} Requirements ------------------------------- +Routes can define any number of parameters, but each of them can only be used +once on each route (e.g. ``/blog/posts-about-{category}/page/{pageNumber}``). -Imagine the ``blog_list`` route will contain a paginated list of blog posts, with -URLs like ``/blog/2`` and ``/blog/3`` for pages 2 and 3. If you change the route's -path to ``/blog/{page}``, you'll have a problem: +.. _routing-requirements: -* blog_list: ``/blog/{page}`` will match ``/blog/*``; -* blog_show: ``/blog/{slug}`` will *also* match ``/blog/*``. +Parameters Validation +~~~~~~~~~~~~~~~~~~~~~ -When two routes match the same URL, the *first* route that's loaded wins. Unfortunately, -that means that ``/blog/yay-routing`` will match the ``blog_list``. No good! +Imagine that your application has a ``blog_show`` route (URL: ``/blog/{slug}``) +and a ``blog_list`` route (URL: ``/blog/{page}``). Given that route parameters +accept any value, there's no way to differentiate both routes. -To fix this, add a *requirement* that the ``{page}`` wildcard can *only* match numbers -(digits): +If the user requests ``/blog/my-first-post``, both routes will match and Symfony +will use the route which was defined first. To fix this, add some validation to +the ``{page}`` parameter using the ``requirements`` option: .. configuration-block:: @@ -323,7 +483,7 @@ To fix this, add a *requirement* that the ``{page}`` wildcard can *only* match n /** * @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"}) */ - public function list($page) + public function list(int $page) { // ... } @@ -341,13 +501,14 @@ To fix this, add a *requirement* that the ``{page}`` wildcard can *only* match n # config/routes.yaml blog_list: - path: /blog/{page} + path: /blog/{page} controller: App\Controller\BlogController::list requirements: page: '\d+' blog_show: - # ... + path: /blog/{slug} + controller: App\Controller\BlogController::show .. code-block:: xml @@ -362,7 +523,9 @@ To fix this, add a *requirement* that the ``{page}`` wildcard can *only* match n \d+ - + + .. code-block:: php @@ -376,20 +539,45 @@ To fix this, add a *requirement* that the ``{page}`` wildcard can *only* match n ->controller([BlogController::class, 'list']) ->requirements(['page' => '\d+']) ; + + $routes->add('blog_show', '/blog/{slug}') + ->controller([BlogController::class, 'show']) + ; // ... }; -The ``\d+`` is a regular expression that matches a *digit* of any length. Now: +The ``requirements`` option defines the `PHP regular expressions`_ that route +parameters must match for the entire route to match. In this example, ``\d+`` is +a regular expression that matches a *digit* of any length. Now: ======================== ============= =============================== URL Route Parameters ======================== ============= =============================== ``/blog/2`` ``blog_list`` ``$page`` = ``2`` -``/blog/yay-routing`` ``blog_show`` ``$slug`` = ``yay-routing`` +``/blog/my-first-post`` ``blog_show`` ``$slug`` = ``my-first-post`` ======================== ============= =============================== -If you prefer, requirements can be inlined in each placeholder using the syntax -``{placeholder_name}``. This feature makes configuration more +.. tip:: + + Route requirements (and route paths too) can include + :ref:`container parameters `, which is useful to + define complex regular expressions once and reuse them in multiple routes. + +.. tip:: + + Parameters also support `PCRE Unicode properties`_, which are escape + sequences that match generic character types. For example, ``\p{Lu}`` + matches any uppercase character in any language, ``\p{Greek}`` matches any + Greek character, etc. + +.. note:: + + When using regular expressions in route parameters, you can set the ``utf8`` + route option to ``true`` to make any ``.`` character match any UTF-8 + characters instead of just a single byte. + +If you prefer, requirements can be inlined in each parameter using the syntax +``{parameter_name}``. This feature makes configuration more concise, but it can decrease route readability when requirements are complex: .. configuration-block:: @@ -407,7 +595,7 @@ concise, but it can decrease route readability when requirements are complex: /** * @Route("/blog/{page<\d+>}", name="blog_list") */ - public function list($page) + public function list(int $page) { // ... } @@ -417,7 +605,7 @@ concise, but it can decrease route readability when requirements are complex: # config/routes.yaml blog_list: - path: /blog/{page<\d+>} + path: /blog/{page<\d+>} controller: App\Controller\BlogController::list .. code-block:: xml @@ -429,7 +617,8 @@ concise, but it can decrease route readability when requirements are complex: xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd"> - + @@ -447,19 +636,17 @@ concise, but it can decrease route readability when requirements are complex: // ... }; -To learn about other route requirements - like HTTP method, hostname and dynamic -expressions - see :doc:`/routing/requirements`. - -Giving {placeholders} a Default Value -------------------------------------- +Optional Parameters +~~~~~~~~~~~~~~~~~~~ -In the previous example, the ``blog_list`` has a path of ``/blog/{page}``. If -the user visits ``/blog/1``, it will match. But if they visit ``/blog``, it -will **not** match. As soon as you add a ``{placeholder}`` to a route, it -*must* have a value. +In the previous example, the URL of ``blog_list`` is ``/blog/{page}``. If users +visit ``/blog/1``, it will match. But if they visit ``/blog``, it will **not** +match. As soon as you add a parameter to a route, it must have a value. -So how can you make ``blog_list`` once again match when the user visits -``/blog``? By adding a *default* value: +You can make ``blog_list`` once again match when the user visits ``/blog`` by +adding a default value for the ``{page}`` parameter. When using annotations, +default values are defined in the arguments of the controller action. In the +other configuration formats they are defined with the ``defaults`` option: .. configuration-block:: @@ -476,7 +663,7 @@ So how can you make ``blog_list`` once again match when the user visits /** * @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"}) */ - public function list($page = 1) + public function list(int $page = 1) { // ... } @@ -486,7 +673,7 @@ So how can you make ``blog_list`` once again match when the user visits # config/routes.yaml blog_list: - path: /blog/{page} + path: /blog/{page} controller: App\Controller\BlogController::list defaults: page: 1 @@ -531,10 +718,31 @@ So how can you make ``blog_list`` once again match when the user visits Now, when the user visits ``/blog``, the ``blog_list`` route will match and ``$page`` will default to a value of ``1``. +.. caution:: + + You can have more than one optional parameter (e.g. ``/blog/{slug}/{page}``), + but everything after an optional parameter must be optional. For example, + ``/{page}/blog`` is a valid path, but ``page`` will always be required + (i.e. ``/blog`` will not match this route). + +.. note:: + + Routes with optional parameters at the end will not match on requests + with a trailing slash (i.e. ``/blog/`` will not match, ``/blog`` will match). + +If you want to always include some default value in the generated URL (for +example to force the generation of ``/blog/1`` instead of ``/blog`` in the +previous example) add the ``!`` character before the parameter name: ``/blog/{!page}`` + +.. versionadded:: 4.3 + + The feature to force the inclusion of default values in generated URLs was + introduced in Symfony 4.3. + As it happens with requirements, default values can also be inlined in each -placeholder using the syntax ``{placeholder_name?default_value}``. This feature +parameter using the syntax ``{parameter_name?default_value}``. This feature is compatible with inlined requirements, so you can inline both in a single -placeholder: +parameter: .. configuration-block:: @@ -551,7 +759,7 @@ placeholder: /** * @Route("/blog/{page<\d+>?1}", name="blog_list") */ - public function list($page) + public function list(int $page) { // ... } @@ -561,7 +769,7 @@ placeholder: # config/routes.yaml blog_list: - path: /blog/{page<\d+>?1} + path: /blog/{page<\d+>?1} controller: App\Controller\BlogController::list .. code-block:: xml @@ -573,7 +781,8 @@ placeholder: xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd"> - + @@ -592,73 +801,871 @@ placeholder: .. tip:: - To give a ``null`` default value to any placeholder, add nothing after the + To give a ``null`` default value to any parameter, add nothing after the ``?`` character (e.g. ``/blog/{page?}``). -Listing all of your Routes --------------------------- +Parameter Conversion +~~~~~~~~~~~~~~~~~~~~ + +A common routing need is to convert the value stored in some parameter (e.g. an +integer acting as the user ID) into another value (e.g. the object that +represents the user). This feature is called "param converter" and is only +available when using annotations to define routes. -As your app grows, you'll eventually have a *lot* of routes! To see them all, run: +In case you didn't run this command before, run it now to add support for +annotations and "param converters": .. code-block:: terminal - $ php bin/console debug:router + $ composer require annotations - ------------------------------ -------- ------------------------------------- - Name Method Path - ------------------------------ -------- ------------------------------------- - app_lucky_number ANY /lucky/number/{max} - ... - ------------------------------ -------- ------------------------------------- +Now, keep the previous route configuration, but change the arguments of the +controller action. Instead of ``string $slug``, add ``BlogPost $post``:: -.. index:: - single: Routing; Advanced example - single: Routing; _format parameter + // src/Controller/BlogController.php + namespace App\Controller; -.. _advanced-routing-example: + use App\Entity\BlogPost; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; -Advanced Routing Example ------------------------- + class BlogController extends AbstractController + { + // ... -With all of this in mind, check out this advanced example: + /** + * @Route("/blog/{slug}", name="blog_show") + */ + public function show(BlogPost $post) + { + // $post is the object whose slug matches the routing parameter -.. configuration-block:: + // ... + } + } - .. code-block:: php-annotations +If your controller arguments include type-hints for objects (``BlogPost`` in +this case), the "param converter" makes a database request to find the object +using the request parameters (``slug`` in this case). If no object is found, +Symfony generates a 404 response automatically. - // src/Controller/ArticleController.php +Read the `full param converter documentation`_ to learn about the converters +provided by Symfony and how to configure them. - // ... - class ArticleController extends AbstractController - { - /** - * @Route( - * "/articles/{_locale}/{year}/{slug}.{_format}", - * defaults={"_format": "html"}, - * requirements={ - * "_locale": "en|fr", - * "_format": "html|rss", - * "year": "\d+" - * } - * ) - */ - public function show($_locale, $year, $slug) - { - } +Special Parameters +~~~~~~~~~~~~~~~~~~ + +In addition to your own parameters, routes can include any of the following +special parameters created by Symfony: + +``_controller`` + This parameter is used to determine which controller and action is executed + when the route is matched. + +.. _routing-format-parameter: + +``_format`` + The matched value is used to set the "request format" of the ``Request`` object. + This is used for such things as setting the ``Content-Type`` of the response + (e.g. a ``json`` format translates into a ``Content-Type`` of ``application/json``). + +``_fragment`` + Used to set the fragment identifier, which is the optional last part of a URL that + starts with a ``#`` character and is used to identify a portion of a document. + +.. _routing-locale-parameter: + +``_locale`` + Used to set the :ref:`locale ` on the request. + +You can include these attributes (except ``_fragment``) both in individual routes +and in route imports. Symfony defines some special attributes with the same name +(except for the leading underscore) so you can define them easier: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Controller/ArticleController.php + + // ... + class ArticleController extends AbstractController + { + /** + * @Route( + * "/articles/{_locale}/search.{_format}", + * locale="en", + * format="html", + * requirements={ + * "_locale": "en|fr", + * "_format": "html|xml", + * } + * ) + */ + public function search() + { + } + } + + .. code-block:: yaml + + # config/routes.yaml + article_search: + path: /articles/{_locale}/search.{_format} + controller: App\Controller\ArticleController::search + locale: en + format: html + requirements: + _locale: en|fr + _format: html|xml + + .. code-block:: xml + + + + + + + + en|fr + html|rss + + + + + .. code-block:: php + + // config/routes.php + namespace Symfony\Component\Routing\Loader\Configurator; + + use App\Controller\ArticleController; + + return function (RoutingConfigurator $routes) { + $routes->add('article_show', '/articles/{_locale}/search.{_format}') + ->controller([ArticleController::class, 'search']) + ->locale('en') + ->format('html') + ->requirements([ + '_locale' => 'en|fr', + '_format' => 'html|rss', + ]) + ; + }; + +.. versionadded:: 4.3 + + The special attributes were introduced in Symfony 4.3. + +Extra Parameters +~~~~~~~~~~~~~~~~ + +In the ``defaults`` option of a route you can optionally define parameters not +included in the route configuration. This is useful to pass extra arguments to +the controllers of the routes: + +.. configuration-block:: + + .. code-block:: php-annotations + + use Symfony\Component\Routing\Annotation\Route; + + class BlogController + { + /** + * @Route("/blog/{page}", name="blog_index", defaults={"page": 1, "title": "Hello world!"}) + */ + public function index(int $page, string $title) + { + // ... + } + } + + .. code-block:: yaml + + # config/routes.yaml + blog_index: + path: /blog/{page} + controller: App\Controller\BlogController::index + defaults: + page: 1 + title: "Hello world!" + + .. code-block:: xml + + + + + + + 1 + Hello world! + + + + .. code-block:: php + + // config/routes.php + use App\Controller\BlogController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + $routes->add('blog_index', '/blog/{page}') + ->controller([BlogController::class, 'index']) + ->defaults([ + 'page' => 1, + 'title' => 'Hello world!', + ]) + ; + }; + +.. _routing-slash-in-parameters: + +Slash Characters in Route Parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Route parameters can contain any values except the ``/`` slash character, +because that's the character used to separate the different parts of the URLs. +For example, if the ``token`` value in the ``/share/{token}`` route contains a +``/`` character, this route won't match. + +A possible solution is to change the parameter requirements to be more permissive: + +.. configuration-block:: + + .. code-block:: php-annotations + + use Symfony\Component\Routing\Annotation\Route; + + class DefaultController + { + /** + * @Route("/share/{token}", name="share", requirements={"token"=".+"}) + */ + public function share($token) + { + // ... + } + } + + .. code-block:: yaml + + # config/routes.yaml + share: + path: /share/{token} + controller: App\Controller\DefaultController::share + requirements: + token: .+ + + .. code-block:: xml + + + + + + + .+ + + + + .. code-block:: php + + // config/routes.php + use App\Controller\DefaultController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + $routes->add('share', '/share/{token}') + ->controller([DefaultController::class, 'share']) + ->requirements([ + 'token' => '.+', + ]) + ; + }; + +.. note:: + + If the route defines several parameter and you apply this permissive + regular expression to all of them, the results won't be the expected. For + example, if the route definition is ``/share/{path}/{token}`` and both + ``path`` and ``token`` accept ``/``, then ``path`` will contain its contents + and the token, and ``token`` will be empty. + +.. note:: + + If the route includes the special ``{_format}`` parameter, you shouldn't + use the ``.+`` requirement for the parameters that allow slashes. For example, + if the pattern is ``/share/{token}.{_format}`` and ``{token}`` allows any + character, the ``/share/foo/bar.json`` URL will consider ``foo/bar.json`` + as the token and the format will be empty. This can be solved by replacing + the ``.+`` requirement by ``[^.]+`` to allow any character except dots. + +.. _routing-route-groups: + +Route Groups and Prefixes +------------------------- + +It's common for a group of routes to share some options (e.g. all routes related +to the blog start with ``/blog``) That's why Symfony includes a feature to share +route configuration. + +When defining routes as annotations, put the common configuration in the +``@Route`` annotation of the controller class. In other routing formats, define +the common configuration using options when importing the routes. + +.. configuration-block:: + + .. code-block:: php-annotations + + use Symfony\Component\Routing\Annotation\Route; + + /** + * @Route("/blog", requirements={"locale": "en|es|fr"}, name="blog_") + */ + class BlogController + { + /** + * @Route("/{_locale}", name="index") + */ + public function index() + { + // ... + } + + /** + * @Route("/{_locale}/posts/{slug}", name="post") + */ + public function show(Post $post) + { + // ... + } + } + + .. code-block:: yaml + + # config/routes/annotations.yaml + controllers: + resource: '../src/Controller/' + type: annotation + # this is added to the beginning of all imported route URLs + prefix: '/blog' + # this is added to the beginning of all imported route names + name_prefix: 'blog_' + # these requirements are added to all imported routes + requirements: + locale: 'en|es|fr' + # An imported route with an empty URL will become "/blog/" + # Uncomment this option to make that URL "/blog" instead + # trailing_slash_on_root: false + + .. code-block:: xml + + + + + + + + + en|es|fr + + + + + + + + + .. code-block:: php + + // config/routes/annotations.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + $routes->import('../src/Controller/', 'annotation') + // this is added to the beginning of all imported route URLs + ->prefix('/blog') + // An imported route with an empty URL will become "/blog/" + // Pass FALSE as the second argument to make that URL "/blog" instead + // ->prefix('/blog', false) + // this is added to the beginning of all imported route names + ->namePrefix('blog_') + // these requirements are added to all imported routes + ->requirements(['locale' => 'en|es|fr']) + ; + }; + +In this example, the route of the ``index()`` action will be called ``blog_index`` +and its URL will be ``/blog/``. The route of the ``show()`` action will be called +``blog_post`` and its URL will be ``/blog/{_locale}/posts/{slug}``. Both routes +will also validate that the ``_locale`` parameter matches the regular expression +defined in the class annotation. + +.. seealso:: + + Symfony can :doc:`import routes from different sources ` + and you can even create your own route loader. + +Getting the Route Name and Parameters +------------------------------------- + +The ``Request`` object created by Symfony stores all the route configuration +(such as the name and parameters) in the "request attributes". You can get this +information in a controller via the ``Request`` object:: + + // src/Controller/BlogController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Routing\Annotation\Route; + + class BlogController extends AbstractController + { + /** + * @Route("/blog", name="blog_list") + */ + public function list(Request $request) + { + // ... + + $routeName = $request->attributes->get('_route'); + $routeParameters = $request->attributes->get('_route_params'); + + // use this to get all the available attributes (not only routing ones): + $allAttributes = $request->attributes->all(); + } + } + +You can get this information in services too injecting the ``request_stack`` +service to :doc:`get the Request object in a service `. +In templates, use the :ref:`Twig global app variable ` to get +the request and its attributes: + +.. code-block:: twig + + {% set route_name = app.request.attributes.get('_route') %} + {% set route_parameters = app.request.attributes.get('_route_params') %} + + {# use this to get all the available attributes (not only routing ones) #} + {% set all_attributes = app.request.attributes.all %} + +Special Routes +-------------- + +Symfony defines some special controllers to render templates and redirect to +other routes from the route configuration so you don't have to create a +controller action. + +Rendering a Template Directly from a Route +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Read the section about :ref:`rendering a template from a route ` +in the main article about Symfony templates. + +Redirecting to URLs and Routes Directly from a Route +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use the ``RedirectController`` to redirect to other routes (``redirectAction``) +and URLs (``urlRedirectAction``): + +.. configuration-block:: + + .. code-block:: yaml + + # config/routes.yaml + doc_shortcut: + path: /doc + controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction + defaults: + route: 'doc_page' + # optionally you can define some arguments passed to the route + page: 'index' + version: 'current' + # redirections are temporary by default (code 302) but you can make them permanent (code 301) + permanent: true + # add this to keep the original query string parameters when redirecting + keepQueryParams: true + # add this to keep the HTTP method when redirecting. The redirect status changes + # * for temporary redirects, it uses the 307 status code instead of 302 + # * for permanent redirects, it uses the 308 status code instead of 301 + keepRequestMethod: true + + legacy_doc: + path: /legacy/doc + controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction + defaults: + # this value can be an absolute path or an absolute URL + path: 'https://legacy.example.com/doc' + permanent: true + + .. code-block:: xml + + + + + + + doc_page + + index + current + + true + + true + + true + + + + + https://legacy.example.com/doc + + true + + + + .. code-block:: php + + // config/routes.php + use App\Controller\DefaultController; + use Symfony\Bundle\FrameworkBundle\Controller\RedirectController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + $routes->add('doc_shortcut', '/doc') + ->controller([RedirectController::class, 'redirectAction']) + ->defaults([ + 'route' => 'doc_page', + // optionally you can define some arguments passed to the template + 'page' => 'index', + 'version' => 'current', + // redirections are temporary by default (code 302) but you can make them permanent (code 301) + 'permanent' => true, + // add this to keep the original query string parameters when redirecting + 'keepQueryParams' => true, + // add this to keep the HTTP method when redirecting. The redirect status changes: + // * for temporary redirects, it uses the 307 status code instead of 302 + // * for permanent redirects, it uses the 308 status code instead of 301 + 'keepRequestMethod' => true, + ]) + ; + + $routes->add('legacy_doc', '/legacy/doc') + ->controller([RedirectController::class, 'urlRedirectAction']) + ->defaults([ + // this value can be an absolute path or an absolute URL + 'path' => 'https://legacy.example.com/doc', + // redirections are temporary by default (code 302) but you can make them permanent (code 301) + 'permanent' => true, + ]) + ; + }; + +.. tip:: + + Symfony also provides some utilities to + :ref:`redirect inside controllers ` + +.. _routing-trailing-slash-redirection: + +Redirecting URLs with Trailing Slashes +...................................... + +Historically, URLs have followed the UNIX convention of adding trailing slashes +for directories (e.g. ``https://example.com/foo/``) and removing them to refer +to files (``https://example.com/foo``). Although serving different contents for +both URLs is OK, nowadays it's common to treat both URLs as the same URL and +redirect between them. + +Symfony follows this logic to redirect between URLs with and without trailing +slashes (but only for ``GET`` and ``HEAD`` requests): + +========== ======================================== ========================================== +Route URL If the requested URL is ``/foo`` If the requested URL is ``/foo/`` +========== ======================================== ========================================== +``/foo`` It matches (``200`` status response) It makes a ``301`` redirect to ``/foo`` +``/foo/`` It makes a ``301`` redirect to ``/foo/`` It matches (``200`` status response) +========== ======================================== ========================================== + +Sub-Domain Routing +------------------ + +Routes can configure a ``host`` option to require that the HTTP host of the +incoming requests matches some specific value. In the following example, both +routes match the same path (``/``) but one of them only responds to a specific +host name: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Controller/MainController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class MainController extends AbstractController + { + /** + * @Route("/", name="mobile_homepage", host="m.example.com") + */ + public function mobileHomepage() + { + // ... + } + + /** + * @Route("/", name="homepage") + */ + public function homepage() + { + // ... + } + } + + .. code-block:: yaml + + # config/routes.yaml + mobile_homepage: + path: / + host: m.example.com + controller: App\Controller\MainController::mobileHomepage + + homepage: + path: / + controller: App\Controller\MainController::homepage + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/routes.php + use App\Controller\MainController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + $routes->add('mobile_homepage', '/') + ->controller([MainController::class, 'mobileHomepage']) + ->host('m.example.com') + ; + $routes->add('homepage', '/') + ->controller([MainController::class, 'homepage']) + ; + }; + + return $routes; + +The value of the ``host`` option can include parameters (which is useful in +multi-tenant applications) and these parameters can be validated too with +``requirements``: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Controller/MainController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class MainController extends AbstractController + { + /** + * @Route( + * "/", + * name="mobile_homepage", + * host="{subdomain}.example.com", + * defaults={"subdomain"="m"}, + * requirements={"subdomain"="m|mobile"} + * ) + */ + public function mobileHomepage() + { + // ... + } + + /** + * @Route("/", name="homepage") + */ + public function homepage() + { + // ... + } + } + + .. code-block:: yaml + + # config/routes.yaml + mobile_homepage: + path: / + host: "{subdomain}.example.com" + controller: App\Controller\MainController::mobileHomepage + defaults: + subdomain: m + requirements: + subdomain: m|mobile + + homepage: + path: / + controller: App\Controller\MainController::homepage + + .. code-block:: xml + + + + + + + m + m|mobile + + + + + + .. code-block:: php + + // config/routes.php + use App\Controller\MainController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + $routes->add('mobile_homepage', '/') + ->controller([MainController::class, 'mobileHomepage']) + ->host('{subdomain}.example.com') + ->defaults([ + 'subdomain' => 'm', + ]) + ->requirements([ + 'subdomain' => 'm|mobile', + ]) + ; + $routes->add('homepage', '/') + ->controller([MainController::class, 'homepage']) + ; + }; + +In the above example, the ``subdomain`` parameter defines a default value because +otherwise you need to include a domain value each time you generate a URL using +these routes. + +.. tip:: + + You can also set the ``host`` option when :ref:`importing routes ` + to make all of them require that host name. + +.. note:: + + When using sub-domain routing, you must set the ``Host`` HTTP headers in + :doc:`functional tests ` or routes won't match:: + + $crawler = $client->request( + 'GET', + '/', + [], + [], + ['HTTP_HOST' => 'm.example.com'] + // or get the value from some container parameter: + // ['HTTP_HOST' => 'm.' . $client->getContainer()->getParameter('domain')] + ); + +.. _i18n-routing: + +Localized Routes (i18n) +----------------------- + +If your application is translated into multiple languages, each route can define +a different URL per each :doc:`translation locale `. This +avoids the need for duplicating routes, which also reduces the potential bugs: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Controller/CompanyController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class CompanyController extends AbstractController + { + /** + * @Route({ + * "en": "/about-us", + * "nl": "/over-ons" + * }, name="about_us") + */ + public function about() + { + // ... + } } .. code-block:: yaml # config/routes.yaml - article_show: - path: /articles/{_locale}/{year}/{slug}.{_format} - controller: App\Controller\ArticleController::show - defaults: - _format: html - requirements: - _locale: en|fr - _format: html|rss - year: \d+ + about_us: + path: + en: /about-us + nl: /over-ons + controller: App\Controller\CompanyController::about .. code-block:: xml @@ -669,239 +1676,496 @@ With all of this in mind, check out this advanced example: xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd"> - - - html - en|fr - html|rss - \d+ - + + /about-us + /over-ons .. code-block:: php // config/routes.php - use App\Controller\ArticleController; + use App\Controller\CompanyController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; return function (RoutingConfigurator $routes) { - $routes->add('article_show', '/articles/{_locale}/{year}/{slug}.{_format}') - ->controller([ArticleController::class, 'show']) - ->defaults([ - '_format' => 'html', - ]) - ->requirements([ - '_locale' => 'en|fr', - '_format' => 'html|rss', - 'year' => '\d+', - ]) + $routes->add('about_us', [ + 'en' => '/about-us', + 'nl' => '/over-ons', + ]) + ->controller([CompanyController::class, 'about']) ; }; -As you've seen, this route will only match if the ``{_locale}`` portion of -the URL is either ``en`` or ``fr`` and if the ``{year}`` is a number. This -route also shows how you can use a dot between placeholders instead of -a slash. URLs matching this route might look like: +When a localized route is matched, Symfony uses the same locale automatically +during the entire request. -* ``/articles/en/2010/my-post`` -* ``/articles/fr/2010/my-post.rss`` -* ``/articles/en/2013/my-latest-post.html`` +.. tip:: -.. _routing-format-param: + When the application uses full "language + territory" locales (e.g. ``fr_FR``, + ``fr_BE``), if the URLs are the same in all related locales, routes can use + only the language part (e.g. ``fr``) to avoid repeating the same URLs. -.. sidebar:: The Special ``_format`` Routing Parameter +A common requirement for internationalized applications is to prefix all routes +with a locale. This can be done by defining a different prefix for each locale +(and setting an empty prefix for your default locale if you prefer it): - This example also highlights the special ``_format`` routing parameter. - When using this parameter, the matched value becomes the "request format" - of the ``Request`` object. +.. configuration-block:: - Ultimately, the request format is used for such things as setting the - ``Content-Type`` of the response (e.g. a ``json`` request format translates - into a ``Content-Type`` of ``application/json``). + .. code-block:: yaml -.. note:: + # config/routes/annotations.yaml + controllers: + resource: '../src/Controller/' + type: annotation + prefix: + en: '' # don't prefix URLs for English, the default locale + nl: '/nl' - Sometimes you want to make certain parts of your routes globally configurable. - Symfony provides you with a way to do this by leveraging service container - parameters. Read more about this in ":doc:`/routing/service_container_parameters`". + .. code-block:: xml -Special Routing Parameters -~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + -As you've seen, each routing parameter or default value is eventually available -as an argument in the controller method. Additionally, there are four parameters -that are special: each adds a unique piece of functionality inside your application: + + + + /nl + + -``_controller`` - As you've seen, this parameter is used to determine which controller is - executed when the route is matched. + .. code-block:: php -``_format`` - Used to set the request format (:ref:`read more `). + // config/routes/annotations.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; -``_fragment`` - Used to set the fragment identifier, the optional last part of a URL that - starts with a ``#`` character and is used to identify a portion of a document. + return function (RoutingConfigurator $routes) { + $routes->import('../src/Controller/', 'annotation') + ->prefix([ + // don't prefix URLs for English, the default locale + 'en' => '', + 'nl' => '/nl' + ]) + ; + }; -``_locale`` - Used to set the locale on the request (:ref:`read more `). +.. _routing-generating-urls: -.. _routing-trailing-slash-redirection: +Generating URLs +--------------- -Redirecting URLs with Trailing Slashes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Routing systems are bidirectional: 1) they associate URLs with controllers (as +explained in the previous sections); 2) they generate URLs for a given route. +Generating URLs from routes allows you to not write the ```` +values manually in your HTML templates. Also, if the URL of some route changes, +you only have to update the route configuration and all links will be updated. -Historically, URLs have followed the UNIX convention of adding trailing slashes -for directories (e.g. ``https://example.com/foo/``) and removing them to refer -to files (``https://example.com/foo``). Although serving different contents for -both URLs is OK, nowadays it's common to treat both URLs as the same URL and -redirect between them. +To generate a URL, you need to specify the name of the route (e.g. +``blog_show``) and the values of the parameters defined by the route (e.g. +``slug = my-blog-post``). -Symfony follows this logic to redirect between URLs with and without trailing -slashes (but only for ``GET`` and ``HEAD`` requests): +For that reason each route has an internal name that must be unique in the +application. If you don't set the route name explicitly with the ``name`` +option, Symfony generates an automatic name based on the controller and action. -========== ======================================== ========================================== -Route path If the requested URL is ``/foo`` If the requested URL is ``/foo/`` ----------- ---------------------------------------- ------------------------------------------ -``/foo`` It matches (``200`` status response) It makes a ``301`` redirect to ``/foo`` -``/foo/`` It makes a ``301`` redirect to ``/foo/`` It matches (``200`` status response) -========== ======================================== ========================================== +Generating URLs in Controllers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. note:: +If your controller extends from the :ref:`AbstractController `, +use the ``generateUrl()`` helper:: - If your application defines different routes for each path (``/foo`` and - ``/foo/``) this automatic redirection doesn't take place and the right - route is always matched. + // src/Controller/BlogController.php + namespace App\Controller; -.. index:: - single: Routing; Controllers - single: Controller; String naming format + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -.. _controller-string-syntax: + class BlogController extends AbstractController + { + /** + * @Route("/blog", name="blog_list") + */ + public function list() + { + // ... -Controller Naming Pattern -------------------------- + // generate a URL with no route arguments + $signUpPage = $this->generateUrl('sign_up'); -The ``controller`` value in your routes has the format ``CONTROLLER_CLASS::METHOD``. + // generate a URL with route arguments + $userProfilePage = $this->generateUrl('user_profile', [ + 'username' => $user->getUsername(), + ]); -.. tip:: + // generated URLs are "absolute paths" by default. Pass a third optional + // argument to generate different URLs (e.g. an "absolute URL") + $signUpPage = $this->generateUrl('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL); - To refer to an action that is implemented as the ``__invoke()`` method of a controller class, - you do not have to pass the method name, you can also use the fully qualified class name (e.g. - ``App\Controller\BlogController``). + // when a route is localized, Symfony uses by default the current request locale + // pass a different '_locale' value if you want to set the locale explicitly + $signUpPageInDutch = $this->generateUrl('sign_up', ['_locale' => 'nl']); + } + } -.. index:: - single: Routing; Generating URLs +.. note:: -.. _routing-generate: + If you pass to the ``generateUrl()`` method some parameters that are not + part of the route definition, they are included in the generated URL as a + query string::: -Generating URLs ---------------- + $this->generateUrl('blog', ['page' => 2, 'category' => 'Symfony']); + // the 'blog' route only defines the 'page' parameter; the generated URL is: + // /blog/2?category=Symfony -The routing system can also generate URLs. In reality, routing is a bidirectional -system: mapping the URL to a controller and also a route back to a URL. +If your controller does not extend from ``AbstractController``, you'll need to +:ref:`fetch services in your controller ` and +follow the instructions of the next section. -To generate a URL, you need to specify the name of the route (e.g. ``blog_show``) -and any wildcards (e.g. ``slug = my-blog-post``) used in the path for that -route. With this information, an URL can be generated in a controller:: +.. _routing-generating-urls-in-services: - class BlogController extends AbstractController +Generating URLs in Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Inject the ``router`` Symfony service into your own services and use its +``generate()`` method. When using :doc:`service autowiring ` +you only need to add an argument in the service constructor and type-hint it with +the :class:`Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface` class:: + + // src/Service/SomeService.php + use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + + class SomeService { - public function show($slug) + private $router; + + public function __construct(UrlGeneratorInterface $router) + { + $this->router = $router; + } + + public function someMethod() { // ... - // /blog/my-blog-post - $url = $this->generateUrl( - 'blog_show', - ['slug' => 'my-blog-post'] - ); + // generate a URL with no route arguments + $signUpPage = $this->router->generate('sign_up'); + + // generate a URL with route arguments + $userProfilePage = $this->router->generate('user_profile', [ + 'username' => $user->getUsername(), + ]); + + // generated URLs are "absolute paths" by default. Pass a third optional + // argument to generate different URLs (e.g. an "absolute URL") + $signUpPage = $this->router->generate('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL); + + // when a route is localized, Symfony uses by default the current request locale + // pass a different '_locale' value if you want to set the locale explicitly + $signUpPageInDutch = $this->router->generate('sign_up', ['_locale' => 'nl']); } } -If you need to generate a URL from a service, type-hint the :class:`Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface` -service:: +Generating URLs in Templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // src/Service/SomeService.php +Read the section about :ref:`creating links between pages ` +in the main article about Symfony templates. + +Generating URLs in JavaScript +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your JavaScript code is included in a Twig template, you can use the +``path()`` and ``url()`` Twig functions to generate the URLs and store them in +JavaScript variables. The ``escape()`` function is needed to escape any +non-JavaScript-safe values: + +.. code-block:: html+twig + + + +If you need to generate URLs dynamically or if you are using pure JavaScript +code, this solution doesn't work. In those cases, consider using the +`FOSJsRoutingBundle`_. + +Generating URLs in Commands +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Generating URLs in commands works the same as +:ref:`generating URLs in services `. The +only difference is that commands are not executed in the HTTP context, so they +don't have access to HTTP requests. In practice, this means that if you generate +absolute URLs, you'll get ``http://localhost/`` as the host name instead of your +real host name. + +The solution is to configure the "request context" used by commands when they +generate URLs. This context can be configured globally for all commands: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + router.request_context.host: 'example.org' + router.request_context.base_url: 'my/path' + asset.request_context.base_path: '%router.request_context.base_url%' + + .. code-block:: xml + + + + + + + example.org + my/path + %router.request_context.base_url% + + + + + .. code-block:: php + + // config/services.php + $container->setParameter('router.request_context.host', 'example.org'); + $container->setParameter('router.request_context.base_url', 'my/path'); + $container->setParameter('asset.request_context.base_path', $container->getParameter('router.request_context.base_url')); + +This information can be configured per command too:: + + // src/Command/SomeCommand.php use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + use Symfony\Component\Routing\RouterInterface; + // ... - class SomeService + class SomeCommand extends Command { private $router; - public function __construct(UrlGeneratorInterface $router) + public function __construct(RouterInterface $router) { + parent::__construct(); + $this->router = $router; } - public function someMethod() + protected function execute(InputInterface $input, OutputInterface $output) { - $url = $this->router->generate( - 'blog_show', - ['slug' => 'my-blog-post'] - ); + // these values override any global configuration + $context = $this->router->getContext(); + $context->setHost('example.com'); + $context->setBaseUrl('my/path'); + + // generate a URL with no route arguments + $signUpPage = $this->router->generate('sign_up'); + + // generate a URL with route arguments + $userProfilePage = $this->router->generate('user_profile', [ + 'username' => $user->getUsername(), + ]); + + // generated URLs are "absolute paths" by default. Pass a third optional + // argument to generate different URLs (e.g. an "absolute URL") + $signUpPage = $this->router->generate('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL); + + // when a route is localized, Symfony uses by default the current request locale + // pass a different '_locale' value if you want to set the locale explicitly + $signUpPageInDutch = $this->router->generate('sign_up', ['_locale' => 'nl']); + // ... } } -.. index:: - single: Routing; Generating URLs in a template +Checking if a Route Exists +~~~~~~~~~~~~~~~~~~~~~~~~~~ -Generating URLs with Query Strings -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +In highly dynamic applications, it may be necessary to check whether a route +exists before using it to generate a URL. In those cases, don't use the +:method:`Symfony\\Component\\Routing\\Router::getRouteCollection` method because +that regenerates the routing cache and slows down the application. -The ``generate()`` method takes an array of wildcard values to generate the URI. -But if you pass extra ones, they will be added to the URI as a query string:: +Instead, try to generate the URL and catch the +:class:`Symfony\\Component\\Routing\\Exception\\RouteNotFoundException` thrown +when the route doesn't exist:: - $this->router->generate('blog', [ - 'page' => 2, - 'category' => 'Symfony', - ]); - // /blog/2?category=Symfony + use Symfony\Component\Routing\Exception\RouteNotFoundException; -Generating Localized URLs -~~~~~~~~~~~~~~~~~~~~~~~~~ + // ... -When a route is localized, Symfony uses by default the current request locale to -generate the URL. In order to generate the URL for a different locale you must -pass the ``_locale`` in the parameters array:: + try { + $url = $this->router->generate($routeName, $routeParameters); + } catch (RouteNotFoundException $e) { + // the route is not defined... + } - $this->router->generate('about_us', [ - '_locale' => 'nl', - ]); - // generates: /over-ons +.. _routing-force-https: -Generating URLs from a Template +Forcing HTTPS on Generated URLs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To generate URLs inside Twig, see the templating article: :ref:`templating-pages`. -If you also need to generate URLs in JavaScript, see :doc:`/routing/generate_url_javascript`. +By default, generated URLs use the same HTTP scheme as the current request. +In console commands, where there is no HTTP request, URLs use ``http`` by +default. You can change this per command (via the router's ``getContext()`` +method) or globally with these configuration parameters: -.. index:: - single: Routing; Absolute URLs +.. configuration-block:: -Generating Absolute URLs -~~~~~~~~~~~~~~~~~~~~~~~~ + .. code-block:: yaml -By default, the router will generate relative URLs (e.g. ``/blog``). From -a controller, pass ``UrlGeneratorInterface::ABSOLUTE_URL`` to the third argument of the ``generateUrl()`` -method:: + # config/services.yaml + parameters: + router.request_context.scheme: 'https' + asset.request_context.secure: true - use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + .. code-block:: xml + + + + + + + https + true + + + + + .. code-block:: php + + // config/services.php + $container->setParameter('router.request_context.scheme', 'https'); + $container->setParameter('asset.request_context.secure', true); + +Outside of console commands, use the ``schemes`` option to define the scheme of +each route explicitly: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Controller/MainController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class SecurityController extends AbstractController + { + /** + * @Route("/login", name="login", schemes={"https"}) + */ + public function login() + { + // ... + } + } + + .. code-block:: yaml + + # config/routes.yaml + login: + path: /login + controller: App\Controller\SeurityController::login + schemes: [https] + + .. code-block:: xml + + + + + + + + + + .. code-block:: php + + // config/routes.php + use App\Controller\SecurityController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + $routes->add('login', '/login') + ->controller([SecurityController::class, 'login']) + ->schemes(['https']) + ; + }; + +The URL generated for the ``login`` route will always use HTTPS. This means that +when using the ``path()`` Twig function to generate URLs, you may get an +absolute URL instead of a relative URL if the HTTP scheme of the original +request is different from the scheme used by the route: + +.. code-block:: twig + + {# if the current scheme is HTTPS, generates a relative URL: /login #} + {{ path('login') }} + + {# if the current scheme is HTTP, generates an absolute URL to change + the scheme: https://example.com/login #} + {{ path('login') }} + +The scheme requirement is also enforced for incoming requests. If you try to +access the ``/login`` URL with HTTP, you will automatically be redirected to the +same URL, but with the HTTPS scheme. + +If you want to force a group of routes to use HTTPS, you can define the default +scheme when importing them. The following example forces HTTPS on all routes +defined as annotations: + +.. configuration-block:: - $this->generateUrl('blog_show', ['slug' => 'my-blog-post'], UrlGeneratorInterface::ABSOLUTE_URL); - // http://www.example.com/blog/my-blog-post + .. code-block:: yaml + + # config/routes/annotations.yaml + controllers: + resource: '../src/Controller/' + type: annotation + defaults: + schemes: [https] + + .. code-block:: xml + + + + + + + HTTPS + + + + .. code-block:: php + + // config/routes/annotations.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + $routes->import('../src/Controller/', 'annotation') + ->schemes(['https']) + ; + }; .. note:: - The host that's used when generating an absolute URL is automatically - detected using the current ``Request`` object. When generating absolute - URLs from outside the web context (for instance in a console command) this - doesn't work. See :doc:`/console/request_context` to learn how to - solve this problem. + The Security component provides + :doc:`another way to enforce HTTP or HTTPS ` + via the ``requires_channel`` setting. Troubleshooting --------------- @@ -918,27 +2182,22 @@ This happens when your controller method has an argument (e.g. ``$slug``):: // ... } -But your route path does *not* have a ``{slug}`` wildcard (e.g. it is ``/blog/show``). -Add a ``{slug}`` to your route path: ``/blog/show/{slug}`` or give the argument -a default value (i.e. ``$slug = null``). +But your route path does *not* have a ``{slug}`` parameter (e.g. it is +``/blog/show``). Add a ``{slug}`` to your route path: ``/blog/show/{slug}`` or +give the argument a default value (i.e. ``$slug = null``). Some mandatory parameters are missing ("slug") to generate a URL for route "blog_show". -This means that you're trying to generate a URL to the ``blog_show`` route but you -are *not* passing a ``slug`` value (which is required, because it has a ``{slug}``) -wildcard in the route path. To fix this, pass a ``slug`` value when generating the -route:: +This means that you're trying to generate a URL to the ``blog_show`` route but +you are *not* passing a ``slug`` value (which is required, because it has a +``{slug}`` parameter in the route path). To fix this, pass a ``slug`` value when +generating the route:: $this->generateUrl('blog_show', ['slug' => 'slug-value']); // or, in Twig - // {{ path('blog_show', {'slug': 'slug-value'}) }} - -Keep Going! ------------ - -Routing, check! Now, uncover the power of :doc:`controllers `. + // {{ path('blog_show', {slug: 'slug-value'}) }} Learn more about Routing ------------------------ @@ -953,3 +2212,8 @@ Learn more about Routing :glob: routing/* + +.. _`PHP regular expressions`: https://www.php.net/manual/en/book.pcre.php +.. _`PCRE Unicode properties`: http://php.net/manual/en/regexp.reference.unicode.php +.. _`full param converter documentation`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html +.. _`FOSJsRoutingBundle`: https://github.com/FriendsOfSymfony/FOSJsRoutingBundle diff --git a/routing/conditions.rst b/routing/conditions.rst deleted file mode 100644 index 48b00f8b55f..00000000000 --- a/routing/conditions.rst +++ /dev/null @@ -1,112 +0,0 @@ -.. index:: - single: Routing; Conditions - -How to Restrict Route Matching through Conditions -================================================= - -A route can be made to match only certain routing placeholders (via regular -expressions), HTTP methods, or host names. If you need more flexibility to -define arbitrary matching logic, use the ``condition`` routing setting: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/DefaultController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class DefaultController extends AbstractController - { - /** - * @Route( - * "/contact", - * name="contact", - * condition="context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'" - * ) - * - * expressions can also include config parameters - * condition: "request.headers.get('User-Agent') matches '%app.allowed_browsers%'" - */ - public function contact() - { - // ... - } - } - - .. code-block:: yaml - - # config/routes.yaml - contact: - path: /contact - controller: 'App\Controller\DefaultController::contact' - condition: "context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'" - # expressions can also include config parameters - # condition: "request.headers.get('User-Agent') matches '%app.allowed_browsers%'" - - .. code-block:: xml - - - - - - - context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i' - - - - - - .. code-block:: php - - // config/routes.php - use App\Controller\DefaultController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('contact', '') - ->controller([DefaultController::class, 'contact']) - ->condition('context.getMethod() in ["GET", "HEAD"] and request.headers.get("User-Agent") matches "/firefox/i"') - // expressions can also include config parameters - // 'request.headers.get("User-Agent") matches "%app.allowed_browsers%"' - ; - }; - -The ``condition`` is an expression, and you can learn more about its syntax -here: :doc:`/components/expression_language/syntax`. With this, the route -won't match unless the HTTP method is either GET or HEAD *and* if the ``User-Agent`` -header matches ``firefox``. - -You can do any complex logic you need in the expression by leveraging two -variables that are passed into the expression: - -``context`` - An instance of :class:`Symfony\\Component\\Routing\\RequestContext`, - which holds the most fundamental information about the route being matched. -``request`` - The Symfony :class:`Symfony\\Component\\HttpFoundation\\Request` object - (see :ref:`component-http-foundation-request`). - -.. caution:: - - Conditions are *not* taken into account when generating a URL. - -.. sidebar:: Expressions are Compiled to PHP - - Behind the scenes, expressions are compiled down to raw PHP. Our example - would generate the following PHP in the cache directory:: - - if (rtrim($pathInfo, '/contact') === '' && ( - in_array($context->getMethod(), [0 => "GET", 1 => "HEAD"]) - && preg_match("/firefox/i", $request->headers->get("User-Agent")) - )) { - // ... - } - - Because of this, using the ``condition`` key causes no extra overhead - beyond the time it takes for the underlying PHP to execute. diff --git a/routing/custom_route_loader.rst b/routing/custom_route_loader.rst index e0bb4dc8271..60202690f17 100644 --- a/routing/custom_route_loader.rst +++ b/routing/custom_route_loader.rst @@ -4,6 +4,90 @@ How to Create a custom Route Loader =================================== +Simple applications can define all their routes in a single configuration file - +usually ``config/routes.yaml`` (see :ref:`routing-creating-routes`). +However, in most applications it's common to import routes definitions from +different resources: PHP annotations in controller files, YAML, XML or PHP +files stored in some directory, etc. + +Built-in Route Loaders +---------------------- + +Symfony provides several route loaders for the most common needs: + +.. configuration-block:: + + .. code-block:: yaml + + # config/routes.yaml + app_file: + # loads routes from the given routing file stored in some bundle + resource: '@AcmeBundle/Resources/config/routing.yaml' + + app_annotations: + # loads routes from the PHP annotations of the controllers found in that directory + resource: '../src/Controller/' + type: annotation + + app_directory: + # loads routes from the YAML, XML or PHP files found in that directory + resource: '../legacy/routing/' + type: directory + + app_bundle: + # loads routes from the YAML, XML or PHP files found in some bundle directory + resource: '@AcmeOtherBundle/Resources/config/routing/' + type: directory + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/routes.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + // loads routes from the given routing file stored in some bundle + $routes->import('@AcmeBundle/Resources/config/routing.yaml'); + + // loads routes from the PHP annotations of the controllers found in that directory + $routes->import('../src/Controller/', 'annotation'); + + // loads routes from the YAML or XML files found in that directory + $routes->import('../legacy/routing/', 'directory'); + + // loads routes from the YAML or XML files found in some bundle directory + $routes->import('@AcmeOtherBundle/Resources/config/routing/', 'directory'); + }; + +.. note:: + + When importing resources, the key (e.g. ``app_file``) is the name of collection. + Just be sure that it's unique per file so no other lines override it. + +If your application needs are different, you can create your own custom route +loader as explained in the next section. + What is a Custom Route Loader ----------------------------- @@ -133,6 +217,15 @@ extend or implement any special class, but the called method must return a cached by the framework. So whenever your service should load new routes, don't forget to clear the cache. +.. tip:: + + If your service is invokable, you don't need to precise the method to use. + +.. versionadded:: 4.3 + + The support of the ``__invoke()`` method to create invokable service route + loaders was introduced in Symfony 4.3. + Creating a custom Loader ------------------------ diff --git a/routing/debug.rst b/routing/debug.rst deleted file mode 100644 index 861de0bdd0a..00000000000 --- a/routing/debug.rst +++ /dev/null @@ -1,66 +0,0 @@ -.. index:: - single: Routing; Debugging - -How to Visualize And Debug Routes -================================= - -While adding and customizing routes, it's helpful to be able to visualize -and get detailed information about your routes. A great way to see every -route in your application is via the ``debug:router`` console command, which, -by default, lists *all* the configured routes in your application: - -.. code-block:: terminal - - $ php bin/console debug:router - - ------------------ -------- -------- ------ ---------------------------------------------- - Name Method Scheme Host Path - ------------------ -------- -------- ------ ---------------------------------------------- - homepage ANY ANY ANY / - contact GET ANY ANY /contact - contact_process POST ANY ANY /contact - article_show ANY ANY ANY /articles/{_locale}/{year}/{title}.{_format} - blog ANY ANY ANY /blog/{page} - blog_show ANY ANY ANY /blog/{slug} - ------------------ -------- -------- ------ ---------------------------------------------- - -You can also get very specific information on a single route by including -the route name as the command argument: - -.. code-block:: terminal - - $ php bin/console debug:router article_show - - # or use part of the name to search for routes - $ php bin/console debug:router blo - - Select one of the matching routes: - [0] blog - [1] blog_show - -Likewise, if you want to test whether a URL matches a given route, use the -``router:match`` command. This is useful to debug routing issues and find out -which route is associated with the given URL: - -.. code-block:: terminal - - $ php bin/console router:match /blog/my-latest-post - - Route "blog_show" matches - - +--------------+---------------------------------------------------------+ - | Property | Value | - +--------------+---------------------------------------------------------+ - | Route Name | blog_show | - | Path | /blog/{slug} | - | Path Regex | #^/blog/(?P[^/]++)$#sDu | - | Host | ANY | - | Host Regex | | - | Scheme | ANY | - | Method | ANY | - | Requirements | NO CUSTOM | - | Class | Symfony\Component\Routing\Route | - | Defaults | _controller: App\Controller\BlogController:show | - | Options | compiler_class: Symfony\Component\Routing\RouteCompiler | - | | utf8: true | - +--------------+---------------------------------------------------------+ diff --git a/routing/external_resources.rst b/routing/external_resources.rst deleted file mode 100644 index c3283e6561e..00000000000 --- a/routing/external_resources.rst +++ /dev/null @@ -1,260 +0,0 @@ -.. index:: - single: Routing; Importing routing resources - -How to Include External Routing Resources -========================================= - -Simple applications can define all their routes in a single configuration file - -usually ``config/routes.yaml`` (see :ref:`routing-creating-routes`). -However, in most applications it's common to import routes definitions from -different resources: PHP annotations in controller files, YAML, XML or PHP -files stored in some directory, etc. - -This can be done by importing routing resources from the main routing file: - -.. configuration-block:: - - .. code-block:: yaml - - # config/routes.yaml - app_file: - # loads routes from the given routing file stored in some bundle - resource: '@AcmeBundle/Resources/config/routing.yaml' - - app_annotations: - # loads routes from the PHP annotations of the controllers found in that directory - resource: '../src/Controller/' - type: annotation - - app_directory: - # loads routes from the YAML, XML or PHP files found in that directory - resource: '../legacy/routing/' - type: directory - - app_bundle: - # loads routes from the YAML, XML or PHP files found in some bundle directory - resource: '@AcmeOtherBundle/Resources/config/routing/' - type: directory - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - .. code-block:: php - - // config/routes.php - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - // loads routes from the given routing file stored in some bundle - $routes->import('@AcmeBundle/Resources/config/routing.yaml'); - - // loads routes from the PHP annotations of the controllers found in that directory - $routes->import('../src/Controller/', 'annotation'); - - // loads routes from the YAML or XML files found in that directory - $routes->import('../legacy/routing/', 'directory'); - - // loads routes from the YAML or XML files found in some bundle directory - $routes->import('@AcmeOtherBundle/Resources/config/routing/', 'directory'); - }; - -.. note:: - - When importing resources, the key (e.g. ``app_file``) is the name of collection. - Just be sure that it's unique per file so no other lines override it. - -.. _prefixing-imported-routes: - -Prefixing the URLs of Imported Routes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can also choose to provide a "prefix" for the imported routes. For example, -to prefix all application routes with ``/site`` (e.g. ``/site/blog/{slug}`` -instead of ``/blog/{slug}``): - -.. configuration-block:: - - .. code-block:: php-annotations - - use Symfony\Component\Routing\Annotation\Route; - - /** - * @Route("/site") - */ - class DefaultController - { - // ... - } - - .. code-block:: yaml - - # config/routes.yaml - controllers: - resource: '../src/Controller/' - type: annotation - prefix: /site - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // config/routes.php - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->import('../src/Controller/', 'annotation') - ->prefix('/site') - ; - }; - -The path of each route being loaded from the new routing resource will now -be prefixed with the string ``/site``. - -.. note:: - - If any of the prefixed routes defines an empty path, Symfony adds a trailing - slash to it. In the previous example, an empty path prefixed with ``/site`` - will result in the ``/site/`` URL. If you want to avoid this behavior, set - the ``trailing_slash_on_root`` option to ``false``: - - .. configuration-block:: - - .. code-block:: yaml - - # config/routes.yaml - controllers: - resource: '../src/Controller/' - type: annotation - prefix: /site - trailing_slash_on_root: false - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // config/routes.php - use App\Controller\ArticleController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->import('../src/Controller/', 'annotation') - ->prefix('/site', false) - ; - }; - -Prefixing the Names of Imported Routes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You also have the possibility to prefix the names of all the routes defined in -a controller class or imported from a configuration file: - -.. configuration-block:: - - .. code-block:: php-annotations - - use Symfony\Component\Routing\Annotation\Route; - - /** - * @Route(name="blog_") - */ - class BlogController - { - /** - * @Route("/blog", name="index") - */ - public function index() - { - // ... - } - - /** - * @Route("/blog/posts/{slug}", name="post") - */ - public function show(Post $post) - { - // ... - } - } - - .. code-block:: yaml - - # config/routes.yaml - controllers: - resource: '../src/Controller/' - type: annotation - name_prefix: 'blog_' - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // config/routes.php - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->import('../src/Controller/', 'annotation') - ->namePrefix('blog_') - ; - }; - -In this example, the names of the routes will be ``blog_index`` and ``blog_post``. - -Adding a Host Requirement to Imported Routes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can set the host regex on imported routes. For more information, see -:ref:`component-routing-host-imported`. diff --git a/routing/extra_information.rst b/routing/extra_information.rst deleted file mode 100644 index ce833426102..00000000000 --- a/routing/extra_information.rst +++ /dev/null @@ -1,102 +0,0 @@ -.. index:: - single: Routing; Extra Information - -How to Pass Extra Information from a Route to a Controller -========================================================== - -Parameters inside the ``defaults`` collection don't necessarily have to match -a placeholder in the route ``path``. In fact, you can use the ``defaults`` -array to specify extra parameters that will then be accessible as arguments -to your controller, and as attributes of the ``Request`` object: - -.. configuration-block:: - - .. code-block:: php-annotations - - use Symfony\Component\Routing\Annotation\Route; - - /** - * @Route(name="blog_") - */ - class BlogController - { - /** - * @Route("/blog/{page}", name="index", defaults={"page": 1, "title": "Hello world!"}) - */ - public function index($page) - { - // ... - } - } - - # config/routes.yaml - blog: - path: /blog/{page} - controller: App\Controller\BlogController::index - defaults: - page: 1 - title: "Hello world!" - - .. code-block:: yaml - - # config/routes.yaml - blog: - path: /blog/{page} - controller: App\Controller\BlogController::index - defaults: - page: 1 - title: "Hello world!" - - .. code-block:: xml - - - - - - - 1 - Hello world! - - - - .. code-block:: php - - // config/routes.php - use App\Controller\BlogController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('blog', '/blog/{page}') - ->controller([BlogController::class, 'index']) - ->defaults([ - 'page' => 1, - 'title' => 'Hello world!', - ]) - ; - }; - -Now, you can access this extra parameter in your controller, as an argument -to the controller method:: - - public function index($page, $title) - { - // ... - } - -Alternatively, the title could be accessed through the ``Request`` object:: - - use Symfony\Component\HttpFoundation\Request; - - public function index(Request $request, $page) - { - $title = $request->attributes->get('title'); - - // ... - } - -As you can see, the ``$title`` variable was never defined inside the route -path, but you can still access its value from inside your controller, through -the method's argument, or from the ``Request`` object's ``attributes`` bag. diff --git a/routing/generate_url_javascript.rst b/routing/generate_url_javascript.rst deleted file mode 100644 index 0893b53de8e..00000000000 --- a/routing/generate_url_javascript.rst +++ /dev/null @@ -1,25 +0,0 @@ -How to Generate Routing URLs in JavaScript -========================================== - -If you're in a Twig template, you can use the same ``path()`` function to set -JavaScript variables. The ``escape()`` function helps escape any -non-JavaScript-safe values: - -.. code-block:: html+twig - - - -But if you *actually* need to generate routes in pure JavaScript, consider using -the `FOSJsRoutingBundle`_. It makes the following possible: - -.. code-block:: html+twig - - - -.. _`FOSJsRoutingBundle`: https://github.com/FriendsOfSymfony/FOSJsRoutingBundle diff --git a/routing/hostname_pattern.rst b/routing/hostname_pattern.rst deleted file mode 100644 index f92e13f8285..00000000000 --- a/routing/hostname_pattern.rst +++ /dev/null @@ -1,439 +0,0 @@ -.. index:: - single: Routing; Matching on Hostname - -How to Match a Route Based on the Host -====================================== - -You can also match any route with the HTTP *host* of the incoming request. - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/MainController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends AbstractController - { - /** - * @Route("/", name="mobile_homepage", host="m.example.com") - */ - public function mobileHomepage() - { - // ... - } - - /** - * @Route("/", name="homepage") - */ - public function homepage() - { - // ... - } - } - - .. code-block:: yaml - - # config/routes.yaml - mobile_homepage: - path: / - host: m.example.com - controller: App\Controller\MainController::mobileHomepage - - homepage: - path: / - controller: App\Controller\MainController::homepage - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // config/routes.php - use App\Controller\MainController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('mobile_homepage', '/') - ->controller([MainController::class, 'mobileHomepage']) - ->host('m.example.com') - ; - $routes->add('homepage', '/') - ->controller([MainController::class, 'homepage']) - ; - }; - - return $routes; - -Both routes match the same path, ``/``. However, the first one will only -match if the host is ``m.example.com``. - -Using Placeholders ------------------- - -The host option uses the same syntax as the path matching system. This means -you can use placeholders in your hostname: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/MainController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends AbstractController - { - /** - * @Route("/", name="projects_homepage", host="{project}.example.com") - */ - public function projectsHomepage(string $project) - { - // ... - } - - /** - * @Route("/", name="homepage") - */ - public function homepage() - { - // ... - } - } - - .. code-block:: yaml - - # config/routes.yaml - projects_homepage: - path: / - host: "{project}.example.com" - controller: App\Controller\MainController::projectsHomepage - - homepage: - path: / - controller: App\Controller\MainController::homepage - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // config/routes.php - use App\Controller\MainController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('project_homepage', '/') - ->controller([MainController::class, 'projectHomepage']) - ->host('{project}.example.com') - ; - $routes->add('homepage', '/') - ->controller([MainController::class, 'homepage']) - ; - }; - -Also, any requirement or default can be set for these placeholders. For -instance, if you want to match both ``m.example.com`` and -``mobile.example.com``, you can use this: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/MainController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends AbstractController - { - /** - * @Route( - * "/", - * name="mobile_homepage", - * host="{subdomain}.example.com", - * defaults={"subdomain"="m"}, - * requirements={"subdomain"="m|mobile"} - * ) - */ - public function mobileHomepage() - { - // ... - } - - /** - * @Route("/", name="homepage") - */ - public function homepage() - { - // ... - } - } - - .. code-block:: yaml - - # config/routes.yaml - mobile_homepage: - path: / - host: "{subdomain}.example.com" - controller: App\Controller\MainController::mobileHomepage - defaults: - subdomain: m - requirements: - subdomain: m|mobile - - homepage: - path: / - controller: App\Controller\MainController::homepage - - .. code-block:: xml - - - - - - - m - m|mobile - - - - - - .. code-block:: php - - // config/routes.php - use App\Controller\MainController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('mobile_homepage', '/') - ->controller([MainController::class, 'mobileHomepage']) - ->host('{subdomain}.example.com') - ->defaults([ - 'subdomain' => 'm', - ]) - ->requirements([ - 'subdomain' => 'm|mobile', - ]) - ; - $routes->add('homepage', '/') - ->controller([MainController::class, 'homepage']) - ; - }; - -.. tip:: - - You can also use service parameters if you do not want to hardcode the - hostname: - - .. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/MainController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends AbstractController - { - /** - * @Route( - * "/", - * name="mobile_homepage", - * host="m.{domain}", - * defaults={"domain"="%domain%"}, - * requirements={"domain"="%domain%"} - * ) - */ - public function mobileHomepage() - { - // ... - } - - /** - * @Route("/", name="homepage") - */ - public function homepage() - { - // ... - } - } - - .. code-block:: yaml - - # config/routes.yaml - mobile_homepage: - path: / - host: "m.{domain}" - controller: App\Controller\MainController::mobileHomepage - defaults: - domain: '%domain%' - requirements: - domain: '%domain%' - - homepage: - path: / - controller: App\Controller\MainController::homepage - - .. code-block:: xml - - - - - - - %domain% - %domain% - - - - - - .. code-block:: php - - // config/routes.php - use App\Controller\MainController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('mobile_homepage', '/') - ->controller([MainController::class, 'mobileHomepage']) - ->host('m.{domain}') - ->defaults([ - 'domain' => '%domain%', - ]) - ->requirements([ - 'domain' => '%domain%', - ]) - ; - $routes->add('homepage', '/') - ->controller([MainController::class, 'homepage']) - ; - }; - -.. tip:: - - Make sure you also include a default option for the ``domain`` placeholder, - otherwise you need to include a domain value each time you generate - a URL using the route. - -.. _component-routing-host-imported: - -Using Host Matching of Imported Routes --------------------------------------- - -You can also set the host option on imported routes: - -.. configuration-block:: - - .. code-block:: php-annotations - - // vendor/acme/acme-hello-bundle/src/Controller/MainController.php - namespace Acme\AcmeHelloBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - /** - * @Route(host="hello.example.com") - */ - class MainController extends AbstractController - { - // ... - } - - .. code-block:: yaml - - # config/routes.yaml - app_hello: - resource: '@AcmeHelloBundle/Resources/config/routing.yaml' - host: "hello.example.com" - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // config/routes.php - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->import("@AcmeHelloBundle/Resources/config/routing.php") - ->host('hello.example.com') - ; - }; - -The host ``hello.example.com`` will be set on each route loaded from the new -routing resource. - -Testing your Controllers ------------------------- - -You need to set the Host HTTP header on your request objects if you want to get -past url matching in your functional tests:: - - $crawler = $client->request( - 'GET', - '/', - [], - [], - ['HTTP_HOST' => 'm.' . $client->getContainer()->getParameter('domain')] - ); diff --git a/routing/optional_placeholders.rst b/routing/optional_placeholders.rst deleted file mode 100644 index a988e4f7840..00000000000 --- a/routing/optional_placeholders.rst +++ /dev/null @@ -1,198 +0,0 @@ -.. index:: - single: Routing; Optional Placeholders - -How to Define Optional Placeholders -=================================== - -To make things more exciting, add a new route that displays a list of all -the available blog posts for this imaginary blog application: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/BlogController.php - use Symfony\Component\Routing\Annotation\Route; - - class BlogController - { - /** - * @Route("/blog") - */ - public function index() - { - // ... - } - // ... - } - - .. code-block:: yaml - - # config/routes.yaml - blog: - path: /blog - controller: App\Controller\BlogController::index - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // config/routes.php - use App\Controller\BlogController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('blog', '/blog') - ->controller([BlogController::class, 'index']) - ; - }; - -So far, this route is as simple as possible - it contains no placeholders -and will only match the exact URL ``/blog``. But what if you need this route -to support pagination, where ``/blog/2`` displays the second page of blog -entries? Update the route to have a new ``{page}`` placeholder: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/BlogController.php - - // ... - - /** - * @Route("/blog/{page}") - */ - public function index($page) - { - // ... - } - - .. code-block:: yaml - - # config/routes.yaml - blog: - path: /blog/{page} - controller: App\Controller\BlogController::index - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // config/routes.php - use App\Controller\BlogController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('blog', '/blog/{page}') - ->controller([BlogController::class, 'index']) - ; - }; - -Like the ``{slug}`` placeholder before, the value matching ``{page}`` will -be available inside your controller. Its value can be used to determine which -set of blog posts to display for the given page. - -But hold on! Since placeholders are required by default, this route will -no longer match on ``/blog`` alone. Instead, to see page 1 of the blog, -you'd need to use the URL ``/blog/1``! Since that's no way for a rich web -app to behave, modify the route to make the ``{page}`` parameter optional. -This is done by including it in the ``defaults`` collection: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/BlogController.php - - // ... - - /** - * @Route("/blog/{page}", defaults={"page"=1}) - */ - public function index($page) - { - // ... - } - - .. code-block:: yaml - - # config/routes.yaml - blog: - path: /blog/{page} - controller: App\Controller\BlogController::index - defaults: { page: 1 } - - .. code-block:: xml - - - - - - - 1 - - - - .. code-block:: php - - // config/routes.php - use App\Controller\BlogController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('blog', '/blog/{page}') - ->controller([BlogController::class, 'index']) - ->defaults([ - 'page' => 1, - ]) - ; - }; - -By adding ``page`` to the ``defaults`` key, the ``{page}`` placeholder is -no longer required. The URL ``/blog`` will match this route and the value -of the ``page`` parameter will be set to ``1``. The URL ``/blog/2`` will -also match, giving the ``page`` parameter a value of ``2``. Perfect. - -=========== ======== ================== -URL Route Parameters -=========== ======== ================== -``/blog`` ``blog`` ``{page}`` = ``1`` -``/blog/1`` ``blog`` ``{page}`` = ``1`` -``/blog/2`` ``blog`` ``{page}`` = ``2`` -=========== ======== ================== - -.. caution:: - - You can have more than one optional placeholder (e.g. ``/blog/{slug}/{page}``), - but everything after an optional placeholder must be optional. For example, - ``/{page}/blog`` is a valid path, but ``page`` will always be required - (i.e. ``/blog`` will not match this route). - -.. tip:: - - Routes with optional parameters at the end will not match on requests - with a trailing slash (i.e. ``/blog/`` will not match, ``/blog`` will match). diff --git a/routing/redirect_in_config.rst b/routing/redirect_in_config.rst deleted file mode 100644 index 87abde17df3..00000000000 --- a/routing/redirect_in_config.rst +++ /dev/null @@ -1,260 +0,0 @@ -.. index:: - single: Routing; Redirect using Framework:RedirectController - -How to Configure a Redirect without a custom Controller -======================================================= - -Sometimes, a URL needs to redirect to another URL. You can do that by creating -a new controller action whose only task is to redirect, but using the -:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\RedirectController` of -the FrameworkBundle is even easier. - -You can redirect to a specific path (e.g. ``/about``) or to a specific route -using its name (e.g. ``homepage``). - -Redirecting Using a Path ------------------------- - -Assume there is no default controller for the ``/`` path of your application -and you want to redirect these requests to ``/app``. You will need to use the -:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\RedirectController::urlRedirectAction` -action to redirect to this new url: - -.. configuration-block:: - - .. code-block:: yaml - - # config/routes.yaml - - # load some routes - one should ultimately have the path "/app" - controllers: - resource: '../src/Controller/' - type: annotation - prefix: /app - - # redirecting the homepage - homepage: - path: / - controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction - defaults: - path: /app - permanent: true - - .. code-block:: xml - - - - - - - - - - - /app - true - - - - .. code-block:: php - - // config/routes.php - use Symfony\Bundle\FrameworkBundle\Controller\RedirectController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - // load some routes - one should ultimately have the path "/app" - $routes->import('../src/Controller/', 'annotation') - ->prefix('/app') - ; - // redirecting the homepage - $routes->add('homepage', '/') - ->controller([RedirectController::class, 'urlRedirectAction']) - ->defaults([ - 'path' => '/app', - 'permanent' => true, - ]) - ; - }; - -In this example, you configured a route for the ``/`` path and let the -``RedirectController`` redirect it to ``/app``. The ``permanent`` switch -tells the action to issue a ``301`` HTTP status code instead of the default -``302`` HTTP status code. - -Redirecting Using a Route -------------------------- - -Assume you are migrating your website from WordPress to Symfony, you want to -redirect ``/wp-admin`` to the route ``sonata_admin_dashboard``. You don't know -the path, only the route name. This can be achieved using the -:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\RedirectController::redirectAction` -action: - -.. configuration-block:: - - .. code-block:: yaml - - # config/routes.yaml - - # ... - - admin: - path: /wp-admin - controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction - defaults: - route: sonata_admin_dashboard - # make a permanent redirection... - permanent: true - # ...and keep the original query string parameters - keepQueryParams: true - - .. code-block:: xml - - - - - - - - - sonata_admin_dashboard - - true - - true - - - - .. code-block:: php - - // config/routes.php - use Symfony\Bundle\FrameworkBundle\Controller\RedirectController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - // redirecting the homepage - $routes->add('admin', '/wp-admin') - ->controller([RedirectController::class, 'redirectAction']) - ->defaults([ - 'route' => 'sonata_admin_dashboard', - // make a permanent redirection... - 'permanent' => true, - // ...and keep the original query string parameters - 'keepQueryParams' => true, - ]) - ; - }; - -.. caution:: - - Because you are redirecting to a route instead of a path, the required - option is called ``route`` in the ``redirect()`` action, instead of ``path`` - in the ``urlRedirect()`` action. - -Keeping the Request Method when Redirecting -------------------------------------------- - -The redirections performed in the previous examples use the ``301`` and ``302`` -HTTP status codes. For legacy reasons, these HTTP redirections change the method -of ``POST`` requests to ``GET`` (because redirecting a ``POST`` request didn't -work well in old browsers). - -However, in some scenarios it's either expected or required that the redirection -request uses the same HTTP method. That's why the HTTP standard defines two -additional status codes (``307`` and ``308``) to perform temporary/permanent -redirects that maintain the original request method. - -The :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\RedirectController::urlRedirectAction` -and :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\RedirectController::redirectAction` -methods accept an additional argument called ``keepRequestMethod``. When it's -set to ``true``, temporary redirects use ``307`` code instead of ``302`` and -permanent redirects use ``308`` code instead of ``301``: - -.. configuration-block:: - - .. code-block:: yaml - - # config/routes.yaml - - # redirects with the 308 status code - route_foo: - # ... - controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction - defaults: - # ... - permanent: true - keepRequestMethod: true - - # redirects with the 307 status code - route_bar: - # ... - controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction - defaults: - # ... - permanent: false - keepRequestMethod: true - - .. code-block:: xml - - - - - - - - - true - true - - - - - - false - true - - - - .. code-block:: php - - // config/routes.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $collection = new RouteCollection(); - - // redirects with the 308 status code - $collection->add('route_foo', new Route('...', [ - // ... - '_controller' => 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction', - 'permanent' => true, - 'keepRequestMethod' => true, - ])); - - // redirects with the 307 status code - $collection->add('route_bar', new Route('...', [ - // ... - '_controller' => 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction', - 'permanent' => false, - 'keepRequestMethod' => true, - ])); - - return $collection; diff --git a/routing/redirect_trailing_slash.rst b/routing/redirect_trailing_slash.rst deleted file mode 100644 index 9f17a8c0b07..00000000000 --- a/routing/redirect_trailing_slash.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. index:: - single: Routing; Redirect URLs with a trailing slash - -Redirect URLs with a Trailing Slash -=================================== - -.. caution:: - - In Symfony 4.1 the automatic URL redirection was improved as explained in - :ref:`routing-trailing-slash-redirection`. That's why you no longer need to - do that redirection yourself and this article has been removed because it's - no longer needed. diff --git a/routing/requirements.rst b/routing/requirements.rst deleted file mode 100644 index e01655fb7ff..00000000000 --- a/routing/requirements.rst +++ /dev/null @@ -1,310 +0,0 @@ -.. index:: - single: Routing; Requirements - -How to Define Route Requirements -================================ - -:ref:`Route requirements ` can be used to make a specific route -*only* match under specific conditions. The simplest example involves restricting -a routing ``{wildcard}`` to only match some regular expression: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/BlogController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class BlogController extends AbstractController - { - /** - * @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"}) - */ - public function list($page) - { - // ... - } - } - - .. code-block:: yaml - - # config/routes.yaml - blog_list: - path: /blog/{page} - controller: App\Controller\BlogController::list - requirements: - page: '\d+' - - .. code-block:: xml - - - - - - - \d+ - - - - - - .. code-block:: php - - // config/routes.php - use App\Controller\BlogController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('blog_list', '/blog/{page}') - ->controller([BlogController::class, 'list']) - ->requirements([ - 'page' => '\d+', - ]) - ; - // ... - }; - -Thanks to the ``\d+`` requirement (i.e. a "digit" of any length), ``/blog/2`` will -match this route but ``/blog/some-string`` will *not* match. - -.. sidebar:: Earlier Routes Always Win - - Why would you ever care about requirements? If a request matches *two* routes, - then the first route always wins. By adding requirements to the first route, - you can make each route match in just the right situations. See :ref:`routing-requirements` - for an example. - -Since the parameter requirements are regular expressions, the complexity -and flexibility of each requirement is entirely up to you. Suppose the homepage -of your application is available in two different languages, based on the -URL: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/MainController.php - - // ... - class MainController extends AbstractController - { - /** - * @Route("/{_locale}", defaults={"_locale"="en"}, requirements={ - * "_locale"="en|fr" - * }) - */ - public function homepage($_locale) - { - // ... - } - } - - .. code-block:: yaml - - # config/routes.yaml - homepage: - path: /{_locale} - controller: App\Controller\MainController::homepage - defaults: { _locale: en } - requirements: - _locale: en|fr - - .. code-block:: xml - - - - - - - en - en|fr - - - - .. code-block:: php - - // config/routes.php - use App\Controller\MainController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('homepage', '/{_locale}') - ->controller([MainController::class, 'homepage']) - ->defaults([ - '_locale' => 'en', - ]) - ->requirements([ - '_locale' => 'en|fr', - ]) - ; - }; - -For incoming requests, the ``{_locale}`` portion of the URL is matched against -the regular expression ``(en|fr)``. - -======= ======================== -Path Parameters -======= ======================== -``/`` ``{_locale}`` = ``"en"`` -``/en`` ``{_locale}`` = ``"en"`` -``/fr`` ``{_locale}`` = ``"fr"`` -``/es`` *won't match this route* -======= ======================== - -.. note:: - - You can enable UTF-8 route matching by setting the ``utf8`` option when - declaring or importing routes. This will make e.g. a ``.`` in requirements - match any UTF-8 characters instead of just a single byte. - -.. tip:: - - The route requirements can also include container parameters, as explained - in :doc:`this article `. - This comes in handy when the regular expression is very complex and used - repeatedly in your application. - -.. index:: - single: Routing; Method requirement - -.. _routing-method-requirement: - -Adding HTTP Method Requirements -------------------------------- - -In addition to the URL, you can also match on the *method* of the incoming -request (i.e. GET, HEAD, POST, PUT, DELETE). Suppose you create an API for -your blog and you have 2 routes: One for displaying a post (on a GET or HEAD -request) and one for updating a post (on a PUT request). This can be -accomplished with the following route configuration: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/BlogApiController.php - namespace App\Controller; - - // ... - - class BlogApiController extends AbstractController - { - /** - * @Route("/api/posts/{id}", methods={"GET","HEAD"}) - */ - public function show($id) - { - // ... return a JSON response with the post - } - - /** - * @Route("/api/posts/{id}", methods={"PUT"}) - */ - public function edit($id) - { - // ... edit a post - } - } - - .. code-block:: yaml - - # config/routes.yaml - api_post_show: - path: /api/posts/{id} - controller: App\Controller\BlogApiController::show - methods: GET|HEAD - - api_post_edit: - path: /api/posts/{id} - controller: App\Controller\BlogApiController::edit - methods: PUT - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // config/routes.php - use App\Controller\BlogApiController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('api_post_show', '/api/posts/{id}') - ->controller([BlogApiController::class, 'show']) - ->methods(['GET', 'HEAD']) - ; - $routes->add('api_post_edit', '/api/posts/{id}') - ->controller([BlogApiController::class, 'edit']) - ->methods(['PUT']) - ; - - // or use collection - $api = $routes->collection('api_post_') - ->prefix('/api/posts/{id}') - ; - $api->add('show') - ->controller([BlogApiController::class, 'show']) - ->methods(['GET', 'HEAD']) - ; - $api->add('edit') - ->controller([BlogApiController::class, 'edit']) - ->methods(['PUT']) - ; - }; - -Despite the fact that these two routes have identical paths -(``/api/posts/{id}``), the first route will match only GET or HEAD requests and -the second route will match only PUT requests. This means that you can display -and edit the post with the same URL, while using distinct controllers for the -two actions. - -.. note:: - - If no ``methods`` are specified, the route will match on *all* methods. - -.. tip:: - - If you're using HTML forms and HTTP methods *other* than ``GET`` and ``POST``, - you'll need to include a ``_method`` parameter to *fake* the HTTP method. See - :doc:`/form/action_method` for more information. - -Adding a Host Requirement -------------------------- - -You can also match on the HTTP *host* of the incoming request. For more -information, see :doc:`/routing/hostname_pattern` in the Routing -component documentation. - -Adding Dynamic Requirements with Expressions --------------------------------------------- - -For really complex requirements, you can use dynamic expressions to match *any* -information on the request. See :doc:`/routing/conditions`. - -.. _`PCRE Unicode property`: http://php.net/manual/en/regexp.reference.unicode.php diff --git a/routing/routing_from_database.rst b/routing/routing_from_database.rst index b39960ad79e..01281de39a8 100644 --- a/routing/routing_from_database.rst +++ b/routing/routing_from_database.rst @@ -24,7 +24,7 @@ When all routes are known during deploy time and the number is not too high, using a :doc:`custom route loader ` is the preferred way to add more routes. When working with just one type of objects, a slug parameter on the object and the ``@ParamConverter`` -annotation work fine (see FrameworkExtraBundle_) . +annotation work fine (see `FrameworkExtraBundle`_) . The ``DynamicRouter`` is useful when you need ``Route`` objects with the full feature set of Symfony. Each route can define a specific diff --git a/routing/scheme.rst b/routing/scheme.rst deleted file mode 100644 index 0ca8b8587fb..00000000000 --- a/routing/scheme.rst +++ /dev/null @@ -1,94 +0,0 @@ -.. index:: - single: Routing; Scheme requirement - -How to Force Routes to Always Use HTTPS or HTTP -=============================================== - -Sometimes, you want to secure some routes and be sure that they are always -accessed via the HTTPS protocol. The Routing component allows you to enforce -the URI scheme with the ``schemes`` setting: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/MainController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends AbstractController - { - /** - * @Route("/secure", name="secure", schemes={"https"}) - */ - public function secure() - { - // ... - } - } - - .. code-block:: yaml - - # config/routes.yaml - secure: - path: /secure - controller: App\Controller\MainController::secure - schemes: [https] - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // config/routes.php - use App\Controller\MainController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('secure', '/secure') - ->controller([MainController::class, 'secure']) - ->schemes(['https']) - ; - }; - -The above configuration forces the ``secure`` route to always use HTTPS. - -When generating the ``secure`` URL, and if the current scheme is HTTP, Symfony -will automatically generate an absolute URL with HTTPS as the scheme, even when -using the ``path()`` function: - -.. code-block:: twig - - {# If the current scheme is HTTPS #} - {{ path('secure') }} - {# generates a relative URL: /secure #} - - {# If the current scheme is HTTP #} - {{ path('secure') }} - {# generates an absolute URL: https://example.com/secure #} - -The requirement is also enforced for incoming requests. If you try to access -the ``/secure`` path with HTTP, you will automatically be redirected to the -same URL, but with the HTTPS scheme. - -The above example uses ``https`` for the scheme, but you can also force a URL -to always use ``http``. - -.. note:: - - The Security component provides another way to enforce HTTP or HTTPS via - the ``requires_channel`` setting. This alternative method is better suited - to secure an "area" of your website (all URLs under ``/admin``) or when - you want to secure URLs defined in a third party bundle (see - :doc:`/security/force_https` for more details). diff --git a/routing/service_container_parameters.rst b/routing/service_container_parameters.rst deleted file mode 100644 index ba73b35d63c..00000000000 --- a/routing/service_container_parameters.rst +++ /dev/null @@ -1,208 +0,0 @@ -.. index:: - single: Routing; Service Container Parameters - -How to Use Service Container Parameters in your Routes -====================================================== - -Sometimes you may find it useful to make some parts of your routes -globally configurable. For instance, if you build an internationalized -site, you'll probably start with one or two locales. Surely you'll -add a requirement to your routes to prevent a user from matching a locale -other than the locales you support. - -You *could* hardcode your ``_locale`` requirement in all your routes, but -a better solution is to use a configurable service container parameter right -inside your routing configuration: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/MainController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends AbstractController - { - /** - * @Route("/{_locale}/contact", name="contact", requirements={ - * "_locale"="%app.locales%" - * }) - */ - public function contact() - { - // ... - } - } - - .. code-block:: yaml - - # config/routes.yaml - contact: - path: /{_locale}/contact - controller: App\Controller\MainController::contact - requirements: - _locale: '%app.locales%' - - .. code-block:: xml - - - - - - - %app.locales% - - - - .. code-block:: php - - // config/routes.php - use App\Controller\MainController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('contact', '/{_locale}/contact') - ->controller([MainController::class, 'contact']) - ->requirements([ - '_locale' => '%app.locales%', - ]) - ; - }; - -You can now control and set the ``app.locales`` parameter somewhere -in your container: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - parameters: - app.locales: en|es - - .. code-block:: xml - - - - - - - en|es - - - - .. code-block:: php - - // config/services.php - $container->setParameter('app.locales', 'en|es'); - -You can also use a parameter to define your route path (or part of your -path): - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/MainController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends AbstractController - { - /** - * @Route("/%app.route_prefix%/contact", name="contact") - */ - public function contact() - { - // ... - } - } - - .. code-block:: yaml - - # config/routes.yaml - some_route: - path: /%app.route_prefix%/contact - controller: App\Controller\MainController::contact - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // config/routes.php - use App\Controller\MainController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('contact', '/%app.route_prefix%/contact') - ->controller([MainController::class, 'contact']) - ; - }; - -Now make sure that the ``app.route_prefix`` parameter is set somewhere in your -container: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - parameters: - app.route_prefix: 'foo' - - .. code-block:: xml - - - - - - - foo - - - - .. code-block:: php - - // config/services.php - $container->setParameter('app.route_prefix', 'foo'); - -.. note:: - - Just like in normal service container configuration files, if you actually - need a ``%`` in your route, you can escape the percent sign by doubling - it, e.g. ``/score-50%%``, which would resolve to ``/score-50%``. - - However, as the ``%`` characters included in any URL are automatically encoded, - the resulting URL of this example would be ``/score-50%25`` (``%25`` is the - result of encoding the ``%`` character). - -.. seealso:: - - For parameter handling within a Dependency Injection Class see - :doc:`/configuration/using_parameters_in_dic`. diff --git a/routing/slash_in_parameter.rst b/routing/slash_in_parameter.rst deleted file mode 100644 index 9b56def082b..00000000000 --- a/routing/slash_in_parameter.rst +++ /dev/null @@ -1,99 +0,0 @@ -.. index:: - single: Routing; Allow / in route parameter - -.. _routing/slash_in_parameter: - -How to Allow a "/" Character in a Route Parameter -================================================= - -Sometimes, you need to compose URLs with parameters that can contain a slash -``/``. For example, consider the ``/share/{token}`` route. If the ``token`` -value contains a ``/`` character this route won't match. This is because Symfony -uses this character as separator between route parts. - -This article explains how you can modify a route definition so that placeholders -can contain the ``/`` character too. - -Configure the Route -------------------- - -By default, the Symfony Routing component requires that the parameters match -the following regular expression: ``[^/]+``. This means that all characters are -allowed except ``/``. - -You must explicitly allow ``/`` to be part of your placeholder by specifying -a more permissive regular expression for it: - -.. configuration-block:: - - .. code-block:: php-annotations - - use Symfony\Component\Routing\Annotation\Route; - - class DefaultController - { - /** - * @Route("/share/{token}", name="share", requirements={"token"=".+"}) - */ - public function share($token) - { - // ... - } - } - - .. code-block:: yaml - - # config/routes.yaml - share: - path: /share/{token} - controller: App\Controller\DefaultController::share - requirements: - token: .+ - - .. code-block:: xml - - - - - - - .+ - - - - .. code-block:: php - - // config/routes.php - use App\Controller\DefaultController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('share', '/share/{token}') - ->controller([DefaultController::class, 'share']) - ->requirements([ - 'token' => '.+', - ]) - ; - }; - -That's it! Now, the ``{token}`` parameter can contain the ``/`` character. - -.. note:: - - If the route includes the special ``{_format}`` placeholder, you shouldn't - use the ``.+`` requirement for the parameters that allow slashes. For example, - if the pattern is ``/share/{token}.{_format}`` and ``{token}`` allows any - character, the ``/share/foo/bar.json`` URL will consider ``foo/bar.json`` - as the token and the format will be empty. This can be solved by replacing the - ``.+`` requirement by ``[^.]+`` to allow any character except dots. - -.. note:: - - If the route defines several placeholders and you apply this permissive - regular expression to all of them, the results won't be the expected. For - example, if the route definition is ``/share/{path}/{token}`` and both - ``path`` and ``token`` accept ``/``, then ``path`` will contain its contents - and the token, and ``token`` will be empty. diff --git a/security.rst b/security.rst index 687e5b1b1b2..ce7c634a730 100644 --- a/security.rst +++ b/security.rst @@ -30,7 +30,7 @@ A few other important topics are discussed after. 1) Installation --------------- -In applications using :doc:`Symfony Flex `, run this command to +In applications using :ref:`Symfony Flex `, run this command to install the security feature before using it: .. code-block:: terminal @@ -124,10 +124,9 @@ command will pre-configure this for you: encoders: # use your user class name here App\Entity\User: - # bcrypt or argon2i are recommended - # argon2i is more secure, but requires PHP 7.2 or the Sodium extension - algorithm: bcrypt - cost: 12 + # Use native password encoder + # This value auto-selects the best possible hashing algorithm. + algorithm: auto .. code-block:: xml @@ -436,7 +435,11 @@ start with ``/admin``, you can: access_control: # require ROLE_ADMIN for /admin* - - { path: ^/admin, roles: ROLE_ADMIN } + - { path: '^/admin', roles: ROLE_ADMIN } + + # the 'path' value can be any valid regular expression + # (this one will match URLs like /api/post/7298 and /api/comment/528491) + - { path: ^/api/(post|comment)/\d+$, roles: ROLE_USER } .. code-block:: xml @@ -457,6 +460,10 @@ start with ``/admin``, you can: + + + @@ -474,7 +481,11 @@ start with ``/admin``, you can: ], 'access_control' => [ // require ROLE_ADMIN for /admin* - ['path' => '^/admin', 'role' => 'ROLE_ADMIN'], + ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'], + + // the 'path' value can be any valid regular expression + // (this one will match URLs like /api/post/7298 and /api/comment/528491) + ['path' => '^/api/(post|comment)/\d+$', 'roles' => 'ROLE_USER'], ], ]); @@ -492,10 +503,10 @@ the list and stops when it finds the first match: access_control: # matches /admin/users/* - - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN } + - { path: '^/admin/users', roles: ROLE_SUPER_ADMIN } # matches /admin/* except for anything matching the above rule - - { path: ^/admin, roles: ROLE_ADMIN } + - { path: '^/admin', roles: ROLE_ADMIN } .. code-block:: xml @@ -522,8 +533,8 @@ the list and stops when it finds the first match: // ... 'access_control' => [ - ['path' => '^/admin/users', 'role' => 'ROLE_SUPER_ADMIN'], - ['path' => '^/admin', 'role' => 'ROLE_ADMIN'], + ['path' => '^/admin/users', 'roles' => 'ROLE_SUPER_ADMIN'], + ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'], ], ]); @@ -724,8 +735,8 @@ If you need to get the logged in user from a service, use the Fetch the User in a Template ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In a Twig Template the user object can be accessed via the :ref:`app.user ` -key: +In a Twig Template the user object is available via the ``app.user`` variable +thanks to the :ref:`Twig global app variable `: .. code-block:: html+twig @@ -1000,7 +1011,7 @@ Authorization (Denying Access) security/acl security/force_https -.. _`frameworkextrabundle documentation`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html +.. _`FrameworkExtraBundle documentation`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html .. _`HWIOAuthBundle`: https://github.com/hwi/HWIOAuthBundle .. _`Symfony ACL bundle`: https://github.com/symfony/acl-bundle .. _`Symfony Security screencast series`: https://symfonycasts.com/screencast/symfony-security diff --git a/security/access_control.rst b/security/access_control.rst index fef4b42a728..65707e662e0 100644 --- a/security/access_control.rst +++ b/security/access_control.rst @@ -24,11 +24,11 @@ for each ``access_control`` entry, which determines whether or not a given access control should be used on this request. The following ``access_control`` options are used for matching: -* ``path`` -* ``ip`` or ``ips`` (netmasks are also supported) -* ``port`` -* ``host`` -* ``methods`` +* ``path``: a regular expression (without delimiters) +* ``ip`` or ``ips``: netmasks are also supported +* ``port``: an integer +* ``host``: a regular expression +* ``methods``: one or many methods Take the following ``access_control`` entries as an example: @@ -40,11 +40,12 @@ Take the following ``access_control`` entries as an example: security: # ... access_control: - - { path: ^/admin, roles: ROLE_USER_IP, ip: 127.0.0.1 } - - { path: ^/admin, roles: ROLE_USER_PORT, ip: 127.0.0.1, port: 8080 } - - { path: ^/admin, roles: ROLE_USER_HOST, host: symfony\.com$ } - - { path: ^/admin, roles: ROLE_USER_METHOD, methods: [POST, PUT] } - - { path: ^/admin, roles: ROLE_USER } + - { path: '^/admin', roles: ROLE_USER_IP, ip: 127.0.0.1 } + - { path: '^/admin', roles: ROLE_USER_PORT, ip: 127.0.0.1, port: 8080 } + - { path: '^/admin', roles: ROLE_USER_HOST, host: symfony\.com$ } + - { path: '^/admin', roles: ROLE_USER_METHOD, methods: [POST, PUT] } + # when defining multiple roles, users must have at least one of them (it's like an OR condition) + - { path: '^/admin', roles: [ROLE_MANAGER, ROLE_ADMIN] } .. code-block:: xml @@ -62,7 +63,8 @@ Take the following ``access_control`` entries as an example: - + + @@ -74,28 +76,29 @@ Take the following ``access_control`` entries as an example: 'access_control' => [ [ 'path' => '^/admin', - 'role' => 'ROLE_USER_IP', - 'ip' => '127.0.0.1', + 'roles' => 'ROLE_USER_IP', + 'ips' => '127.0.0.1', ], [ 'path' => '^/admin', - 'role' => 'ROLE_USER_PORT', + 'roles' => 'ROLE_USER_PORT', 'ip' => '127.0.0.1', 'port' => '8080', ], [ 'path' => '^/admin', - 'role' => 'ROLE_USER_HOST', + 'rolse' => 'ROLE_USER_HOST', 'host' => 'symfony\.com$', ], [ 'path' => '^/admin', - 'role' => 'ROLE_USER_METHOD', + 'roles' => 'ROLE_USER_METHOD', 'methods' => 'POST, PUT', ], [ 'path' => '^/admin', - 'role' => 'ROLE_USER', + // when defining multiple roles, users must have at least one of them (it's like an OR condition) + 'roles' => ['ROLE_MANAGER', 'ROLE_ADMIN'], ], ], ]); @@ -127,9 +130,10 @@ if ``ip``, ``port``, ``host`` or ``method`` are not specified for an entry, that | ``/admin/user`` | 168.0.0.1 | 80 | example.com | POST | rule #4 (``ROLE_USER_METHOD``) | The ``ip`` and ``host`` don't match the first two entries, | | | | | | | | but the third - ``ROLE_USER_METHOD`` - matches and is used. | +-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 168.0.0.1 | 80 | example.com | GET | rule #5 (``ROLE_USER``) | The ``ip``, ``host`` and ``method`` prevent the first | +| ``/admin/user`` | 168.0.0.1 | 80 | example.com | GET | rule #4 (``ROLE_MANAGER``) | The ``ip``, ``host`` and ``method`` prevent the first | | | | | | | | three entries from matching. But since the URI matches the | -| | | | | | | ``path`` pattern of the ``ROLE_USER`` entry, it is used. | +| | | | | | | ``path`` pattern, then the ``ROLE_MANAGER`` (or the | +| | | | | | | ``ROLE_ADMIN``) is used. | +-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ | ``/foo`` | 127.0.0.1 | 80 | symfony.com | POST | matches no entries | This doesn't match any ``access_control`` rules, since its | | | | | | | | URI doesn't match any of the ``path`` values. | @@ -147,9 +151,7 @@ options: * ``roles`` If the user does not have the given role, then access is denied (internally, an :class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException` is thrown). If this value is an array of multiple roles, the user must have - at least one of them (when using the default ``affirmative`` strategy in the - :ref:`Access Decision Manager `) - or all of them when using the ``unanimous`` strategy; + at least one of them. * ``allow_if`` If the expression returns false, then access is denied; @@ -157,6 +159,14 @@ options: does not match this value (e.g. ``https``), the user will be redirected (e.g. redirected from ``http`` to ``https``, or vice versa). +.. tip:: + + Behind the scenes, the array value of ``roles`` is passed as the + ``$attributes`` argument to each voter in the application with the + :class:`Symfony\\Component\\HttpFoundation\\Request` as ``$subject``. You + can learn how to use your custom attributes by reading + :ref:`security/custom-voter`. + .. tip:: If access is denied, the system will try to authenticate the user if not @@ -193,8 +203,8 @@ pattern so that it is only accessible by requests from the local server itself: access_control: # # the 'ips' option supports IP addresses and subnet masks - - { path: ^/internal, roles: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1, 192.168.0.1/24] } - - { path: ^/internal, roles: ROLE_NO_ACCESS } + - { path: '^/internal', roles: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1, 192.168.0.1/24] } + - { path: '^/internal', roles: ROLE_NO_ACCESS } .. code-block:: xml @@ -227,13 +237,13 @@ pattern so that it is only accessible by requests from the local server itself: 'access_control' => [ [ 'path' => '^/internal', - 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', + 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY', // the 'ips' option supports IP addresses and subnet masks 'ips' => ['127.0.0.1', '::1'], ], [ 'path' => '^/internal', - 'role' => 'ROLE_NO_ACCESS', + 'roles' => 'ROLE_NO_ACCESS', ], ], ]); @@ -278,7 +288,10 @@ key: access_control: - path: ^/_internal/secure - allow_if: "'127.0.0.1' == request.getClientIp() or is_granted('ROLE_ADMIN')" + # the 'role' and 'allow-if' options work like an OR expression, so + # access is granted if the expression is TRUE or the user has ROLE_ADMIN + roles: 'ROLE_ADMIN' + allow_if: "'127.0.0.1' == request.getClientIp() or request.header.has('X-Secure-Access')" .. code-block:: xml @@ -292,8 +305,11 @@ key: + + role="ROLE_ADMIN" + allow-if="'127.0.0.1' == request.getClientIp() or request.header.has('X-Secure-Access')"/> @@ -305,14 +321,23 @@ key: 'access_control' => [ [ 'path' => '^/_internal/secure', - 'allow_if' => '"127.0.0.1" == request.getClientIp() or is_granted("ROLE_ADMIN")', + // the 'role' and 'allow-if' options work like an OR expression, so + // access is granted if the expression is TRUE or the user has ROLE_ADMIN + 'roles' => 'ROLE_ADMIN', + 'allow_if' => '"127.0.0.1" == request.getClientIp() or request.header.has('X-Secure-Access')', ], ], ]); -In this case, when the user tries to access any URL starting with ``/_internal/secure``, -they will only be granted access if the IP address is ``127.0.0.1`` or if -the user has the ``ROLE_ADMIN`` role. +In this case, when the user tries to access any URL starting with +``/_internal/secure``, they will only be granted access if the IP address is +``127.0.0.1`` or a secure header, or if the user has the ``ROLE_ADMIN`` role. + +.. note:: + + Internally ``allow_if`` triggers the built-in + :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\ExpressionVoter` + as like it was part of the attributes defined in the ``roles`` option. Inside the expression, you have access to a number of different variables and functions including ``request``, which is the Symfony @@ -422,7 +447,7 @@ the user will be redirected to ``https``: 'access_control' => [ [ 'path' => '^/cart/checkout', - 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', + 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'requires_channel' => 'https', ], ], diff --git a/security/csrf.rst b/security/csrf.rst index 42db5695557..620df0f887e 100644 --- a/security/csrf.rst +++ b/security/csrf.rst @@ -108,6 +108,15 @@ this can be customized on a form-by-form basis:: // ... } +You can also customize the rendering of the CSRF form field creating a custom +:doc:`form theme ` and using ``csrf_token`` as the prefix of +the field (e.g. define ``{% block csrf_token_widget %} ... {% endblock %}`` to +customize the entire form field contents). + +.. versionadded:: 4.3 + + The ``csrf_token`` form field prefix was introduced in Symfony 4.3. + CSRF Protection in Login Forms ------------------------------ diff --git a/security/custom_authentication_provider.rst b/security/custom_authentication_provider.rst index 783b7e45e61..1a0a78bf226 100644 --- a/security/custom_authentication_provider.rst +++ b/security/custom_authentication_provider.rst @@ -100,7 +100,7 @@ is responsible for fielding requests to the firewall and calling the authenticat provider. A listener must be an instance of :class:`Symfony\\Component\\Security\\Http\\Firewall\\ListenerInterface`. A security listener should handle the -:class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent` event, and +:class:`Symfony\\Component\\HttpKernel\\Event\\RequestEvent` event, and set an authenticated token in the token storage if successful:: // src/Security/Firewall/WsseListener.php @@ -108,7 +108,7 @@ set an authenticated token in the token storage if successful:: use App\Security\Authentication\Token\WsseUserToken; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\HttpKernel\Event\GetResponseEvent; + use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; @@ -125,7 +125,7 @@ set an authenticated token in the token storage if successful:: $this->authenticationManager = $authenticationManager; } - public function handle(GetResponseEvent $event) + public function handle(RequestEvent $event) { $request = $event->getRequest(); @@ -641,4 +641,3 @@ in the factory and consumed or passed to the other classes in the container. .. _`WSSE`: http://www.xml.com/pub/a/2003/12/17/dive.html .. _`nonce`: https://en.wikipedia.org/wiki/Cryptographic_nonce -.. _`timing attacks`: https://en.wikipedia.org/wiki/Timing_attack diff --git a/security/expressions.rst b/security/expressions.rst index 3b0ad7e0851..595a81a9e28 100644 --- a/security/expressions.rst +++ b/security/expressions.rst @@ -52,22 +52,24 @@ Inside the expression, you have access to a number of variables: Additionally, you have access to a number of functions inside the expression: -``is_authenticated`` +``is_authenticated()`` Returns ``true`` if the user is authenticated via "remember-me" or authenticated "fully" - i.e. returns true if the user is "logged in". -``is_anonymous`` - Equal to using ``IS_AUTHENTICATED_ANONYMOUSLY`` with the ``isGranted()`` function. -``is_remember_me`` +``is_anonymous()`` + Returns ``true`` if the user is anonymous. That is, the firewall confirms that it + does not know this user's identity. This is different from ``IS_AUTHENTICATED_ANONYMOUSLY``, + which is granted to *all* users, including authenticated ones. +``is_remember_me()`` Similar, but not equal to ``IS_AUTHENTICATED_REMEMBERED``, see below. -``is_fully_authenticated`` - Similar, but not equal to ``IS_AUTHENTICATED_FULLY``, see below. -``is_granted`` +``is_fully_authenticated()`` + Equal to checking if the user has the ``IS_AUTHENTICATED_FULLY`` role. +``is_granted()`` Checks if the user has the given permission. Optionally accepts a second argument with the object where permission is checked on. It's equivalent to using the :doc:`isGranted() method ` from the authorization checker service. -.. sidebar:: ``is_remember_me`` is different than checking ``IS_AUTHENTICATED_REMEMBERED`` +.. sidebar:: ``is_remember_me()`` is different than checking ``IS_AUTHENTICATED_REMEMBERED`` The ``is_remember_me()`` and ``is_fully_authenticated()`` functions are *similar* to using ``IS_AUTHENTICATED_REMEMBERED`` and ``IS_AUTHENTICATED_FULLY`` @@ -90,7 +92,7 @@ Additionally, you have access to a number of functions inside the expression: Here, ``$access1`` and ``$access2`` will be the same value. Unlike the behavior of ``IS_AUTHENTICATED_REMEMBERED`` and ``IS_AUTHENTICATED_FULLY``, the ``is_remember_me()`` function *only* returns true if the user is authenticated - via a remember-me cookie and ``is_fully_authenticated`` *only* returns + via a remember-me cookie and ``is_fully_authenticated()`` *only* returns true if the user has actually logged in during this session (i.e. is full-fledged). diff --git a/security/force_https.rst b/security/force_https.rst index bf69d7ad995..98ad24176d7 100644 --- a/security/force_https.rst +++ b/security/force_https.rst @@ -23,10 +23,10 @@ access control: # ... access_control: - - { path: ^/secure, roles: ROLE_ADMIN, requires_channel: https } - - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } + - { path: '^/secure', roles: ROLE_ADMIN, requires_channel: https } + - { path: '^/login', roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } # catch all other URLs - - { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } + - { path: '^/', roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } .. code-block:: xml @@ -62,7 +62,7 @@ access control: 'access_control' => [ [ 'path' => '^/secure', - 'role' => 'ROLE_ADMIN', + 'roles' => 'ROLE_ADMIN', 'requires_channel' => 'https', ], [ @@ -85,8 +85,10 @@ like ``requires_channel: '%env(SECURE_SCHEME)%'``. In your ``.env`` file, set See :doc:`/security/access_control` for more details about ``access_control`` in general. -It is also possible to specify using HTTPS in the routing configuration, -see :doc:`/routing/scheme` for more details. +.. note:: + + An alternative way to enforce HTTP or HTTPS is to use + :ref:`the scheme option ` of a route or group of routes. .. note:: diff --git a/security/form_login_setup.rst b/security/form_login_setup.rst index bd6a8c65bb5..a6555dcdce9 100644 --- a/security/form_login_setup.rst +++ b/security/form_login_setup.rst @@ -32,6 +32,9 @@ and your generated code may be slightly different: Choose a name for the controller class (e.g. SecurityController) [SecurityController]: > SecurityController + Do you want to generate a '/logout' URL? (yes/no) [yes]: + > yes + created: src/Security/LoginFormAuthenticator.php updated: config/packages/security.yaml created: src/Controller/SecurityController.php @@ -421,3 +424,36 @@ deal with this low level session variable. However, the can be used to read (like in the example above) or set this value manually. .. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html +======= + // ... + 'access_control' => [ + ['path' => '^/login', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'], + ['path' => '^/', 'roles' => 'ROLE_ADMIN'], + ], + +3. Be Sure check_path Is Behind a Firewall +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Next, make sure that your ``check_path`` URL (e.g. ``/login``) is behind +the firewall you're using for your form login (in this example, the single +firewall matches *all* URLs, including ``/login``). If ``/login`` +doesn't match any firewall, you'll receive a ``Unable to find the controller +for path "/login"`` exception. + +4. Multiple Firewalls Don't Share the Same Security Context +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you're using multiple firewalls and you authenticate against one firewall, +you will *not* be authenticated against any other firewalls automatically. +Different firewalls are like different security systems. To do this you have +to explicitly specify the same :ref:`reference-security-firewall-context` +for different firewalls. But usually for most applications, having one +main firewall is enough. + +5. Routing Error Pages Are not Covered by Firewalls +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As routing is done *before* security, 404 error pages are not covered by +any firewall. This means you can't check for security or even access the +user object on these pages. See :doc:`/controller/error_pages` +for more details. diff --git a/security/guard_authentication.rst b/security/guard_authentication.rst index 0c292241e98..205c52b286e 100644 --- a/security/guard_authentication.rst +++ b/security/guard_authentication.rst @@ -509,6 +509,5 @@ Frequently Asked Questions question) or use the ``User`` object from FOSUserBundle and create your own authenticator(s) (just like in this article). -.. _`must be quoted with backticks`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#quoting-reserved-words .. _`Social Authentication`: https://github.com/knpuniversity/oauth2-client-bundle#authenticating-with-guard .. _`HWIOAuthBundle`: https://github.com/hwi/HWIOAuthBundle diff --git a/security/impersonating_user.rst b/security/impersonating_user.rst index cbd7fd03239..a4f4e724e27 100644 --- a/security/impersonating_user.rst +++ b/security/impersonating_user.rst @@ -98,11 +98,17 @@ to show a link to exit impersonation: Finding the Original User ------------------------- +.. versionadded:: 4.3 + + The ``SwitchUserToken`` class was introduced in Symfony 4.3. + In some cases, you may need to get the object that represents the impersonator -user rather than the impersonated user. Use the following snippet to iterate -over the user's roles until you find one that is a ``SwitchUserRole`` object:: +user rather than the impersonated user. When a user is impersonated the token +stored in the token storage will be a ``SwitchUserToken`` instance. Use the +following snippet to obtain the original token which gives you access to +the impersonator user:: - use Symfony\Component\Security\Core\Role\SwitchUserRole; + use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Security; // ... @@ -119,14 +125,13 @@ over the user's roles until you find one that is a ``SwitchUserRole`` object:: { // ... - if ($this->security->isGranted('ROLE_PREVIOUS_ADMIN')) { - foreach ($this->security->getToken()->getRoles() as $role) { - if ($role instanceof SwitchUserRole) { - $impersonatorUser = $role->getSource()->getUser(); - break; - } - } + $token = $this->security->getToken(); + + if ($token instanceof SwitchUserToken) { + $impersonatorUser = $token->getOriginalToken()->getUser(); } + + // ... } } diff --git a/security/ldap.rst b/security/ldap.rst index 73a93f4e2be..2bb3eee95bd 100644 --- a/security/ldap.rst +++ b/security/ldap.rst @@ -37,7 +37,7 @@ This means that the following scenarios will work: Installation ------------ -In applications using :doc:`Symfony Flex `, run this command to +In applications using :ref:`Symfony Flex `, run this command to install the Ldap component before using it: .. code-block:: terminal diff --git a/security/multiple_guard_authenticators.rst b/security/multiple_guard_authenticators.rst index b71d5186817..53e8972557c 100644 --- a/security/multiple_guard_authenticators.rst +++ b/security/multiple_guard_authenticators.rst @@ -108,9 +108,9 @@ the solution is to split the configuration into two separate firewalls: authenticators: - App\Security\LoginFormAuthenticator access_control: - - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/api, roles: ROLE_API_USER } - - { path: ^/, roles: ROLE_USER } + - { path: '^/login', roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: '^/api', roles: ROLE_API_USER } + - { path: '^/', roles: ROLE_USER } .. code-block:: xml @@ -168,8 +168,8 @@ the solution is to split the configuration into two separate firewalls: ], ], 'access_control' => [ - ['path' => '^/login', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'], - ['path' => '^/api', 'role' => 'ROLE_API_USER'], - ['path' => '^/', 'role' => 'ROLE_USER'], + ['path' => '^/login', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'], + ['path' => '^/api', 'roles' => 'ROLE_API_USER'], + ['path' => '^/', 'roles' => 'ROLE_USER'], ], ]); diff --git a/security/named_encoders.rst b/security/named_encoders.rst index 1160384bbe5..462ade3a1da 100644 --- a/security/named_encoders.rst +++ b/security/named_encoders.rst @@ -109,7 +109,7 @@ be done with named encoders: If you are running PHP 7.2+ or have the `libsodium`_ extension installed, then the recommended hashing algorithm to use is - :ref:`Argon2i `. + :ref:`Sodium `. This creates an encoder named ``harsh``. In order for a ``User`` instance to use it, the class must implement diff --git a/security/voters.rst b/security/voters.rst index b0fd95411fc..05415c1f39f 100644 --- a/security/voters.rst +++ b/security/voters.rst @@ -1,6 +1,8 @@ .. index:: single: Security; Data Permission Voters +.. _security/custom-voter: + How to Use Voters to Check User Permissions =========================================== @@ -19,7 +21,8 @@ How Symfony Uses Voters In order to use voters, you have to understand how Symfony works with them. All voters are called each time you use the ``isGranted()`` method on Symfony's authorization checker or call ``denyAccessUnlessGranted()`` in a controller (which -uses the authorization checker). +uses the authorization checker), or by +:ref:`access controls `. Ultimately, Symfony takes the responses from all voters and makes the final decision (to allow or deny access to the resource) according to the strategy defined diff --git a/serializer.rst b/serializer.rst index 3812b4d285d..2466f525d92 100644 --- a/serializer.rst +++ b/serializer.rst @@ -14,8 +14,8 @@ its philosophy and the normalizers and encoders terminology. Installation ------------ -In applications using :doc:`Symfony Flex `, run this command to -install the serializer before using it: +In applications using :ref:`Symfony Flex `, run this command to +install the ``serializer`` :ref:`Symfony pack ` before using it: .. code-block:: terminal @@ -228,7 +228,6 @@ take a look at how this bundle works. serializer/custom_encoders serializer/custom_normalizer -.. _`APCu`: https://github.com/krakjoe/apcu .. _`API Platform`: https://api-platform.com .. _`JSON-LD`: http://json-ld.org .. _`Hydra Core Vocabulary`: http://hydra-cg.com diff --git a/serializer/custom_encoders.rst b/serializer/custom_encoders.rst index f639294f44d..5bb78def4e4 100644 --- a/serializer/custom_encoders.rst +++ b/serializer/custom_encoders.rst @@ -54,8 +54,8 @@ create your own encoder that uses the ``supportsEncoding`` method, make sure to implement ``Symfony\Component\Serializer\Encoder\ContextAwareDecoderInterface`` or ``Symfony\Component\Serializer\Encoder\ContextAwareEncoderInterface`` accordingly. - - + + Registering it in your app -------------------------- @@ -69,5 +69,3 @@ that's done automatically! to register your class as a service and tag it with ``serializer.encoder``. Now you'll be able to serialize and deserialize YAML! - -.. _tracker: https://github.com/symfony/symfony/issues diff --git a/serializer/custom_normalizer.rst b/serializer/custom_normalizer.rst index a85f70e4eac..9f2e50fdcf1 100644 --- a/serializer/custom_normalizer.rst +++ b/serializer/custom_normalizer.rst @@ -21,10 +21,10 @@ to customize the normalized data. To do that, leverage the ``ObjectNormalizer``: use App\Entity\Topic; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; - class TopicNormalizer implements NormalizerInterface + class TopicNormalizer implements ContextAwareNormalizerInterface { private $router; private $normalizer; diff --git a/service_container.rst b/service_container.rst index d8a354d59a0..fc5405c1d7e 100644 --- a/service_container.rst +++ b/service_container.rst @@ -460,23 +460,29 @@ Service Parameters ------------------ In addition to holding service objects, the container also holds configuration, -called ``parameters``. To create a parameter, add it under the ``parameters`` key -and reference it with the ``%parameter_name%`` syntax: +called **parameters**. The main article about Symfony configuration explains the +:ref:`configuration parameters ` in detail and shows +all their types (string, boolean, array, binary and PHP constant parameters). + +However, there is another type of parameter related to services. In YAML config, +any string which starts with ``@`` is considered as the ID of a service, instead +of a regular string. In XML config, use the ``type="service"`` type for the +parameter and in PHP config use the ``Reference`` class: .. configuration-block:: .. code-block:: yaml # config/services.yaml - parameters: - admin_email: manager@example.com - services: - # ... + App\Service\MessageGenerator: + # this is not a string, but a reference to a service called 'logger' + arguments: ['@logger'] - App\Updates\SiteUpdateManager: - arguments: - $adminEmail: '%admin_email%' + # if the value of a string parameter starts with '@', you need to escape + # it by adding another '@' so Symfony doesn't consider it a service + # (this will be parsed as the string '@securepassword') + mailer_password: '@@securepassword' .. code-block:: xml @@ -487,15 +493,9 @@ and reference it with the ``%parameter_name%`` syntax: xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> - - manager@example.com - - - - - - %admin_email% + + @@ -503,45 +503,38 @@ and reference it with the ``%parameter_name%`` syntax: .. code-block:: php // config/services.php - use App\Updates\SiteUpdateManager; - $container->setParameter('admin_email', 'manager@example.com'); - - $container->autowire(SiteUpdateManager::class) - // ... - ->setArgument('$adminEmail', '%admin_email%'); + use App\Service\MessageGenerator; + use Symfony\Component\DependencyInjection\Reference; -Actually, once you define a parameter, it can be referenced via the -``%parameter_name%`` syntax in *any* other configuration file. Many parameters -are defined in the ``config/services.yaml`` file. + $container->autowire(MessageGenerator::class) + ->setAutoconfigured(true) + ->setPublic(false) + ->setArgument(0, new Reference('logger')); -You can then fetch the parameter in the service:: +Working with container parameters is straightforward using the container's +accessor methods for parameters:: - class SiteUpdateManager - { - // ... + // checks if a parameter is defined (parameter names are case-sensitive) + $container->hasParameter('mailer.transport'); - private $adminEmail; + // gets value of a parameter + $container->getParameter('mailer.transport'); - public function __construct($adminEmail) - { - $this->adminEmail = $adminEmail; - } - } + // adds a new parameter + $container->setParameter('mailer.transport', 'sendmail'); -You can also fetch parameters directly from the container:: - - public function new() - { - // ... +.. caution:: - // this shortcut ONLY works if you extend the base AbstractController - $adminEmail = $this->getParameter('admin_email'); + The used ``.`` notation is a + :ref:`Symfony convention ` to make parameters + easier to read. Parameters are flat key-value elements, they can't + be organized into a nested array - // this is the equivalent code of the previous shortcut: - // $adminEmail = $this->container->get('parameter_bag')->get('admin_email'); - } +.. note:: -For more info about parameters, see :doc:`/service_container/parameters`. + You can only set a parameter before the container is compiled, not at run-time. + To learn more about compiling the container see + :doc:`/components/dependency_injection/compilation`. .. _services-wire-specific-service: @@ -724,39 +717,6 @@ argument for *any* service defined in this file! You can bind arguments by name The ``bind`` config can also be applied to specific services or when loading many services at once (i.e. :ref:`service-psr4-loader`). -Getting Container Parameters as a Service -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If some service or controller needs lots of container parameters, there's an -easier alternative to binding all of them with the ``services._defaults.bind`` -option. Type-hint any of its constructor arguments with the -:class:`Symfony\\Component\\DependencyInjection\\ParameterBag\\ParameterBagInterface` -or the new :class:`Symfony\\Component\\DependencyInjection\\ParameterBag\\ContainerBagInterface` -and the service will get all container parameters in a -:class:`Symfony\\Component\\DependencyInjection\\ParameterBag\\ParameterBag` object:: - - // src/Service/MessageGenerator.php - // ... - - use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; - - class MessageGenerator - { - private $params; - - public function __construct(ParameterBagInterface $params) - { - $this->params = $params; - } - - public function someMethod() - { - // get any param from $this->params, which stores all container parameters - $sender = $this->params->get('mailer_sender'); - // ... - } - } - .. _services-autowire: The autowire Option @@ -1081,6 +1041,5 @@ Learn more /service_container/* -.. _`service-oriented architecture`: https://en.wikipedia.org/wiki/Service-oriented_architecture .. _`glob pattern`: https://en.wikipedia.org/wiki/Glob_(programming) .. _`Symfony Fundamentals screencast series`: https://symfonycasts.com/screencast/symfony-fundamentals diff --git a/service_container/3.3-di-changes.rst b/service_container/3.3-di-changes.rst index 2932436a18a..7aed2e63980 100644 --- a/service_container/3.3-di-changes.rst +++ b/service_container/3.3-di-changes.rst @@ -373,12 +373,12 @@ create the class:: // ... use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\FilterResponseEvent; + use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; class SetHeaderSusbcriber implements EventSubscriberInterface { - public function onKernelResponse(FilterResponseEvent $event) + public function onKernelResponse(ResponseEvent $event) { $event->getResponse()->headers->set('X-SYMFONY-3.3', 'Less config'); } diff --git a/service_container/alias_private.rst b/service_container/alias_private.rst index 5de1d1e5290..c1e9da0e790 100644 --- a/service_container/alias_private.rst +++ b/service_container/alias_private.rst @@ -146,6 +146,69 @@ This means that when using the container directly, you can access the # ... app.mailer: '@App\Mail\PhpMailer' +Deprecating Service Aliases +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you decide to deprecate the use of a service alias (because it is outdated +or you decided not to maintain it anymore), you can deprecate its definition: + +.. configuration-block:: + + .. code-block:: yaml + + app.mailer: + alias: '@App\Mail\PhpMailer' + + # this will display a generic deprecation message... + deprecated: true + + # ...but you can also define a custom deprecation message + deprecated: 'The "%alias_id%" alias is deprecated. Don\'t use it anymore.' + + .. code-block:: xml + + + + + + + + + + + The "%alias_id%" service alias is deprecated. Don't use it anymore. + + + + + .. code-block:: php + + $container + ->setAlias('app.mailer', 'App\Mail\PhpMailer') + + // this will display a generic deprecation message... + ->setDeprecated(true) + + // ...but you can also define a custom deprecation message + ->setDeprecated( + true, + 'The "%alias_id%" service alias is deprecated. Don\'t use it anymore.' + ) + ; + +Now, every time this service alias is used, a deprecation warning is triggered, +advising you to stop or to change your uses of that alias. + +The message is actually a message template, which replaces occurrences of the +``%alias_id%`` placeholder by the service alias id. You **must** have at least +one occurrence of the ``%alias_id%`` placeholder in your template. + +.. versionadded:: 4.3 + + The ``deprecated`` option for service aliases was introduced in Symfony 4.3. + Anonymous Services ------------------ diff --git a/service_container/autowiring.rst b/service_container/autowiring.rst index 26c8f55ed0d..0e1fad1aed1 100644 --- a/service_container/autowiring.rst +++ b/service_container/autowiring.rst @@ -549,6 +549,5 @@ Public and Reusable Bundles Public bundles should explicitly configure their services and not rely on autowiring. -.. _Rapid Application Development: https://en.wikipedia.org/wiki/Rapid_application_development .. _ROT13: https://en.wikipedia.org/wiki/ROT13 .. _service definition prototype: https://symfony.com/blog/new-in-symfony-3-3-psr-4-based-service-discovery diff --git a/service_container/configurators.rst b/service_container/configurators.rst index 6185d381cb9..29b06bdea69 100644 --- a/service_container/configurators.rst +++ b/service_container/configurators.rst @@ -184,6 +184,79 @@ all the classes are already loaded as services. All you need to do is specify th $container->getDefinition(GreetingCardManager::class) ->setConfigurator([new Reference(EmailConfigurator::class), 'configure']); +.. _configurators-invokable: + +.. versionadded:: 4.3 + + Invokable configurators for services were introduced in Symfony 4.3. + +Services can be configured via invokable configurators (replacing the +``configure()`` method with ``__invoke()``) by omitting the method name, just as +routes can reference :ref:`invokable controllers `. + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + # registers all classes as services, including App\Mail\EmailConfigurator + App\: + resource: '../src/*' + # ... + + # override the services to set the configurator + App\Mail\NewsletterManager: + configurator: '@App\Mail\EmailConfigurator' + + App\Mail\GreetingCardManager: + configurator: '@App\Mail\EmailConfigurator' + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + use App\Mail\GreetingCardManager; + use App\Mail\NewsletterManager; + use Symfony\Component\DependencyInjection\Definition; + use Symfony\Component\DependencyInjection\Reference; + + // Same as before + $definition = new Definition(); + + $definition->setAutowired(true); + + $this->registerClasses($definition, 'App\\', '../src/*'); + + $container->getDefinition(NewsletterManager::class) + ->setConfigurator(new Reference(EmailConfigurator::class)); + + $container->getDefinition(GreetingCardManager::class) + ->setConfigurator(new Reference(EmailConfigurator::class)); + That's it! When requesting the ``App\Mail\NewsletterManager`` or ``App\Mail\GreetingCardManager`` service, the created instance will first be passed to the ``EmailConfigurator::configure()`` method. diff --git a/service_container/factories.rst b/service_container/factories.rst index c7901f6924b..2c926bab59c 100644 --- a/service_container/factories.rst +++ b/service_container/factories.rst @@ -144,6 +144,73 @@ Configuration of the service container then looks like this: 'createNewsletterManager', ]); +.. _factories-invokable: + +Suppose you now change your factory method to ``__invoke()`` so that your +factory service can be used as a callback:: + + class InvokableNewsletterManagerFactory + { + public function __invoke() + { + $newsletterManager = new NewsletterManager(); + + // ... + + return $newsletterManager; + } + } + +.. versionadded:: 4.3 + + Invokable factories for services were introduced in Symfony 4.3. + +Services can be created and configured via invokable factories by omitting the +method name, just as routes can reference +:ref:`invokable controllers `. + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\Email\NewsletterManager: + class: App\Email\NewsletterManager + factory: '@App\Email\NewsletterManagerFactory' + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + use App\Email\NewsletterManager; + use App\Email\NewsletterManagerFactory; + use Symfony\Component\DependencyInjection\Reference; + + // ... + $container->register(NewsletterManager::class, NewsletterManager::class) + ->setFactory(new Reference(NewsletterManagerFactory::class)); + .. _factories-passing-arguments-factory-method: Passing Arguments to the Factory Method diff --git a/service_container/parameters.rst b/service_container/parameters.rst deleted file mode 100644 index c1c7d570447..00000000000 --- a/service_container/parameters.rst +++ /dev/null @@ -1,351 +0,0 @@ -.. index:: - single: DependencyInjection; Parameters - -Introduction to Parameters -========================== - -You can define parameters in the service container which can then be used -directly or as part of service definitions. This can help to separate out -values that you will want to change more regularly. - -Parameters in Configuration Files ---------------------------------- - -Use the ``parameters`` section of a config file to set parameters: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - parameters: - mailer.transport: sendmail - - .. code-block:: xml - - - - - - - sendmail - - - - .. code-block:: php - - // config/services.php - $container->setParameter('mailer.transport', 'sendmail'); - -You can refer to parameters elsewhere in any config file by surrounding them -with percent (``%``) signs, e.g. ``%mailer.transport%``. One use for this is -to inject the values into your services. This allows you to configure different -versions of services between applications or multiple services based on the -same class but configured differently within a single application. You could -inject the choice of mail transport into the ``Mailer`` class directly. But -declaring it as a parameter makes it easier to change rather than being tied up -and hidden with the service definition: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - parameters: - mailer.transport: sendmail - - services: - App\Service\Mailer: - arguments: ['%mailer.transport%'] - - .. code-block:: xml - - - - - - - sendmail - - - - - %mailer.transport% - - - - - .. code-block:: php - - // config/services.php - use App\Mailer; - - $container->setParameter('mailer.transport', 'sendmail'); - - $container->register(Mailer::class) - ->addArgument('%mailer.transport%'); - -.. caution:: - - The values between ``parameter`` tags in XML configuration files are - not trimmed. - - This means that the following configuration sample will have the value - ``\n sendmail\n``: - - .. code-block:: xml - - - sendmail - - - In some cases (for constants or class names), this could throw errors. - In order to prevent this, you must always inline your parameters as - follow: - - .. code-block:: xml - - sendmail - -.. note:: - - If you use a string that starts with ``@`` or has ``%`` anywhere in it, you - need to escape it by adding another ``@`` or ``%``: - - .. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - parameters: - # This will be parsed as string '@securepass' - mailer_password: '@@securepass' - - # Parsed as http://symfony.com/?foo=%s&bar=%d - url_pattern: 'http://symfony.com/?foo=%%s&bar=%%d' - - .. code-block:: xml - - - - - @securepass - - - http://symfony.com/?foo=%%s&bar=%%d - - - .. code-block:: php - - // config/services.php - // the @ symbol does NOT need to be escaped in XML - $container->setParameter('mailer_password', '@securepass'); - - // But % does need to be escaped - $container->setParameter('url_pattern', 'http://symfony.com/?foo=%%s&bar=%%d'); - -Getting and Setting Container Parameters in PHP ------------------------------------------------ - -Working with container parameters is straightforward using the container's -accessor methods for parameters:: - - // checks if a parameter is defined (parameter names are case-sensitive) - $container->hasParameter('mailer.transport'); - - // gets value of a parameter - $container->getParameter('mailer.transport'); - - // adds a new parameter - $container->setParameter('mailer.transport', 'sendmail'); - -.. caution:: - - The used ``.`` notation is a - :ref:`Symfony convention ` to make parameters - easier to read. Parameters are flat key-value elements, they can't - be organized into a nested array - -.. note:: - - You can only set a parameter before the container is compiled: not at run-time. - To learn more about compiling the container see - :doc:`/components/dependency_injection/compilation`. - -.. _component-di-parameters-array: - -Array Parameters ----------------- - -Parameters do not need to be flat strings, they can also contain array values. -For the XML format, you need to use the ``type="collection"`` attribute -for all parameters that are arrays. - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - parameters: - my_mailer.gateways: [mail1, mail2, mail3] - - my_multilang.language_fallback: - en: - - en - - fr - fr: - - fr - - en - - .. code-block:: xml - - - - - - - - mail1 - mail2 - mail3 - - - - - en - fr - - - - fr - en - - - - - - .. code-block:: php - - // config/services.php - $container->setParameter('my_mailer.gateways', ['mail1', 'mail2', 'mail3']); - $container->setParameter('my_multilang.language_fallback', [ - 'en' => ['en', 'fr'], - 'fr' => ['fr', 'en'], - ]); - -Environment Variables and Dynamic Values ----------------------------------------- - -See :ref:`config-env-vars`. - -.. _component-di-parameters-constants: - -Constants as Parameters ------------------------ - -Setting PHP constants as parameters is also supported: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - parameters: - global.constant.value: !php/const GLOBAL_CONSTANT - my_class.constant.value: !php/const My_Class::CONSTANT_NAME - - .. code-block:: xml - - - - - - - GLOBAL_CONSTANT - My_Class::CONSTANT_NAME - - - - .. code-block:: php - - // config/services.php - $container->setParameter('global.constant.value', GLOBAL_CONSTANT); - $container->setParameter('my_class.constant.value', My_Class::CONSTANT_NAME); - -Binary Values as Parameters ---------------------------- - -If the value of a container parameter is a binary value, set it as a base64 -encoded value in YAML and XML configs and use the escape sequences in PHP: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - parameters: - some_parameter: !!binary VGhpcyBpcyBhIEJlbGwgY2hhciAH - - .. code-block:: xml - - - - - - - VGhpcyBpcyBhIEJlbGwgY2hhciAH - - - - .. code-block:: php - - // config/services.php - $container->setParameter('some_parameter', 'This is a Bell char \x07'); - -PHP Keywords in XML -------------------- - -By default, ``true``, ``false`` and ``null`` in XML are converted to the -PHP keywords (respectively ``true``, ``false`` and ``null``): - -.. code-block:: xml - - - false - - - - -To disable this behavior, use the ``string`` type: - -.. code-block:: xml - - - true - - - - -.. note:: - - This is not available for YAML and PHP, because they already have built-in - support for the PHP keywords. diff --git a/service_container/service_decoration.rst b/service_container/service_decoration.rst index 80c16374def..0904f9cb500 100644 --- a/service_container/service_decoration.rst +++ b/service_container/service_decoration.rst @@ -284,4 +284,4 @@ The generated code will be the following:: $this->services[Foo::class] = new Baz(new Bar(new Foo())); -.. _decorator pattern: https://en.wikipedia.org/wiki/Decorator_pattern +.. _`Decorator pattern`: https://en.wikipedia.org/wiki/Decorator_pattern diff --git a/service_container/service_subscribers_locators.rst b/service_container/service_subscribers_locators.rst index 150f9bef638..952e61fee77 100644 --- a/service_container/service_subscribers_locators.rst +++ b/service_container/service_subscribers_locators.rst @@ -243,8 +243,8 @@ Defining a Service Locator -------------------------- To manually define a service locator, create a new service definition and add -the ``container.service_locator`` tag to it. Use its ``arguments`` option to -include as many services as needed in it. +the ``container.service_locator`` tag to it. Use the first argument of the +service definition to pass a collection of services to the service locator: .. configuration-block:: @@ -262,6 +262,13 @@ include as many services as needed in it. # add the following tag to the service definition: # tags: ['container.service_locator'] + # if the element has no key, the ID of the original service is used + app.another_command_handler_locator: + class: Symfony\Component\DependencyInjection\ServiceLocator + arguments: + - + - '@app.command_handler.baz' + .. code-block:: xml @@ -276,6 +283,8 @@ include as many services as needed in it. + + + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; + use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; + + $container->register(App\Handler\One::class) + ->addTag('app.handler', ['key' => 'handler_one']); + + $container->register(App\Handler\Two::class) + ->addTag('app.handler', ['key' => 'handler_two']); + + $container->register(App\Handler\HandlerCollection::class) + // inject all services tagged with app.handler as first argument + ->addArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('app.handler', 'key'))); + +Inside this locator you can retrieve services by index using the value of the +``key`` attribute. For example, to get the ``App\Handler\Two`` service:: + + // src/Handler/HandlerCollection.php + namespace App\Handler; + + use Symfony\Component\DependencyInjection\ServiceLocator; + + class HandlerCollection + { + public function __construct(ServiceLocator $locator) + { + $handlerTwo = $locator->get('handler_two'): + } + + // ... + } + +Instead of defining the index in the service definition, you can return its +value in a method called ``getDefaultIndexName()`` inside the class associated +to the service:: + + // src/Handler/One.php + namespace App\Handler; + + class One + { + public static function getDefaultIndexName(): string + { + return 'handler_one'; + } + + // ... + } + +If you prefer to use another method name, add a ``default_index_method`` +attribute to the locator service defining the name of this custom method: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\HandlerCollection: + arguments: [!tagged_locator { tag: 'app.handler', default_index_method: 'myOwnMethodName' }] + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + // ... + + $container->register(App\HandlerCollection::class) + ->addArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('app.handler', null, 'myOwnMethodName'))); Service Subscriber Trait ------------------------ @@ -462,3 +627,5 @@ and compose your services with them:: When creating these helper traits, the service id cannot be ``__METHOD__`` as this will include the trait name, not the class name. Instead, use ``__CLASS__.'::'.__FUNCTION__`` as the service id. + +.. _`Command pattern`: https://en.wikipedia.org/wiki/Command_pattern diff --git a/session/locale_sticky_session.rst b/session/locale_sticky_session.rst index c5b1a6a0582..f8caef23370 100644 --- a/session/locale_sticky_session.rst +++ b/session/locale_sticky_session.rst @@ -21,7 +21,7 @@ correct locale however you want:: namespace App\EventSubscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\GetResponseEvent; + use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; class LocaleSubscriber implements EventSubscriberInterface @@ -33,7 +33,7 @@ correct locale however you want:: $this->defaultLocale = $defaultLocale; } - public function onKernelRequest(GetResponseEvent $event) + public function onKernelRequest(RequestEvent $event) { $request = $event->getRequest(); if (!$request->hasPreviousSession()) { diff --git a/setup.rst b/setup.rst index f4d3d7a85ee..a4937e7b6c5 100644 --- a/setup.rst +++ b/setup.rst @@ -10,57 +10,83 @@ Installing & Setting up the Symfony Framework Do you prefer video tutorials? Check out the `Stellar Development with Symfony`_ screencast series. -Installing Symfony ------------------- +.. _symfony-tech-requirements: -Before creating your first Symfony application, make sure to meet the following -requirements: +Technical Requirements +---------------------- -* Your server has PHP 7.1 or higher installed (and :doc:`these PHP extensions ` - which are installed and enabled by default by PHP); -* You have `installed Composer`_, which is used to install PHP packages; -* You have installed the :doc:`Symfony local web server `, - which provides all the tools you need to develop your application locally. +Before creating your first Symfony application you must: -Once these requirements are installed, open your terminal and run any of these -commands to create the Symfony application: +* Install PHP 7.1 or higher and these PHP extensions (which are installed and + enabled by default in most PHP 7 installations): `Ctype`_, `iconv`_, `JSON`_, + `PCRE`_, `Session`_, `SimpleXML`_, and `Tokenizer`_; +* `Install Composer`_, which is used to install PHP packages; +* `Install Symfony`_, which creates in your computer a binary called ``symfony`` + that provides all the tools you need to develop your application locally. + +The ``symfony`` binary provides a tool to check if your computer meets these +requirements. Open your console terminal and run this command: + +.. code-block:: terminal + + $ symfony check:requirements + +.. _creating-symfony-applications: + +Creating Symfony Applications +----------------------------- + +Open your console terminal and run any of these commands to create a new Symfony +application: .. code-block:: terminal # run this if you are building a traditional web application - $ symfony new --full my_project + $ symfony new --full my_project_name # run this if you are building a microservice, console application or API - $ symfony new my-project + $ symfony new my_project_name The only difference between these two commands is the number of packages -installed. The ``--full`` option installs all the packages that you usually -need to build web applications. Therefore, the installation size will be much bigger. +installed by default. The ``--full`` option installs all the packages that you +usually need to build web applications, so the installation size will be bigger. -Both commands will create a new ``my-project/`` directory, download some -dependencies into it and even generate the basic directories and files you'll -need to get started. In other words, your new app is ready! +If you can't or don't want to `install Symfony`_ for any reason, run these +commands to create the new Symfony application using Composer: -.. seealso:: +.. code-block:: terminal - If you can't use the ``symfony`` command provided by the Symfony local web - server, use the alternative installation commands based on Composer and - displayed on the `Symfony download page`_. + # run this if you are building a traditional web application + $ composer create-project symfony/website-skeleton my_project_name -Running your Symfony Application --------------------------------- + # run this if you are building a microservice, console application or API + $ composer create-project symfony/skeleton my_project_name + +No matter which command you run to create the Symfony application. All of them +will create a new ``my_project_name/`` directory, download some dependencies +into it and even generate the basic directories and files you'll need to get +started. In other words, your new application is ready! + +.. note:: + + The project's cache and logs directory (by default, ``/var/cache/`` + and ``/var/log/``) must be writable by the web server. If you have + any issue, read how to :doc:`set up permissions for Symfony applications `. + +Running Symfony Applications +---------------------------- On production, you should use a web server like Nginx or Apache (see :doc:`configuring a web server to run Symfony `). But for development, it's more convenient to use the -:doc:`Symfony Local Web Server ` installed earlier. +:doc:`local web server ` provided by Symfony. This local server provides support for HTTP/2, TLS/SSL, automatic generation of security certificates and many other features. It works with any PHP application, not only Symfony projects, so it's a very useful development tool. -Open your terminal, move into your new project directory and start the local web -server as follows: +Open your console terminal, move into your new project directory and start the +local web server as follows: .. code-block:: terminal @@ -71,42 +97,15 @@ Open your browser and navigate to ``http://localhost:8000/``. If everything is working, you'll see a welcome page. Later, when you are finished working, stop the server by pressing ``Ctrl+C`` from your terminal. -.. tip:: - - If you're having any problems running Symfony, your system may be missing - some technical requirements. Use the :doc:`Symfony Requirements Checker ` - tool to make sure your system is set up. - -.. note:: - - If you want to use a virtual machine (VM) with Vagrant, check out - :doc:`Homestead `. - -Storing your Project in git ---------------------------- - -Storing your project in services like GitHub, GitLab and Bitbucket works like with -any other code project! Init a new repository with ``Git`` and you are ready to push -to your remote: - -.. code-block:: terminal - - $ git init - $ git add . - $ git commit -m "Initial commit" - -Your project already has a sensible ``.gitignore`` file. And as you install more -packages, a system called :ref:`Flex ` will add more lines to -that file when needed. - .. _install-existing-app: Setting up an Existing Symfony Project -------------------------------------- -If you're working on an existing Symfony application, you only need to get the -project code and install the dependencies with Composer. Assuming your team uses Git, -setup your project with the following commands: +In addition to creating new Symfony projects, you will also work on projects +already created by other developers. In that case, you only need to get the +project code and install the dependencies with Composer. Assuming your team uses +Git, setup your project with the following commands: .. code-block:: terminal @@ -118,15 +117,133 @@ setup your project with the following commands: $ cd my-project/ $ composer install -You'll probably also need to customize your :ref:`.env ` and do a -few other project-specific tasks (e.g. creating database schema). When working -on a existing Symfony app for the first time, it may be useful to run this -command which displays information about the app: +You'll probably also need to customize your :ref:`.env file ` +and do a few other project-specific tasks (e.g. creating a database). When +working on a existing Symfony application for the first time, it may be useful +to run this command which displays information about the project: .. code-block:: terminal $ php bin/console about +.. _symfony-flex: + +Installing Packages +------------------- + +A common practice when developing Symfony applications is to install packages +(Symfony calls them :doc:`bundles `) that provide ready-to-use +features. Packages usually require some setup before using them (editing some +file to enable the bundle, creating some file to add some initial config, etc.) + +Most of the time this setup can be automated and that's why Symfony includes +`Symfony Flex`_, a tool to simplify the installation/removal of packages in +Symfony applications. Technically speaking, Symfony Flex is a Composer plugin +that is installed by default when creating a new Symfony application and which +**automates the most common tasks of Symfony applications**. + +.. tip:: + + You can also :doc:`add Symfony Flex to an existing project `. + +Symfony Flex modifies the behavior of the ``require``, ``update``, and +``remove`` Composer commands to provide advanced features. Consider the +following example: + +.. code-block:: terminal + + $ cd my-project/ + $ composer require logger + +If you execute that command in a Symfony application which doesn't use Flex, +you'll see a Composer error explaining that ``logger`` is not a valid package +name. However, if the application has Symfony Flex installed, that command +installs and enables all the packages needed to use the official Symfony logger. + +This is possible because lots of Symfony packages/bundles define **"recipes"**, +which are a set of automated instructions to install and enable packages into +Symfony applications. Flex keeps tracks of the recipes it installed in a +``symfony.lock`` file, which must be committed to your code repository. + +Symfony Flex recipes are contributed by the community and they are stored in +two public repositories: + +* `Main recipe repository`_, is a curated list of recipes for high quality and + maintained packages. Symfony Flex only looks in this repository by default. + +* `Contrib recipe repository`_, contains all the recipes created by the + community. All of them are guaranteed to work, but their associated packages + could be unmaintained. Symfony Flex will ask your permission before installing + any of these recipes. + +Read the `Symfony Recipes documentation`_ to learn everything about how to +create recipes for your own packages. + +.. _symfony-packs: + +Symfony Packs +~~~~~~~~~~~~~ + +Sometimes a single feature requires installing several packages and bundles. +Instead of installing them individually, Symfony provides **packs**, which are +Composer metapackages that include several dependencies. + +For example, to add debugging features in your application, you can run the +``composer require --dev debug`` command. This installs the ``symfony/debug-pack``, +which in turn installs several packages like ``symfony/debug-bundle``, +``symfony/monolog-bundle``, ``symfony/var-dumper``, etc. + +By default, when installing Symfony packs, your ``composer.json`` file shows the +pack dependency (e.g. ``"symfony/debug-pack": "^1.0"``) instead of the actual +packages installed. To show the packages, add the ``--unpack`` option when +installing a pack (e.g. ``composer require debug --dev --unpack``) or run this +command to unpack the already installed packs: ``composer unpack PACK_NAME`` +(e.g. ``composer unpack debug``). + +.. _security-checker: + +Checking Security Vulnerabilities +--------------------------------- + +The ``symfony`` binary created when you `install Symfony`_ provides a command to +check whether your project's dependencies contain any known security +vulnerability: + +.. code-block:: terminal + + $ symfony check:security + +A good security practice is to execute this command regularly to be able to +update or replace compromised dependencies as soon as possible. The security +check is done locally by cloning the public `PHP security advisories database`_, +so your ``composer.lock`` file is not sent on the network. + +.. tip:: + + The ``check:security`` command terminates with a non-zero exit code if + any of your dependencies is affected by a known security vulnerability. + This way you can add it to your project build process and your continuous + integration workflows to make them fail when there are vulnerabilities. + +Symfony LTS Versions +-------------------- + +According to the :doc:`Symfony release process `, +"long-term support" (or LTS for short) versions are published every two years. +Check out the `Symfony roadmap`_ to know which is the latest LTS version. + +By default, the command that creates new Symfony applications uses the latest +stable version. If you want to use an LTS version, add the ``--version`` option: + +.. code-block:: terminal + + # find the latest LTS version at https://symfony.com/roadmap + $ symfony new --version=3.4 my_project_name_name + + # you can also base your project on development versions + $ symfony new --version=4.4.x-dev my_project_name + $ symfony new --version=dev-master my_project_name + The Symfony Demo application ---------------------------- @@ -134,15 +251,19 @@ The Symfony Demo application recommended way to develop Symfony applications. It's a great learning tool for Symfony newcomers and its code contains tons of comments and helpful notes. -To check out its code and install it locally, see `symfony/symfony-demo`_. +Run this command to create a new project based on the Symfony Demo application: + +.. code-block:: terminal + + $ symfony new --demo my_project_name Start Coding! ------------- With setup behind you, it's time to :doc:`Create your first page in Symfony `. -Go Deeper with Setup --------------------- +Learn More +---------- .. toctree:: :hidden: @@ -154,15 +275,24 @@ Go Deeper with Setup :glob: setup/homestead - setup/built_in_web_server setup/web_server_configuration setup/* .. _`Stellar Development with Symfony`: http://symfonycasts.com/screencast/symfony -.. _`Composer`: https://getcomposer.org/ -.. _`installed Composer`: https://getcomposer.org/download/ -.. _`Download the Symfony local web server`: https://symfony.com/download -.. _`Symfony download page`: https://symfony.com/download -.. _`the Security Checker`: https://github.com/sensiolabs/security-checker#integration -.. _`The Symfony Demo application`: https://github.com/symfony/demo -.. _`symfony/symfony-demo`: https://github.com/symfony/demo +.. _`Install Composer`: https://getcomposer.org/download/ +.. _`Install Symfony`: https://symfony.com/download +.. _`install Symfony`: https://symfony.com/download +.. _`The Symfony Demo Application`: https://github.com/symfony/demo +.. _`Symfony Flex`: https://github.com/symfony/flex +.. _`PHP security advisories database`: https://github.com/FriendsOfPHP/security-advisories +.. _`Symfony roadmap`: https://symfony.com/roadmap +.. _`Main recipe repository`: https://github.com/symfony/recipes +.. _`Contrib recipe repository`: https://github.com/symfony/recipes-contrib +.. _`Symfony Recipes documentation`: https://github.com/symfony/recipes/blob/master/README.rst +.. _`iconv`: https://php.net/book.iconv +.. _`JSON`: https://php.net/book.json +.. _`Session`: https://php.net/book.session +.. _`Ctype`: https://php.net/book.ctype +.. _`Tokenizer`: https://php.net/book.tokenizer +.. _`SimpleXML`: https://php.net/book.simplexml +.. _`PCRE`: https://php.net/book.pcre diff --git a/setup/bundles.rst b/setup/bundles.rst index a83916898c7..53104c6c9ba 100644 --- a/setup/bundles.rst +++ b/setup/bundles.rst @@ -142,7 +142,7 @@ following recommended configuration as the starting point of your own configurat matrix: include: - php: 5.3.3 - env: COMPOSER_FLAGS='--prefer-lowest --prefer-stable' SYMFONY_DEPRECATIONS_HELPER=weak + env: COMPOSER_FLAGS='--prefer-lowest --prefer-stable' SYMFONY_DEPRECATIONS_HELPER=max[total]=999999 - php: 5.6 env: SYMFONY_VERSION='2.7.*' - php: 5.6 diff --git a/setup/flex.rst b/setup/flex.rst index 055cd30d015..87d40dd6c70 100644 --- a/setup/flex.rst +++ b/setup/flex.rst @@ -1,135 +1,15 @@ .. index:: Flex -Using Symfony Flex to Manage Symfony Applications -================================================= - -`Symfony Flex`_ is the new way to install and manage Symfony applications. Flex -is not a new Symfony version, but a tool that replaces and improves the -`Symfony Installer`_ and the `Symfony Standard Edition`_. - -Symfony Flex **automates the most common tasks of Symfony applications**, like -installing and removing bundles and other Composer dependencies. Symfony -Flex works for Symfony 3.3 and higher. Starting from Symfony 4.0, Flex -should be used by default, but it is still optional. - -How Does Flex Work ------------------- - -Symfony Flex is a Composer plugin that modifies the behavior of the -``require``, ``update``, and ``remove`` commands. When installing or removing -dependencies in a Flex-enabled application, Symfony can perform tasks before -and after the execution of Composer tasks. - -Consider the following example: - -.. code-block:: terminal - - $ cd my-project/ - $ composer require mailer - -If you execute that command in a Symfony application which doesn't use Flex, -you'll see a Composer error explaining that ``mailer`` is not a valid package -name. However, if the application has Symfony Flex installed, that command ends -up installing and enabling the SwiftmailerBundle, which is the best way to -integrate Swiftmailer, the official mailer for Symfony applications. - -When Symfony Flex is installed in the application and you execute ``composer -require``, the application makes a request to the Symfony Flex server before -trying to install the package with Composer. - -* If there's no information about that package, the Flex server returns nothing and - the package installation follows the usual procedure based on Composer; - -* If there's special information about that package, Flex returns it in a file - called a "recipe" and the application uses it to decide which package to - install and which automated tasks to run after the installation. - -In the above example, Symfony Flex asks about the ``mailer`` package and the -Symfony Flex server detects that ``mailer`` is in fact an alias for -SwiftmailerBundle and returns the "recipe" for it. - -Flex keeps tracks of the recipes it installed in a ``symfony.lock`` file, which -must be committed to your code repository. - -Symfony Flex Recipes -~~~~~~~~~~~~~~~~~~~~ - -Recipes are defined in a ``manifest.json`` file and can contain any number of -other files and directories. For example, this is the ``manifest.json`` for -SwiftmailerBundle: - -.. code-block:: javascript - - { - "bundles": { - "Symfony\\Bundle\\SwiftmailerBundle\\SwiftmailerBundle": ["all"] - }, - "copy-from-recipe": { - "config/": "%CONFIG_DIR%/" - }, - "env": { - "MAILER_URL": "smtp://localhost:25?encryption=&auth_mode=" - }, - "aliases": ["mailer", "mail"] - } - -The ``aliases`` option allows Flex to install packages using short and easy to -remember names (``composer require mailer`` vs -``composer require symfony/swiftmailer-bundle``). The ``bundles`` option tells -Flex in which environments this bundle should be enabled automatically (``all`` -in this case). The ``env`` option makes Flex add new environment variables to -the application. Finally, the ``copy-from-recipe`` option allows the recipe to -copy files and directories into your application. - -The instructions defined in this ``manifest.json`` file are also used by -Symfony Flex when uninstalling dependencies (e.g. ``composer remove mailer``) -to undo all changes. This means that Flex can remove the SwiftmailerBundle from -the application, delete the ``MAILER_URL`` environment variable and any other -file and directory created by this recipe. - -Symfony Flex recipes are contributed by the community and they are stored in -two public repositories: - -* `Main recipe repository`_, is a curated list of recipes for high quality and - maintained packages. Symfony Flex only looks in this repository by default. - -* `Contrib recipe repository`_, contains all the recipes created by the - community. All of them are guaranteed to work, but their associated packages - could be unmaintained. Symfony Flex will ask your permission before installing - any of these recipes. - -Read the `Symfony Recipes documentation`_ to learn everything about how to -create recipes for your own packages. - -Using Symfony Flex in New Applications --------------------------------------- - -Symfony has published a new "skeleton" project, which is a minimal Symfony -project recommended to create new applications. This "skeleton" already -includes Symfony Flex as a dependency. This means you can create a new Flex-enabled -Symfony application by executing the following command: - -.. code-block:: terminal - - $ composer create-project symfony/skeleton my-project - -.. note:: - - The use of the Symfony Installer to create new applications is no longer - recommended since Symfony 3.3. Use the Composer ``create-project`` command - instead. - -.. _upgrade-to-flex: - -Upgrading Existing Applications to Flex ---------------------------------------- +Upgrading Existing Applications to Symfony Flex +=============================================== Using Symfony Flex is optional, even in Symfony 4, where Flex is used by default. However, Flex is so convenient and improves your productivity so much that it's strongly recommended to upgrade your existing applications to it. -The only caveat is that Symfony Flex requires that applications use the -following directory structure, which is the same used by default in Symfony 4: +Symfony Flex recommends that applications use the following directory structure, +which is the same used by default in Symfony 4, but you can +:ref:`customize some directories `: .. code-block:: text @@ -165,7 +45,7 @@ manual steps: $ composer require symfony/flex #. If the project's ``composer.json`` file contains ``symfony/symfony`` dependency, - it still depends on the Symfony Standard edition, which is no longer available + it still depends on the Symfony Standard Edition, which is no longer available in Symfony 4. First, remove this dependency: .. code-block:: terminal @@ -281,6 +161,8 @@ manual steps: #. Update the ``.gitignore`` file to replace the existing ``var/logs/`` entry by ``var/log/``, which is the new name for the log directory. +.. _flex-customize-paths: + Customizing Flex Paths ---------------------- @@ -311,12 +193,6 @@ If you customize these paths, some files copied from a recipe still may contain references to the original path. In other words: you may need to update some things manually after a recipe is installed. -.. _`Symfony Flex`: https://github.com/symfony/flex -.. _`Symfony Installer`: https://github.com/symfony/symfony-installer -.. _`Symfony Standard Edition`: https://github.com/symfony/symfony-standard -.. _`Main recipe repository`: https://github.com/symfony/recipes -.. _`Contrib recipe repository`: https://github.com/symfony/recipes-contrib -.. _`Symfony Recipes documentation`: https://github.com/symfony/recipes/blob/master/README.rst .. _`default services.yaml file`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/3.3/config/services.yaml .. _`shown in this example`: https://github.com/symfony/skeleton/blob/8e33fe617629f283a12bbe0a6578bd6e6af417af/composer.json#L24-L33 .. _`shown in this example of the skeleton-project`: https://github.com/symfony/skeleton/blob/8e33fe617629f283a12bbe0a6578bd6e6af417af/composer.json#L44-L46 diff --git a/setup/homestead.rst b/setup/homestead.rst index 747f6acf425..c68790800d3 100644 --- a/setup/homestead.rst +++ b/setup/homestead.rst @@ -4,8 +4,8 @@ Using Symfony with Homestead/Vagrant ==================================== In order to develop a Symfony application, you might want to use a virtual -development environment instead of the built-in server or WAMP/LAMP. Homestead_ -is an easy-to-use Vagrant_ box to get a virtual environment up and running +development environment instead of the built-in server or WAMP/LAMP. `Homestead`_ +is an easy-to-use `Vagrant`_ box to get a virtual environment up and running quickly. .. tip:: @@ -72,8 +72,8 @@ developing your Symfony application! integration, automatic creation of MySQL databases and more, read the `Daily Usage`_ section of the Homestead documentation. -.. _Homestead: https://laravel.com/docs/homestead -.. _Vagrant: https://www.vagrantup.com/ -.. _the Homestead documentation: https://laravel.com/docs/homestead#installation-and-setup -.. _Daily Usage: https://laravel.com/docs/homestead#daily-usage -.. _this blog post: https://www.whitewashing.de/2013/08/19/speedup_symfony2_on_vagrant_boxes.html +.. _`Homestead`: https://laravel.com/docs/homestead +.. _`Vagrant`: https://www.vagrantup.com/ +.. _`the Homestead documentation`: https://laravel.com/docs/homestead#installation-and-setup +.. _`Daily Usage`: https://laravel.com/docs/homestead#daily-usage +.. _`this blog post`: https://www.whitewashing.de/2013/08/19/speedup_symfony2_on_vagrant_boxes.html diff --git a/setup/symfony_server.rst b/setup/symfony_server.rst index fc4a1212372..cfba57fd09f 100644 --- a/setup/symfony_server.rst +++ b/setup/symfony_server.rst @@ -14,9 +14,8 @@ PHP application and even with HTML/SPA (single page applications). Installation ------------ -The Symfony server is distributed as a free installable binary and has support -for Linux, macOS and Windows. Go to `symfony.com/download`_ and follow the -instructions for your operating system. +The Symfony server is part of the ``symfony`` binary created when you +`install Symfony`_ and has support for Linux, macOS and Windows. .. note:: @@ -172,7 +171,7 @@ local IP. However, sometimes it is preferable to associate a domain name to them * It's more convenient when you work continuously on the same project because port numbers can change but domains don't; * The behavior of some applications depend on their domains/subdomains; -* To have stable endpoints, such as the local redirection URL for Oauth2. +* To have stable endpoints, such as the local redirection URL for OAuth2. Setting up the Local Proxy ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -184,11 +183,12 @@ server. First, start the proxy: $ symfony proxy:start -If this is the first time you run the proxy, you must follow these additional steps: +If this is the first time you run the proxy, you must configure it as follows: -* Open the **network configuration** of your operating system; -* Find the **proxy settings** and select the **"Automatic Proxy Configuration"**; -* Set the following URL as its value: ``http://127.0.0.1:7080/proxy.pac`` +* Open the **proxy settings** of your operating system (`proxy settings in Windows`_, + `proxy settings in macOS`_, `proxy settings in Ubuntu`_); +* Set the following URL as the value of the **Automatic Proxy Configuration**: + ``http://127.0.0.1:7080/proxy.pac`` Defining the Local Domain ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -315,68 +315,11 @@ debug any issues. `Read SymfonyCloud technical docs`_. -Bonus Features --------------- - -In addition to being a local web server, the Symfony server provides other -useful features: - -.. _security-checker: - -Looking for Security Vulnerabilities -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Run the following command to check whether your project's dependencies contain -any known security vulnerability: - -.. code-block:: terminal - - $ symfony security:check - -A good security practice is to execute this command regularly to be able to -update or replace compromised dependencies as soon as possible. The security -check is done locally by cloning the public `PHP security advisories database`_, -so your ``composer.lock`` file is not sent on the network. - -.. tip:: - - The ``security:check`` command terminates with a non-zero exit code if - any of your dependencies is affected by a known security vulnerability. - This way you can add it to your project build process and your continuous - integration workflows to make them fail when there are vulnerabilities. - -Creating Symfony Projects -~~~~~~~~~~~~~~~~~~~~~~~~~ - -In addition to the `different ways of installing Symfony`_, you can use these -commands from the Symfony server: - -.. code-block:: terminal - - # creates a new project based on symfony/skeleton - $ symfony new my_project_name - - # creates a new project based on symfony/website-skeleton - $ symfony new --full my_project_name - - # creates a new project based on the Symfony Demo application - $ symfony new --demo my_project_name - -You can create a project depending on a **development** version as well (note -that Composer will also set the stability to ``dev`` for all root dependencies): - -.. code-block:: terminal - - # creates a new project based on Symfony's master branch - $ symfony new --version=dev-master my_project_name - - # creates a new project based on Symfony's 4.3 dev branch - $ symfony new --version=4.3.x-dev my_project_name - -.. _`symfony.com/download`: https://symfony.com/download +.. _`install Symfony`: https://symfony.com/download .. _`symfony/cli`: https://github.com/symfony/cli -.. _`different ways of installing Symfony`: https://symfony.com/download .. _`Docker`: https://en.wikipedia.org/wiki/Docker_(software) .. _`SymfonyCloud`: https://symfony.com/cloud/ .. _`Read SymfonyCloud technical docs`: https://symfony.com/doc/master/cloud/intro.html -.. _`PHP security advisories database`: https://github.com/FriendsOfPHP/security-advisories +.. _`proxy settings in Windows`: https://www.dummies.com/computers/operating-systems/windows-10/how-to-set-up-a-proxy-in-windows-10/ +.. _`proxy settings in macOS`: https://support.apple.com/guide/mac-help/enter-proxy-server-settings-on-mac-mchlp2591/mac +.. _`proxy settings in Ubuntu`: https://help.ubuntu.com/stable/ubuntu-help/net-proxy.html.en diff --git a/setup/upgrade_major.rst b/setup/upgrade_major.rst index 83e1b41b583..56b25062646 100644 --- a/setup/upgrade_major.rst +++ b/setup/upgrade_major.rst @@ -97,9 +97,9 @@ done! Sometimes, you can't fix all deprecations (e.g. something was deprecated in 3.4 and you still need to support 3.3). In these cases, you can still - use the bridge to fix as many deprecations as possible and then switch - to the weak test mode to make your tests pass again. You can do this by - using the ``SYMFONY_DEPRECATIONS_HELPER`` env variable: + use the bridge to fix as many deprecations as possible and then allow + more of them to make your tests pass again. You can do this by using the + ``SYMFONY_DEPRECATIONS_HELPER`` env variable: .. code-block:: xml @@ -108,11 +108,15 @@ done! - + - (you can also execute the command like ``SYMFONY_DEPRECATIONS_HELPER=weak phpunit``). + You can also execute the command like: + + .. code-block:: terminal + + $ SYMFONY_DEPRECATIONS_HELPER=max[total]=999999 php ./bin/phpunit .. _upgrade-major-symfony-composer: @@ -158,6 +162,4 @@ of. When upgrading to Symfony 4, you will probably also want to upgrade to the new Symfony 4 directory structure so that you can take advantage of Symfony Flex. -This takes some work, but is optional. For details, see :ref:`upgrade-to-flex`. - -.. _`Symfony-Upgrade-Fixer`: https://github.com/umpirsky/Symfony-Upgrade-Fixer +This takes some work, but is optional. For details, see :doc:`/setup/flex`. diff --git a/setup/web_server_configuration.rst b/setup/web_server_configuration.rst index f388dee3e76..bda1c343a9d 100644 --- a/setup/web_server_configuration.rst +++ b/setup/web_server_configuration.rst @@ -35,13 +35,14 @@ to use PHP :ref:`with Nginx `. Adding Rewrite Rules -------------------- -The easiest way is to install the Apache recipe by executing the following command: +The easiest way is to install the ``apache`` :ref:`Symfony pack ` +by executing the following command: .. code-block:: terminal $ composer require symfony/apache-pack -This recipe installs a ``.htaccess`` file in the ``public/`` directory that contains +This pack installs a ``.htaccess`` file in the ``public/`` directory that contains the rewrite rules. .. tip:: @@ -93,6 +94,8 @@ and increase web server performance: ServerAlias www.domain.tld DocumentRoot /var/www/project/public + DirectoryIndex /index.php + AllowOverride None Order Allow,Deny @@ -122,6 +125,11 @@ and increase web server performance: #SetEnv DATABASE_URL "mysql://db_user:db_pass@host:3306/db_name" +.. caution:: + + Use ``FallbackResource`` on Apache 2.4.25 or higher, due to a bug which was + fixed on that release causing the root ``/`` to hang. + .. tip:: If you are using **php-cgi**, Apache does not pass HTTP basic username and @@ -288,6 +296,13 @@ The **minimum configuration** to get your application running under Nginx is: try_files $uri /index.php$is_args$args; } + # optionally disable falling back to PHP script for the asset directories; + # nginx will return a 404 error when files are not found instead of passing the + # request to Symfony (improves performance but Symfony's 404 page is not displayed) + # location /bundles { + # try_files $uri =404; + # } + location ~ ^/index\.php(/|$) { fastcgi_pass unix:/var/run/php/php7.2-fpm.sock; fastcgi_split_path_info ^(.+\.php)(/.*)$; diff --git a/templates.rst b/templates.rst new file mode 100644 index 00000000000..d92357954a0 --- /dev/null +++ b/templates.rst @@ -0,0 +1,1100 @@ +.. index:: + single: Templating + +Creating and Using Templates +============================ + +A template is the best way to organize and render HTML from inside your application, +whether you need to render HTML from a :doc:`controller ` or generate +the :doc:`contents of an email `. Templates in Symfony are created with +Twig: a flexible, fast, and secure template engine. + +.. _twig-language: + +Twig Templating Language +------------------------ + +The `Twig`_ templating language allows you to write concise, readable templates +that are more friendly to web designers and, in several ways, more powerful than +PHP templates. Take a look at the following Twig template example. Even if it's +the first time you see Twig, you probably understand most of it: + +.. code-block:: html+twig + + + + + Welcome to Symfony! + + +

{{ page_title }}

+ + {% if user.isLoggedIn %} + Hello {{ user.name }}! + {% endif %} + + {# ... #} + + + +Twig syntax is based on these three constructs: + +* ``{{ ... }}``, used to display the content of a variable or the result of + evaluating an expression; +* ``{% ... %}``, used to run some logic, such as a conditional or a loop; +* ``{# ... #}``, used to add comments to the template (unlike HTML comments, + these comments are not included in the rendered page). + +You can't run PHP code inside Twig templates, but Twig provides utilities to +run some logic in the templates. For example, **filters** modify content before +being rendered, like the ``upper`` filter to uppercase contents: + +.. code-block:: twig + + {{ title|upper }} + +Twig comes with a long list of `tags`_, `filters`_ and `functions`_ that are +available by default. In Symfony applications you can also use these +:doc:`Twig filters and functions defined by Symfony ` +and you can :doc:`create your own Twig filters and functions `. + +Twig is fast in the ``prod`` :ref:`environment ` +(because templates are compiled into PHP and cached automatically), but +convenient to use in the ``dev`` environment (because templates are recompiled +automatically when you change them). + +Twig Configuration +~~~~~~~~~~~~~~~~~~ + +Twig has several configuration options to define things like the format used +to display numbers and dates, the template caching, etc. Read the +:doc:`Twig configuration reference ` to learn about them. + +Creating Templates +------------------ + +Before explaining in detail how to create and render templates, look at the +following example for a quick overview of the whole process. First, you need to +create a new file in the ``templates/`` directory to store the template contents: + +.. code-block:: html+twig + + {# templates/user/notifications.html.twig #} +

Hello {{ user_first_name }}!

+

You have {{ notifications|length }} new notifications.

+ +Then, create a :doc:`controller ` that renders this template and +passes to it the needed variables:: + + // src/Controller/UserController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + + class UserController extends AbstractController + { + // ... + + public function notifications() + { + // get the user information and notifications somehow + $userFirstName = '...'; + $userNotifications = ['...', '...']; + + // the template path is the relative file path from `templates/` + return $this->render('user/notifications.html.twig', [ + // this array defines the variables passed to the template, + // where the key is the variable name and the value is the variable value + // (Twig recommends using snake_case variable names: 'foo_bar' instead of 'fooBar') + 'user_first_name' => $userFirstName, + 'notifications' => $userNotifications, + ]); + } + } + +Template Naming +~~~~~~~~~~~~~~~ + +Symfony recommends the following for template names: + +* Use `snake case`_ for filenames and directories (e.g. ``blog_posts.twig``, + ``admin/default_theme/blog/index.twig``, etc.); +* Define two extensions for filenames (e.g. ``index.html.twig`` or + ``blog_posts.xml.twig``) being the first extension (``html``, ``xml``, etc.) + the final format that the template will generate. + +Although templates usually generate HTML contents, they can generate any +text-based format. That's why the two-extension convention simplifies the way +templates are created and rendered for multiple formats. + +Template Location +~~~~~~~~~~~~~~~~~ + +Templates are stored by default in the ``templates/`` directory. When a service +or controller renders the ``product/index.html.twig`` template, they are actually +referring to the ``/templates/product/index.html.twig`` file. + +The default templates directory is configurable with the +:ref:`twig.default_path ` option and you can add more +template directories :ref:`as explained later ` in this article. + +Template Variables +~~~~~~~~~~~~~~~~~~ + +A common need for templates is to print the values stored in the templates +passed from the controller or service. Variables usually store objects and +arrays instead of strings, numbers and boolean values. That's why Twig provides +quick access to complex PHP variables. Consider the following template: + +.. code-block:: html+twig + +

{{ user.name }} added this comment on {{ comment.publishedAt|date }}

+ +The ``user.name`` notation means that you want to display some information +(``name``) stored in a variable (``user``). Is ``user`` an array or an object? +Is ``name`` a property or a method? In Twig this doesn't matter. + +When using the ``foo.bar`` notation, Twig tries to get the value of the variable +in the following order: + +#. ``$foo['bar']`` (array and element); +#. ``$foo->bar`` (object and public property); +#. ``$foo->bar()`` (object and public method); +#. ``$foo->getBar()`` (object and *getter* method); +#. ``$foo->isBar()`` (object and *isser* method); +#. ``$foo->hasBar()`` (object and *hasser* method); +#. If none of the above exists, use ``null``. + +This allows to evolve your application code without having to change the +template code (you can start with array variables for the application proof of +concept, then move to objects with methods, etc.) + +.. _templates-link-to-pages: + +Linking to Pages +~~~~~~~~~~~~~~~~ + +Instead of writing the link URLs by hand, use the ``path()`` function to +generate URLs based on the :ref:`routing configuration `. + +Later, if you want to modify the URL of a particular page, all you'll need to do +is change the routing configuration: the templates will automatically generate +the new URL. + +Consider the following routing configuration: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Controller/BlogController.php + + // ... + use Symfony\Component\Routing\Annotation\Route; + + class BlogController extends AbstractController + { + /** + * @Route("/", name="blog_index") + */ + public function index() + { + // ... + } + + /** + * @Route("/article/{slug}", name="blog_post") + */ + public function show(string $slug) + { + // ... + } + } + + .. code-block:: yaml + + # config/routes.yaml + blog_index: + path: / + controller: App\Controller\BlogController::index + + blog_post: + path: /article/{slug} + controller: App\Controller\BlogController::show + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/routes.php + use App\Controller\BlogController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + $routes->add('blog_index', '/') + ->controller([BlogController::class, 'index']) + ; + + $routes->add('blog_post', '/articles/{slug}') + ->controller([BlogController::class, 'show']) + ; + }; + +Use the ``path()`` Twig function to link to these pages and pass the route name +as the first argument and the route parameters as the optional second argument: + +.. code-block:: html+twig + +
Homepage + + {# ... #} + + {% for post in blog_posts %} +

+ {{ post.title }} +

+ +

{{ post.excerpt }}

+ {% endfor %} + +The ``path()`` function generates relative URLs. If you need to generate +absolute URLs (for example when rendering templates for emails or RSS feeds), +use the ``url()`` function, which takes the same arguments as ``path()`` +(e.g. `` ... ``). + +.. _templates-link-to-assets: + +Linking to CSS, JavaScript and Image Assets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a template needs to link to a static asset (e.g. an image), Symfony provides +an ``asset()`` Twig function to help generate that URL. First, install the +``asset`` package: + +.. code-block:: terminal + + $ composer require symfony/asset + +You can now use the ``asset()`` function: + +.. code-block:: html+twig + + {# the image lives at "public/images/logo.png" #} + Symfony! + + {# the CSS file lives at "public/css/blog.css" #} + + + {# the JS file lives at "public/bundles/acme/js/loader.js" #} + + +The ``asset()`` function's main purpose is to make your application more portable. +If your application lives at the root of your host (e.g. ``https://example.com``), +then the rendered path should be ``/images/logo.png``. But if your application +lives in a subdirectory (e.g. ``https://example.com/my_app``), each asset path +should render with the subdirectory (e.g. ``/my_app/images/logo.png``). The +``asset()`` function takes care of this by determining how your application is +being used and generating the correct paths accordingly. + +.. tip:: + + The ``asset()`` function supports various cache busting techniques via the + :ref:`version `, + :ref:`version_format `, and + :ref:`json_manifest_path ` configuration options. + +.. tip:: + + If you'd like help packaging, versioning and minifying your JavaScript and + CSS assets in a modern way, read about :doc:`Symfony's Webpack Encore `. + +If you need absolute URLs for assets, use the ``absolute_url()`` Twig function +as follows: + +.. code-block:: html+twig + + Symfony! + + + +.. _twig-app-variable: + +The App Global Variable +~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony creates a context object that is injected into every Twig template +automatically as a variable called ``app``. It provides access to some +application information: + +.. code-block:: html+twig + +

Username: {{ app.user.username ?? 'Anonymous user' }}

+ {% if app.debug %} +

Request method: {{ app.request.method }}

+

Application Environment: {{ app.environment }}

+ {% endif %} + +The ``app`` variable (which is an instance of :class:`Symfony\\Bridge\\Twig\\AppVariable`) +gives you access to these variables: + +``app.user`` + The :ref:`current user object ` or ``null`` if the user + is not authenticated. +``app.request`` + The :class:`Symfony\\Component\\HttpFoundation\\Request` object that stores + the current :ref:`request data ` (depending on your + application, this can be a :ref:`sub-request ` + or a regular request). +``app.session`` + The :class:`Symfony\\Component\\HttpFoundation\\Session\\Session` object that + represents the current :doc:`user's session ` or ``null`` if there is none. +``app.flashes`` + An array of all the :ref:`flash messages ` stored in the session. + You can also get only the messages of some type (e.g. ``app.flashes('notice')``). +``app.environment`` + The name of the current :ref:`configuration environment ` + (``dev``, ``prod``, etc). +``app.debug`` + True if in :ref:`debug mode `. False otherwise. +``app.token`` + A :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface` + object representing the security token. + +In addition to the global ``app`` variable injected by Symfony, you can also +:doc:`inject variables automatically to all Twig templates `. + +.. _templates-rendering: + +Rendering Templates +------------------- + +Rendering a Template in Controllers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your controller extends from the :ref:`AbstractController `, +use the ``render()`` helper:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + + class ProductController extends AbstractController + { + public function index() + { + // ... + + return $this->render('product/index.html.twig', [ + 'category' => '...', + 'promotions' => ['...', '...'], + ]); + } + } + +If your controller does not extend from ``AbstractController``, you'll need to +:ref:`fetch services in your controller ` and +use the ``render()`` method of the ``twig`` service. + +Rendering a Template in Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Inject the ``twig`` Symfony service into your own services and use its +``render()`` method. When using :doc:`service autowiring ` +you only need to add an argument in the service constructor and type-hint it with +the :class:`Twig\\Environment` class:: + + // src/Service/SomeService.php + use Twig\Environment; + + class SomeService + { + private $twig; + + public function __construct(Environment $twig) + { + $this->twig = $twig; + } + + public function someMethod() + { + // ... + + $htmlContents = $this->twig->render('product/index.html.twig', [ + 'category' => '...', + 'promotions' => ['...', '...'], + ]); + } + } + +Rendering a Template in Emails +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Read the docs about the :ref:`mailer and Twig integration `. + +.. _templates-render-from-route: + +Rendering a Template Directly from a Route +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Although templates are usually rendered in controllers and services, you can +render static pages that don't need any variables directly from the route +definition. Use the special ``TemplateController`` provided by Symfony: + +.. configuration-block:: + + .. code-block:: yaml + + # config/routes.yaml + acme_privacy: + path: /privacy + controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController + defaults: + # the path of the template to render + template: 'static/privacy.html.twig' + + # special options defined by Symfony to set the page cache + maxAge: 86400 + sharedAge: 86400 + + # optionally you can define some arguments passed to the template + site_name: 'ACME' + theme: 'dark' + + .. code-block:: xml + + + + + + + + static/privacy.html.twig + + + 86400 + 86400 + + + ACME + dark + + + + .. code-block:: php + + // config/routes.php + use Symfony\Bundle\FrameworkBundle\Controller\TemplateController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + $routes->add('acme_privacy', '/privacy') + ->controller(TemplateController::class) + ->defaults([ + // the path of the template to render + 'template' => 'static/privacy.html.twig', + + // special options defined by Symfony to set the page cache + 'maxAge' => 86400, + 'sharedAge' => 86400, + + // optionally you can define some arguments passed to the template + 'site_name' => 'ACME', + 'theme' => 'dark', + ]) + ; + }; + +Checking if a Template Exists +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Templates are loaded in the application using a `Twig template loader`_, which +also provides a method to check for template existence. First, get the loader:: + + // in a controller extending from AbstractController + $loader = $this->get('twig')->getLoader(); + + // in a service using autowiring + use Twig\Environment; + + public function __construct(Environment $twig) + { + $loader = $twig->getLoader(); + } + +Then, pass the path of the Twig template to the ``exists()`` method of the loader:: + + if ($loader->exists('theme/layout_responsive.html.twig')) { + // the template exists, do something + // ... + } + +Debugging Templates +------------------- + +Symfony provides several utilities to help you debug issues in your templates. + +Linting Twig Templates +~~~~~~~~~~~~~~~~~~~~~~ + +The ``lint:twig`` command checks that your Twig templates don't have any syntax +errors. It's useful to run it before deploying your application to production +(e.g. in your continuous integration server): + +.. code-block:: terminal + + # check all the templates stored in a directory + $ php bin/console lint:twig templates/ + + # you can also check individual templates + $ php bin/console lint:twig templates/article/recent_list.html.twig + +Inspecting Twig Information +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``debug:twig`` command lists all the information available about Twig +(functions, filters, global variables, etc.). It's useful to check if your +:doc:`custom Twig extensions ` are working properly +and also to check the Twig features added when :ref:`installing packages `: + +.. code-block:: terminal + + # list general information + $ php bin/console debug:twig + + # filter output by any keyword + $ php bin/console debug:twig --filter=date + + # pass a template path to show the physical file which will be loaded + $ php bin/console debug:twig @Twig/Exception/error.html.twig + +The Dump Twig Utilities +~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides a :ref:`dump() function ` as an +improved alternative to PHP's ``var_dump()`` function. This function is useful +to inspect the contents of any variable and you can use it in Twig templates too. + +First, make sure that the VarDumper component is installed in the application: + +.. code-block:: terminal + + $ composer require symfony/var-dumper + +Then, use either the ``{% dump %}`` tag or the ``{{ dump() }}`` function +depending on your needs: + +.. code-block:: html+twig + + {# templates/article/recent_list.html.twig #} + {# the contents of this variable are sent to the Web Debug Toolbar + instead of dumping them inside the page contents #} + {% dump articles %} + + {% for article in articles %} + {# the contents of this variable are dumped inside the page contents + and they are visible on the web page #} + {{ dump(article) }} + + + {{ article.title }} + + {% endfor %} + +To avoid leaking sensitive information, the ``dump()`` function/tag is only +available in the ``dev`` and ``test`` :ref:`configuration environments `. +If you try to use it in the ``prod`` environment, you will see a PHP error. + +Reusing Template Contents +------------------------- + +.. _templates-include: + +Including Templates +~~~~~~~~~~~~~~~~~~~ + +If certain Twig code is repeated in several templates, you can extract it into a +single "template fragment" and include it in other templates. Imagine that the +following code to display the user information is repeated in several places: + +.. code-block:: html+twig + + {# templates/blog/index.html.twig #} + + {# ... #} + + +First, create a new Twig template called ``blog/_user_profile.html.twig`` (the +``_`` prefix is optional, but it's a convention used to better differentiate +between full templates and template fragments). + +Then, remove that content from the original ``blog/index.html.twig`` template +and add the following to include the template fragment: + +.. code-block:: twig + + {# templates/blog/index.html.twig #} + + {# ... #} + {{ include('blog/_user_profile.html.twig') }} + +The ``include()`` Twig function takes as argument the path of the template to +include. The included template has access to all the variables of the template +that includes it (use the `with_context`_ option to control this). + +You can also pass variables to the included template. This is useful for example +to rename variables. Imagine that your template stores the user information in a +variable called ``blog_post.author`` instead of the ``user`` variable that the +template fragment expects. Use the following to *rename* the variable: + +.. code-block:: twig + + {# templates/blog/index.html.twig #} + + {# ... #} + {{ include('blog/_user_profile.html.twig', {user: blog_post.author}) }} + +.. _templates-embed-controllers: + +Embedding Controllers +~~~~~~~~~~~~~~~~~~~~~ + +:ref:`Including template fragments ` is useful to reuse the +same content on several pages. However, this technique is not the best solution +in some cases. + +Imagine that the template fragment displays the three most recent blog articles. +To do that, it needs to make a database query to get those articles. When using +the ``include()`` function, you'd need to do the same database query in every +page that includes the fragment. This is not very convenient. + +A better alternative is to **embed the result of executing some controller** +with the ``render()`` and ``controller()`` Twig functions. + +First, create the controller that renders a certain number of recent articles:: + + // src/Controller/BlogController.php + namespace App\Controller; + + // ... + + class BlogController extends AbstractController + { + public function recentArticles($max = 3) + { + // get the recent articles somehow (e.g. making a database query) + $articles = ['...', '...', '...']; + + return $this->render('blog/_recent_articles.html.twig', [ + 'articles' => $articles + ]); + } + } + +Then, create the ``blog/_recent_articles.html.twig`` template fragment (the +``_`` prefix in the template name is optional, but it's a convention used to +better differentiate between full templates and template fragments): + +.. code-block:: html+twig + + {# templates/blog/_recent_articles.html.twig #} + {% for article in articles %} + + {{ article.title }} + + {% endfor %} + +Now you can call to this controller from any template to embed its result: + +.. code-block:: html+twig + + {# templates/base.html.twig #} + + {# ... #} + + +.. _fragments-path-config: + +When using the ``controller()`` function, controllers are not accessed using a +regular Symfony route but through a special URL used exclusively to serve those +template fragments. Configure that special URL in the ``fragments`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + fragments: { path: /_fragment } + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + $container->loadFromExtension('framework', [ + // ... + 'fragments' => ['path' => '/_fragment'], + ]); + +.. caution:: + + Embedding controllers require making requests to those controllers and + rendering some templates as result. This can have a significant impact in + the application performance if you embed lots of controllers. If possible, + :doc:`cache the template fragment `. + +.. seealso:: + + Templates can also :doc:`embed contents asynchronously ` + with the ``hinclude.js`` JavaScript library. + +Template Inheritance and Layouts +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As your application grows you'll find more and more repeated elements between +pages, such as headers, footers, sidebars, etc. :ref:`Including templates ` +and :ref:`embedding controllers ` can help, but +when pages share a common structure, it's better to use **inheritance**. + +The concept of `Twig template inheritance`_ is similar to PHP class inheritance. +You define a parent template that other templates can extend from and child +templates can override parts of the parent template. + +Symfony recommends the following three-level template inheritance for medium and +complex applications: + +* ``templates/base.html.twig``, defines the common elements of all application + templates, such as ````, ``
``, ``