diff --git a/.alexrc b/.alexrc new file mode 100644 index 00000000000..168d412c177 --- /dev/null +++ b/.alexrc @@ -0,0 +1,16 @@ +{ + "allow": [ + "attack", + "attacks", + "bigger", + "color", + "colors", + "failure", + "hook", + "hooks", + "host-hostess", + "invalid", + "remain", + "special" + ] +} diff --git a/.doctor-rst.yaml b/.doctor-rst.yaml new file mode 100644 index 00000000000..2ecd4fc7d2d --- /dev/null +++ b/.doctor-rst.yaml @@ -0,0 +1,122 @@ +rules: + american_english: ~ + avoid_repetetive_words: ~ + blank_line_after_anchor: ~ + blank_line_after_directive: ~ + blank_line_before_directive: ~ + composer_dev_option_not_at_the_end: ~ + correct_code_block_directive_based_on_the_content: ~ + deprecated_directive_should_have_version: ~ + ensure_bash_prompt_before_composer_command: ~ + ensure_correct_format_for_phpfunction: ~ + ensure_exactly_one_space_before_directive_type: ~ + ensure_exactly_one_space_between_link_definition_and_link: ~ + ensure_explicit_nullable_types: ~ + ensure_github_directive_start_with_prefix: + prefix: 'Symfony' + ensure_link_bottom: ~ + ensure_link_definition_contains_valid_url: ~ + ensure_order_of_code_blocks_in_configuration_block: ~ + ensure_php_reference_syntax: ~ + extend_abstract_controller: ~ + # extension_xlf_instead_of_xliff: ~ + forbidden_directives: + directives: + - '.. index::' + - directive: '.. caution::' + replacements: ['.. warning::', '.. danger::'] + indention: ~ + lowercase_as_in_use_statements: ~ + max_blank_lines: + max: 2 + max_colons: ~ + no_app_console: ~ + no_attribute_redundant_parenthesis: ~ + no_blank_line_after_filepath_in_php_code_block: ~ + no_blank_line_after_filepath_in_twig_code_block: ~ + no_blank_line_after_filepath_in_xml_code_block: ~ + no_blank_line_after_filepath_in_yaml_code_block: ~ + no_brackets_in_method_directive: ~ + no_broken_ref_directive: ~ + no_composer_req: ~ + no_directive_after_shorthand: ~ + no_duplicate_use_statements: ~ + no_empty_literals: ~ + no_explicit_use_of_code_block_php: ~ + no_footnotes: ~ + no_inheritdoc: ~ + no_merge_conflict: ~ + no_namespace_after_use_statements: ~ + no_php_open_tag_in_code_block_php_directive: ~ + no_space_before_self_xml_closing_tag: ~ + non_static_phpunit_assertions: ~ + only_backslashes_in_namespace_in_php_code_block: ~ + only_backslashes_in_use_statements_in_php_code_block: ~ + ordered_use_statements: ~ + php_prefix_before_bin_console: ~ + remove_trailing_whitespace: ~ + replace_code_block_types: ~ + replacement: ~ + short_array_syntax: ~ + space_between_label_and_link_in_doc: ~ + space_between_label_and_link_in_ref: ~ + string_replacement: ~ + title_underline_length_must_match_title_length: ~ + typo: ~ + unused_links: ~ + use_deprecated_directive_instead_of_versionadded: ~ + use_named_constructor_without_new_keyword_rule: ~ + use_https_xsd_urls: ~ + valid_inline_highlighted_namespaces: ~ + valid_use_statements: ~ + versionadded_directive_should_have_version: ~ + yaml_instead_of_yml_suffix: ~ + + # master + versionadded_directive_major_version: + major_version: 7 + + versionadded_directive_min_version: + min_version: '7.0' + + deprecated_directive_major_version: + major_version: 7 + + deprecated_directive_min_version: + min_version: '7.0' + +exclude_rule_for_file: + - path: configuration/multiple_kernels.rst + rule_name: replacement + - path: page_creation.rst + rule_name: no_php_open_tag_in_code_block_php_directive + - path: frontend/create_ux_bundle.rst + rule_name: argument_variable_must_match_type + +# do not report as violation +whitelist: + regex: + - '/``.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' + - 'The bin/console Command' + - '.. _`LDAP injection`: http://projects.webappsec.org/w/page/13246947/LDAP%20Injection' + - '.. versionadded:: 2.8.0' # Doctrine + - '.. versionadded:: 1.9.0' # Encore + - '.. versionadded:: 1.18' # Flex in setup/upgrade_minor.rst + - '.. versionadded:: 1.0.0' # Encore + - '.. versionadded:: 2.7.1' # Doctrine + - '123,' # assertion for var_dumper - components/var_dumper.rst + - '"foo",' # assertion for var_dumper - components/var_dumper.rst + - '$var .= "Because of this `\xE9` octet (\\xE9),\n";' + - '.. versionadded:: 0.2' # MercureBundle + - '.. versionadded:: 3.6' # MonologBundle + - '.. versionadded:: 3.8' # MonologBundle + - '.. versionadded:: 3.5' # Monolog + - '.. versionadded:: 3.0' # Doctrine ORM + - '.. _`a feature to test applications using Mercure`: https://github.com/symfony/panther#creating-isolated-browsers-to-test-apps-using-mercure-or-websocket' + - 'End to End Tests (E2E)' + - '.. versionadded:: 2.2.0' # Panther + - '* Inline code blocks use double-ticks (````like this````).' diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..f9366facfb0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..9eb5d91783b --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,30 @@ +# GithubActions workflows +/.github/workflows* @OskarStark + +# Console +/console* @chalasr +/components/console* @chalasr + +# Form +/forms.rst @xabbuh @HeahDude +/components/form* @xabbuh @HeahDude +/reference/forms* @xabbuh @HeahDude + +# PropertyInfo +/components/property_info* @dunglas + +# Security +/security* @chalasr +/components/security* @chalasr + +# Validator +/validation/* @xabbuh @HeahDude +/components/validator* @xabbuh @HeahDude +/reference/constraints* @xabbuh @HeahDude + +# Workflow +/workflow* @lyrixx +/components/workflow* @lyrixx + +# Yaml +/components/yaml* @xabbuh diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000000..acb0770920e --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,5 @@ +Contributing +------------ + +We love contributors! For more information on how you can contribute to the +Symfony documentation, please read [Contributing to the Documentation](https://symfony.com/doc/current/contributing/documentation/overview.html). diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..f32043e4523 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ + diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000000..497dfd9b430 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,145 @@ +name: CI + +on: + push: + branches-ignore: + - 'github-comments' + pull_request: + branches-ignore: + - 'github-comments' + +permissions: + contents: read + +jobs: + symfony-docs-builder-build: + name: Build (symfony-tools/docs-builder) + + runs-on: ubuntu-latest + + continue-on-error: true + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Set-up PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + coverage: none + + - name: Get composer cache directory + id: composercache + working-directory: _build + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: "Install dependencies" + working-directory: _build + run: composer install --prefer-dist --no-progress + + - name: "Build the docs" + working-directory: _build + run: php build.php --disable-cache + + doctor-rst: + name: Lint (DOCtor-RST) + + runs-on: ubuntu-latest + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Create cache dir" + run: mkdir .cache + + - name: "Extract base branch name" + run: echo "branch=$(echo ${GITHUB_BASE_REF:=${GITHUB_REF##*/}})" >> $GITHUB_OUTPUT + id: extract_base_branch + + - name: "Cache DOCtor-RST" + uses: actions/cache@v3 + with: + path: .cache + key: ${{ runner.os }}-doctor-rst-${{ steps.extract_base_branch.outputs.branch }} + + - name: "Run DOCtor-RST" + uses: docker://oskarstark/doctor-rst:1.67.0 + with: + args: --short --error-format=github --cache-file=/github/workspace/.cache/doctor-rst.cache + + symfony-code-block-checker: + name: Code Blocks + + runs-on: ubuntu-latest + + continue-on-error: true + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + path: 'docs' + + - name: Set-up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + coverage: none + + - name: Fetch branch from where the PR started + working-directory: docs + run: git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* + + - name: Find modified files + id: find-files + working-directory: docs + run: echo "files=$(git diff --name-only origin/${{ github.base_ref }} HEAD | grep ".rst" | tr '\n' ' ')" >> $GITHUB_OUTPUT + + - name: Get composer cache directory + id: composercache + working-directory: docs/_build + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + if: ${{ steps.find-files.outputs.files }} + uses: actions/cache@v3 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-codeBlocks-${{ hashFiles('_checker/composer.lock', '_sf_app/composer.lock') }} + restore-keys: ${{ runner.os }}-composer-codeBlocks- + + - name: Install dependencies + if: ${{ steps.find-files.outputs.files }} + run: composer create-project symfony-tools/code-block-checker:@dev _checker + + - name: Install test application + if: ${{ steps.find-files.outputs.files }} + run: | + git clone -b ${{ github.base_ref }} --depth 5 --single-branch https://github.com/symfony-tools/symfony-application.git _sf_app + cd _sf_app + composer update + + - name: Generate baseline + if: ${{ steps.find-files.outputs.files }} + working-directory: docs + run: | + CURRENT=$(git rev-parse HEAD) + git checkout -m ${{ github.base_ref }} + ../_checker/code-block-checker.php verify:docs `pwd` ${{ steps.find-files.outputs.files }} --generate-baseline=baseline.json --symfony-application=`realpath ../_sf_app` + git checkout -m $CURRENT + cat baseline.json + + - name: Verify examples + if: ${{ steps.find-files.outputs.files }} + working-directory: docs + run: | + ../_checker/code-block-checker.php verify:docs `pwd` ${{ steps.find-files.outputs.files }} --baseline=baseline.json --output-format=github --symfony-application=`realpath ../_sf_app` diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..b69047f69a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/_build/vendor +/_build/output diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 278f7c2c39c..00000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -Contributing ------------- - -We love contributors! For more information on how you can contribute to the -Symfony documentation, please read [Contributing to the Documentation](http://symfony.com/doc/current/contributing/documentation/overview.html) -and notice the [Pull Request Format](http://symfony.com/doc/current/contributing/documentation/overview.html#pull-request-format) -that helps us merge your pull requests faster! diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000000..547ac103984 --- /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/README.markdown b/README.markdown deleted file mode 100644 index afa5925f03f..00000000000 --- a/README.markdown +++ /dev/null @@ -1,16 +0,0 @@ -Symfony Documentation -===================== - -This documentation is rendered online at http://symfony.com/doc/current/ - -Contributing ------------- - ->**Note** ->Unless you're documenting a feature that's new to a specific version of Symfony ->(e.g. Symfony 2.1), all pull requests must be based off of the **2.0** branch, ->**not** the master or 2.1 branch. - -We love contributors! For more information on how you can contribute to the -Symfony documentation, please read -[Contributing to the Documentation](http://symfony.com/doc/current/contributing/documentation/overview.html) diff --git a/README.md b/README.md new file mode 100644 index 00000000000..5c063058c02 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +

+ +

+ +

+ The official Symfony Documentation +

+ +

+ + Online version + + | + + Components + + | + + Screencasts + +

+ +Contributing +------------ + +We love contributors! For more information on how you can contribute, please read +the [Symfony Docs Contributing Guide](https://symfony.com/doc/current/contributing/documentation/overview.html). + +> [!IMPORTANT] +> Use `6.4` branch as the base of your pull requests, unless you are documenting a +> feature that was introduced *after* Symfony 6.4 (e.g. in Symfony 7.2). + +Build Documentation Locally +--------------------------- + +This is not needed for contributing, but it's useful if you would like to debug some +issue in the docs or if you want to read Symfony Documentation offline. + +```bash +$ git clone git@github.com:symfony/symfony-docs.git + +$ cd symfony-docs/ +$ cd _build/ + +$ composer install + +$ php build.php +``` + +After generating docs, serve them with the internal PHP server: + +```bash +$ php -S localhost:8000 -t output/ +``` + +Browse `http://localhost:8000` to read the docs. diff --git a/_build/build.php b/_build/build.php new file mode 100755 index 00000000000..b684700a848 --- /dev/null +++ b/_build/build.php @@ -0,0 +1,91 @@ +#!/usr/bin/env php +register('build-docs') + ->addOption('generate-fjson-files', null, InputOption::VALUE_NONE, 'Use this option to generate docs both in HTML and JSON formats') + ->addOption('disable-cache', null, InputOption::VALUE_NONE, 'Use this option to force a full regeneration of all doc contents') + ->setCode(function(InputInterface $input, OutputInterface $output) { + // the doc building app doesn't work on Windows + if ('\\' === DIRECTORY_SEPARATOR) { + $output->writeln('ERROR: The application that builds Symfony Docs does not support Windows. You can try using a Linux distribution via WSL (Windows Subsystem for Linux).'); + + return 1; + } + + $io = new SymfonyStyle($input, $output); + $io->text('Building all Symfony Docs...'); + + $outputDir = __DIR__.'/output'; + $buildConfig = (new BuildConfig()) + ->setSymfonyVersion('7.1') + ->setContentDir(__DIR__.'/..') + ->setOutputDir($outputDir) + ->setImagesDir(__DIR__.'/output/_images') + ->setImagesPublicPrefix('_images') + ->setTheme('rtd') + ; + + $buildConfig->setExcludedPaths(['.github/', '_build/']); + + if (!$generateJsonFiles = $input->getOption('generate-fjson-files')) { + $buildConfig->disableJsonFileGeneration(); + } + + if ($isCacheDisabled = $input->getOption('disable-cache')) { + $buildConfig->disableBuildCache(); + } + + $io->comment(sprintf('cache: %s / output file type(s): %s', $isCacheDisabled ? 'disabled' : 'enabled', $generateJsonFiles ? 'HTML and JSON' : 'HTML')); + if (!$isCacheDisabled) { + $io->comment('Tip: add the --disable-cache option to this command to force the re-build of all docs.'); + } + + $result = (new DocBuilder())->build($buildConfig); + + if ($result->isSuccessful()) { + // fix assets URLs to make them absolute (otherwise, they don't work in subdirectories) + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($outputDir)); + + foreach (new RegexIterator($iterator, '/^.+\.html$/i', RegexIterator::GET_MATCH) as $match) { + $htmlFilePath = array_shift($match); + $htmlContents = file_get_contents($htmlFilePath); + + $htmlRelativeFilePath = str_replace($outputDir.'/', '', $htmlFilePath); + $subdirLevel = substr_count($htmlRelativeFilePath, '/'); + $baseHref = str_repeat('../', $subdirLevel); + + $htmlContents = str_replace('', '', $htmlContents); + $htmlContents = str_replace('success(sprintf("The Symfony Docs were successfully built at %s", realpath($outputDir))); + } else { + $io->error(sprintf("There were some errors while building the docs:\n\n%s\n", $result->getErrorTrace())); + $io->newLine(); + $io->comment('Tip: you can add the -v, -vv or -vvv flags to this command to get debug information.'); + + return 1; + } + + return 0; + }) + ->getApplication() + ->setDefaultCommand('build-docs', true) + ->run(); diff --git a/_build/composer.json b/_build/composer.json new file mode 100644 index 00000000000..f77976b10f4 --- /dev/null +++ b/_build/composer.json @@ -0,0 +1,22 @@ +{ + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "platform": { + "php": "8.3" + }, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "allow-plugins": { + "symfony/flex": true + } + }, + "require": { + "php": ">=8.3", + "symfony/console": "^6.2", + "symfony/process": "^6.2", + "symfony-tools/docs-builder": "^0.27" + } +} diff --git a/_build/composer.lock b/_build/composer.lock new file mode 100644 index 00000000000..b9a4646f8ae --- /dev/null +++ b/_build/composer.lock @@ -0,0 +1,1792 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "e38eca557458275428db96db370d2c74", + "packages": [ + { + "name": "doctrine/event-manager", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2024-05-22T20:47:39+00:00" + }, + { + "name": "doctrine/rst-parser", + "version": "0.5.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/rst-parser.git", + "reference": "ca7f5f31f9ea58fde5aeffe0f7b8eb569e71a104" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/rst-parser/zipball/ca7f5f31f9ea58fde5aeffe0f7b8eb569e71a104", + "reference": "ca7f5f31f9ea58fde5aeffe0f7b8eb569e71a104", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^1.0 || ^2.0", + "php": "^7.2 || ^8.0", + "symfony/filesystem": "^4.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/finder": "^4.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/string": "^5.3 || ^6.0 || ^7.0", + "symfony/translation-contracts": "^1.1 || ^2.0 || ^3.0", + "twig/twig": "^2.9 || ^3.3" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "gajus/dindent": "^2.0.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.2", + "phpstan/phpstan-strict-rules": "^1.4", + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.0", + "symfony/css-selector": "4.4 || ^5.2 || ^6.0 || ^7.0", + "symfony/dom-crawler": "4.4 || ^5.2 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\RST\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Passault", + "email": "g.passault@gmail.com", + "homepage": "http://www.gregwar.com/" + }, + { + "name": "Jonathan H. Wage", + "email": "jonwage@gmail.com", + "homepage": "https://jwage.com" + } + ], + "description": "PHP library to parse reStructuredText documents and generate HTML or LaTeX documents.", + "homepage": "https://github.com/doctrine/rst-parser", + "keywords": [ + "html", + "latex", + "markup", + "parser", + "reStructuredText", + "rst" + ], + "support": { + "issues": "https://github.com/doctrine/rst-parser/issues", + "source": "https://github.com/doctrine/rst-parser/tree/0.5.6" + }, + "time": "2024-01-14T11:02:23+00:00" + }, + { + "name": "masterminds/html5", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" + }, + "time": "2024-03-31T07:05:07+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "scrivo/highlight.php", + "version": "v9.18.1.10", + "source": { + "type": "git", + "url": "https://github.com/scrivo/highlight.php.git", + "reference": "850f4b44697a2552e892ffe71490ba2733c2fc6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/850f4b44697a2552e892ffe71490ba2733c2fc6e", + "reference": "850f4b44697a2552e892ffe71490ba2733c2fc6e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.4" + }, + "require-dev": { + "phpunit/phpunit": "^4.8|^5.7", + "sabberworm/php-css-parser": "^8.3", + "symfony/finder": "^2.8|^3.4|^5.4", + "symfony/var-dumper": "^2.8|^3.4|^5.4" + }, + "suggest": { + "ext-mbstring": "Allows highlighting code with unicode characters and supports language with unicode keywords" + }, + "type": "library", + "autoload": { + "files": [ + "HighlightUtilities/functions.php" + ], + "psr-0": { + "Highlight\\": "", + "HighlightUtilities\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Geert Bergman", + "homepage": "http://www.scrivo.org/", + "role": "Project Author" + }, + { + "name": "Vladimir Jimenez", + "homepage": "https://allejo.io", + "role": "Maintainer" + }, + { + "name": "Martin Folkers", + "homepage": "https://twobrain.io", + "role": "Contributor" + } + ], + "description": "Server side syntax highlighter that supports 185 languages. It's a PHP port of highlight.js", + "keywords": [ + "code", + "highlight", + "highlight.js", + "highlight.php", + "syntax" + ], + "support": { + "issues": "https://github.com/scrivo/highlight.php/issues", + "source": "https://github.com/scrivo/highlight.php" + }, + "funding": [ + { + "url": "https://github.com/allejo", + "type": "github" + } + ], + "time": "2022-12-17T21:53:22+00:00" + }, + { + "name": "symfony-tools/docs-builder", + "version": "0.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony-tools/docs-builder.git", + "reference": "720b52b2805122a4c08376496bd9661944c2624a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony-tools/docs-builder/zipball/720b52b2805122a4c08376496bd9661944c2624a", + "reference": "720b52b2805122a4c08376496bd9661944c2624a", + "shasum": "" + }, + "require": { + "doctrine/rst-parser": "^0.5", + "ext-curl": "*", + "ext-json": "*", + "php": ">=8.3", + "scrivo/highlight.php": "^9.18.1", + "symfony/console": "^5.2 || ^6.0 || ^7.0", + "symfony/css-selector": "^5.2 || ^6.0 || ^7.0", + "symfony/dom-crawler": "^5.2 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.2 || ^6.0 || ^7.0", + "symfony/finder": "^5.2 || ^6.0 || ^7.0", + "symfony/http-client": "^5.2 || ^6.0 || ^7.0", + "twig/twig": "^2.14 || ^3.3" + }, + "require-dev": { + "gajus/dindent": "^2.0", + "masterminds/html5": "^2.7", + "symfony/phpunit-bridge": "^5.2 || ^6.0 || ^7.0", + "symfony/process": "^5.2 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/docs-builder" + ], + "type": "project", + "autoload": { + "psr-4": { + "SymfonyDocsBuilder\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The build system for Symfony's documentation", + "support": { + "issues": "https://github.com/symfony-tools/docs-builder/issues", + "source": "https://github.com/symfony-tools/docs-builder/tree/0.27.0" + }, + "time": "2025-03-21T09:48:45+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/799445db3f15768ecc382ac5699e6da0520a0a04", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-07T12:07:30+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7", + "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7", + "shasum": "" + }, + "require": { + "masterminds/html5": "^2.6", + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases DOM navigation for HTML and XML documents", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dom-crawler/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-17T15:53:07+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-25T15:15:23+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.2.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-30T19:00:17+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-13T10:27:23+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-07T08:49:48+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v6.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3", + "reference": "7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-04T13:35:48+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/string", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-13T13:31:26+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c", + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "twig/twig", + "version": "v3.20.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "3468920399451a384bef53cf7996965f7cd40183" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/3468920399451a384bef53cf7996965f7cd40183", + "reference": "3468920399451a384bef53cf7996965f7cd40183", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.20.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2025-02-13T08:34:43+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.3" + }, + "platform-dev": {}, + "platform-overrides": { + "php": "8.3" + }, + "plugin-api-version": "2.6.0" +} diff --git a/_build/maintainer_guide.rst b/_build/maintainer_guide.rst new file mode 100644 index 00000000000..9758b4e7397 --- /dev/null +++ b/_build/maintainer_guide.rst @@ -0,0 +1,378 @@ +Symfony Docs Maintainer Guide +============================= + +The `symfony/symfony-docs`_ repository stores the Symfony project documentation +and is managed by the `Symfony Docs team`_. This article explains in detail some +of those management tasks, so it's only useful for maintainers and not regular +readers or Symfony developers. + +Reviewing Pull Requests +----------------------- + +All the recommendations of the `Symfony's respectful review comments`_ apply, +but there are extra things to keep in mind for maintainers: + +* Always be nice in all interactions with all contributors. +* Be extra-patient with new contributors (GitHub shows a special badge for them). +* Don't assume that contributors know what you think is obvious (e.g. lots of + them don't know what to "squash commits" means). +* Don't use acronyms like IMO, IIRC, etc. or complex English words (most + contributors are not native in English and it's intimidating for them). +* Never engage in a heated discussion. Lock it right away using GitHub. +* Never discuss non-tech issues. Some PRs are related to our Diversity initiative + and some people always try to drag you into politics. Never engage in that and + lock the issue/PR as off-topic on GitHub. + +Fixing Minor Issues Yourself +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It's common for new contributors to make lots of minor mistakes in the syntax +of the RST format used in the docs. It's also common for non English speakers to +make minor typos. + +Even if your intention is good, if you add lots of comments when reviewing a +first contribution, that person will probably not contribute again. It's better +to fix the minor errors and typos yourself while merging. If that person +contributes again, it's OK to mention some of the minor issues to educate them. + +.. code-block:: terminal + + $ gh merge 11059 + + Working on symfony/symfony-docs (branch 6.2) + Merging Pull Request 11059: dmaicher/patch-3 + + ... + + # This is important!! Say NO to push the changes now + Push the changes now? (Y/n) n + Now, push with: git push gh "6.2" refs/notes/github-comments + + # Now, open your editor and make the needed changes ... + + $ git commit -a + # Use "Minor reword", "Minor tweak", etc. as the commit message + + # now run the 'push' command shown above by 'gh' (it's different each time) + $ git push gh "6.2" refs/notes/github-comments + +Merging Pull Requests +--------------------- + +Technical Requirements +~~~~~~~~~~~~~~~~~~~~~~ + +* `Git`_ installed and properly configured. +* ``gh`` tool fully installed according to its installation instructions + (GitHub token configured, Git remote configured, etc.) + This is a proprietary CLI tool which only Symfony team members have access to. +* Some previous Git experience, specially merging pull requests. + +First Setup +~~~~~~~~~~~ + +First, fork the using the GitHub web +interface. Then: + +.. code-block:: terminal + + # Clone your fork + $ git clone https://github.com//symfony-docs.git + + $ cd symfony-docs/ + + # Add the original repo as 'upstream' remote + $ git remote add upstream https://github.com/symfony/symfony-docs + + # Add the original repo as 'gh' remote (needed for the 'gh' tool) + $ git remote add gh https://github.com/symfony/symfony-docs + + # Configure 'gh' in Git as the remote used by the 'gh' tool + $ git config gh.remote gh + +Merging Process +~~~~~~~~~~~~~~~ + +At first, it's common to make mistakes and merge things badly. Don't worry. This +has happened to all of us and we've always been able to recover from any mistake. + +Step 1: Select the right branch to merge +........................................ + +PRs must be merged in the oldest maintained branch where they are applicable: + +* Here you can find the currently maintained branches: https://symfony.com/roadmap. +* Typos and old undocumented features are merged into the oldest maintained branch. +* New features are merged into the branch where they were introduced. This + usually means ``master``. And don't forget to check that new feature includes + the ``versionadded`` directive. + +It's very common for contributors (specially newcomers) to select the wrong +branch for their PRs, so we must always check if the change should go to the +proposed branch or not. + +If the branch is wrong, there's no need to ask the contributor to rebase. The +``gh`` tool can do that for us. + +Step 2: Merge the pull request +.............................. + +Never use GitHub's web interface (or desktop clients) to merge PRs or to solve +merge conflicts. Always use the ``gh`` tool for anything related to merges. + +We require two approval votes from team members before merging a PR, except if +it's a typo, a small change or clearly an error. + +If a PR contains lots of commits, there's no need to ask the contributor to +squash them. The ``gh`` tool does that automatically. The only exceptions are +when commits are made by more than one person and when there's a merge commit. +``gh`` can't squash commits in those cases, so it's better to ask to the +original contributor. + +.. code-block:: terminal + + $ cd symfony-docs/ + + # make sure that your local branch is updated + $ git checkout 4.4 + $ git fetch upstream + $ git merge upstream/4.4 + + # merge any PR passing its GitHub number as argument + $ gh merge 11159 + + # the gh tool will ask you some questions... + + # push your changes (you can merge several PRs and push once at the end) + $ git push origin + $ git push upstream + +It's common to have to change the branch where a PR is merged. Instead of asking +the contributors to rebase their PRs, the "gh" tool can change the branch with +the ``-s`` option: + +.. code-block:: terminal + + # e.g. this PR was sent against 'master', but it's merged in '4.4' + $ gh merge 11160 -s 4.4 + +Sometimes, when changing the branch, you may face rebase issues, but they are +usually simple to fix: + +.. code-block:: terminal + + $ gh merge 11160 -s 4.4 + + ... + + Unable to rebase the patch for pull/11183 + The command "'git' 'rebase' '--onto' '4.4' '5.0' 'pull/11160'" failed. + Exit Code: 128(Invalid exit argument) + + [...] + Auto-merging reference/forms/types/entity.rst + CONFLICT (content): Merge conflict in reference/forms/types/entity.rst + Patch failed at 0001 Update entity.rst + The copy of the patch that failed is found in: .git/rebase-apply/patch + + # Now, fix all the conflicts using your editor + + # Add the modified files and continue the rebase + $ git add reference/forms/types/entity.rst ... + $ git rebase --continue + + # Lastly, re-run the exact same original command that resulted in a conflict + # There's no need to change the branch or do anything else. + $ gh merge 11160 -s 4.4 + + The previous run had some conflicts. Do you want to resume the merge? (Y/n) + +Later in this article you can find a troubleshooting section for the errors that +you will usually face while merging. + +Step 3: Merge it into the other branches +........................................ + +If a PR has not been merged in ``master``, you must merge it up into all the +maintained branches until ``master``. Imagine that you are merging a PR against +``4.4`` and the maintained branches are ``4.4``, ``5.0`` and ``master``: + +.. code-block:: terminal + + $ git fetch upstream + + $ git checkout 4.4 + $ git merge upstream/4.4 + + $ gh merge 11159 + $ git push origin + $ git push upstream + + $ git checkout 5.0 + $ git merge upstream/5.0 + $ git merge --log 4.4 + # here you can face several errors explained later + $ git push origin + $ git push upstream + + $ git checkout master + $ git merge upstream/master + $ git merge --log 5.0 + $ git push origin + $ git push upstream + +.. tip:: + + If you followed the full ``gh`` installation instructions you can remove the + ``--log`` option in the above commands. + +.. tip:: + + When the support of a Symfony branch ends, it's recommended to delete your + local branch to avoid merging in it unawarely: + + .. code-block:: terminal + + # if Symfony 3.3 goes out of maintenance today, delete your local branch + $ git branch -D 3.3 + +Troubleshooting +~~~~~~~~~~~~~~~ + +Wrong merge of your local branch +................................ + +When updating your local branches before merging: + +.. code-block:: terminal + + $ git fetch upstream + $ git checkout 4.4 + $ git merge upstream/4.4 + +It's possible that you merge a wrong upstream branch unawarely. It's usually +easy to spot because you'll see lots of conflicts: + +.. code-block:: terminal + + # DON'T DO THIS! It's a wrong branch merge + $ git checkout 4.4 + $ git merge upstream/5.0 + +As long as you don't push this wrong merge, there's no problem. Delete your +local branch and check it out again: + +.. code-block:: terminal + + $ git checkout master + $ git branch -D 4.4 + $ git checkout 4.4 upstream/4.4 + +If you did push the wrong branch merge, ask for help in the documentation +mergers chat and we'll help solve the problem. + +Solving merge conflicts +....................... + +When merging things to upper branches, most of the times you'll see conflicts: + +.. code-block:: terminal + + $ git checkout 5.0 + $ git merge upstream/5.0 + $ git merge --log 4.4 + + Auto-merging security/entity_provider.rst + Auto-merging logging/monolog_console.rst + Auto-merging form/dynamic_form_modification.rst + Auto-merging components/phpunit_bridge.rst + CONFLICT (content): Merge conflict in components/phpunit_bridge.rst + Automatic merge failed; fix conflicts and then commit the result. + +Solve the conflicts with your editor (look for occurrences of ``<<<<``, which is +the marker used by Git for conflicts) and then do this: + +.. code-block:: terminal + + # add all the conflicting files that you fixed + $ git add components/phpunit_bridge.rst + $ git commit -a + $ git push origin + $ git push upstream + +.. tip:: + + When there are lots of conflicts, look for ``<<<<<`` with your editor in all + docs before committing the changes. It's common to forget about some of them. + If you prefer, you can run this too: ``git grep --cached "<<<<<"``. + +Merging deleted files +..................... + +A common cause of conflict when merging PRs into upper branches are files which +were modified by the PR but no longer exist in newer branches: + +.. code-block:: terminal + + $ git checkout 5.0 + $ git merge upstream/5.0 + $ git merge --log 4.4 + + Auto-merging translation/debug.rst + CONFLICT (modify/delete): service_container/scopes.rst deleted in HEAD and + modified in 4.4. Version 4.4 of service_container/scopes.rst left in tree. + Auto-merging service_container.rst + +If the contents of the deleted file were moved to a different file in newer +branches, redo the changes in the new file. Then, delete the file that Git left +in the tree as follows: + +.. code-block:: terminal + + # delete all the conflicting files that no longer exist in this branch + $ git rm service_container/scopes.rst + $ git commit -a + $ git push origin + $ git push upstream + +Merging in the wrong branch +........................... + +A Pull Request was made against ``5.x`` but it should be merged in ``5.1`` and you +forgot to merge as ``gh merge NNNNN -s 5.1`` to change the merge branch. Solution: + +.. code-block:: terminal + + $ git checkout 5.1 + $ git cherry-pick -m 1 + $ git checkout 5.x + $ git revert -m 1 + # now continue with the normal "upmerging" + $ git checkout 5.2 + $ git merge 5.1 + $ ... + +Merging while the target branch changed +....................................... + +Sometimes, someone else merges a PR in ``5.x`` at the same time as you are +doing it. In these cases, ``gh merge ...`` fails to push. Solve this by +resetting your local branch and restarting the merge: + +.. code-block:: terminal + + $ gh merge ... + # this failed + + # fetch the updated 5.x branch from GitHub + $ git fetch upstream + $ git checkout 5.x + $ git reset --hard upstream/5.x + + # restart the merge + $ gh merge ... + +.. _`symfony/symfony-docs`: https://github.com/symfony/symfony-docs +.. _`Symfony Docs team`: https://github.com/orgs/symfony/teams/team-symfony-docs +.. _`Symfony's respectful review comments`: https://symfony.com/doc/current/contributing/community/review-comments.html +.. _`Git`: https://git-scm.com/ diff --git a/_build/redirection_map b/_build/redirection_map new file mode 100644 index 00000000000..ee14c191025 --- /dev/null +++ b/_build/redirection_map @@ -0,0 +1,576 @@ +/book/index /index +/cookbook/index /index +/book/stable_api /contributing/code/bc +/book/internals /reference/events +/configuration/apache_router /routing +/cookbook/console/sending_emails /cookbook/console/request_context +/cookbook/deployment-tools /cookbook/deployment/tools +/cookbook/doctrine/migrations /bundles/DoctrineFixturesBundle/index +/cookbook/doctrine/doctrine_fixtures /bundles/DoctrineFixturesBundle/index +/cookbook/doctrine/mongodb /bundles/DoctrineMongoDBBundle/index +/cookbook/form/dynamic_form_generation /cookbook/form/dynamic_form_modification +/cookbook/form/simple_signup_form_with_mongodb /bundles/DoctrineMongoDBBundle/form +/cookbook/email /email +/cookbook/gmail /cookbook/email/gmail +/cookbook/console /components/console +/cookbook/tools/autoloader https://github.com/symfony/class-loader +/cookbook/tools/finder /components/finder +/cookbook/service_container/parentservices /service_container/parent_services +/cookbook/service_container/factories /service_container/factories +/cookbook/service_container/tags /service_container/tags +/reference/configuration/mongodb /bundles/DoctrineMongoDBBundle/config +/reference/YAML /components/yaml +/cookbook/console/generating_urls /cookbook/console/sending_emails +/cmf/reference/configuration/block /cmf/bundles/block/configuration +/cmf/reference/configuration/content /cmf/bundles/content/configuration +/cmf/reference/configuration/core /cmf/bundles/core/configuration +/cmf/reference/configuration/create /cmf/bundles/create/configuration +/cmf/reference/configuration/media /cmf/bundles/media/configuration +/cmf/reference/configuration/menu /cmf/bundles/menu/configuration +/cmf/reference/configuration/phpcr_odm /cmf/bundles/phpcr_odm/configuration +/cmf/reference/configuration/routing /cmf/bundles/routing/configuration +/cmf/reference/configuration/search /cmf/bundles/search/configuration +/cmf/reference/configuration/seo /cmf/bundles/seo/configuration +/cmf/reference/configuration/simple_cms /cmf/bundles/simple_cms/configuration +/cmf/reference/configuration/tree_browser /cmf/bundles/tree_browser/configuration +/cmf/cookbook/exposing_content_via_rest /cmf/bundles/content/exposing_content_via_rest +/cmf/cookbook/creating_a_cms/auto-routing /cmf/tutorial/auto-routing +/cmf/cookbook/creating_a_cms/conclusion /cmf/tutorial/conclusion +/cmf/cookbook/creating_a_cms/content-to-controllers /cmf/tutorial/content-to-controllers +/cmf/cookbook/creating_a_cms/getting-started /cmf/tutorial/getting-started +/cmf/cookbook/creating_a_cms/index /cmf/tutorial/index +/cmf/cookbook/creating_a_cms/introduction /cmf/tutorial/introduction +/cmf/cookbook/creating_a_cms/make-homepage /cmf/tutorial/make-homepage +/cmf/cookbook/creating_a_cms/sonata-admin /cmf/tutorial/sonata-admin +/cmf/cookbook/creating_a_cms/the-frontend /cmf/tutorial/the-frontend +/cookbook/upgrading /cookbook/upgrade/index +/cookbook/security/voters_data_permission /cookbook/security/voters +/cookbook/configuration/pdo_session_storage /cookbook/doctrine/pdo_session_storage +/cookbook/configuration/mongodb_session_storage /cookbook/doctrine/mongodb_session_storage +/cookbook/service_container/event_listener /event_dispatcher +/create_framework/http-foundation /create_framework/http_foundation +/create_framework/front-controller /create_framework/front_controller +/create_framework/http-kernel-controller-resolver /create_framework/http_kernel_controller_resolver +/create_framework/separation-of-concerns /create_framework/separation_of_concerns +/create_framework/unit-testing /create_framework/unit_testing +/create_framework/event-dispatcher /create_framework/event_dispatcher +/create_framework/http-kernel-httpkernelinterface /create_framework/http_kernel_httpkernelinterface +/create_framework/http-kernel-httpkernel-class /create_framework/http_kernel_httpkernel_class +/create_framework/dependency-injection /create_framework/dependency_injection +/cookbook/doctrine/file_uploads /cookbook/controller/upload_file +/book/installation /setup +/book/page_creation /page_creation +/book/controller /controller +/book/routing /routing +/book/templating /templating +/book/bundles /bundles +/book/doctrine /doctrine +/book/testing /testing +/book/validation /validation +/book/forms /forms +/book/security /security +/book/http_cache /http_cache +/book/translation /translation +/book/service_container /service_container +/book/http_fundamentals /introduction/http_fundamentals +/book/from_flat_php_to_symfony2 /introduction/from_flat_php_to_symfony2 +/book/configuration /configuration +/book/propel /propel/propel +/book/performance /performance +/bundles/installation /bundles +/cookbook/assetic/apply_to_option /frontend/assetic/apply_to_option +/cookbook/assetic/asset_management /frontend/assetic/asset_management +/cookbook/assetic/index /frontend/assetic/index +/cookbook/assetic/jpeg_optimize /frontend/assetic/jpeg_optimize +/cookbook/assetic/php /frontend/assetic/php +/cookbook/assetic/uglifyjs /frontend/assetic/uglifyjs +/cookbook/assetic/yuicompressor /frontend/assetic/yuicompressor +/assetic /frontend/assetic/index +/assetic/apply_to_option /frontend/assetic/apply_to_option +/assetic/asset_management /frontend/assetic/asset_management +/assetic/jpeg_optimize /frontend/assetic/jpeg_optimize +/assetic/php /frontend/assetic/php +/assetic/uglifyjs /frontend/assetic/uglifyjs +/assetic/yuicompressor /frontend/assetic/yuicompressor +/cookbook/bundles/best_practices /bundles/best_practices +/cookbook/bundles/configuration /bundles/configuration +/cookbook/bundles/extension /bundles/extension +/cookbook/bundles/index /bundles +/cookbook/bundles/inheritance /bundles/inheritance +/cookbook/bundles/installation /bundles +/cookbook/bundles/override /bundles/override +/cookbook/bundles/prepend_extension /bundles/prepend_extension +/cookbook/bundles/remove /bundles +/bundles/remove /bundles +/cookbook/cache/form_csrf_caching /http_cache/form_csrf_caching +/cookbook/cache/varnish /http_cache/varnish +/cookbook/composer /setup/composer +/cookbook/configuration/apache_router /routing +/cookbook/configuration/configuration_organization /configuration/configuration_organization +/cookbook/configuration/environments /configuration/environments +/cookbook/configuration/external_parameters /configuration/external_parameters +/cookbook/configuration/front_controllers_and_kernel /configuration/front_controllers_and_kernel +/cookbook/configuration/micro-kernel-trait /configuration/micro_kernel_trait +/cookbook/configuration/index /configuration +/cookbook/configuration/override_dir_structure /configuration/override_dir_structure +/cookbook/configuration/using_parameters_in_dic /configuration/using_parameters_in_dic +/cookbook/configuration/web_server_configuration /setup/web_server_configuration +/cookbook/console/command_in_controller /console/command_in_controller +/cookbook/console/commands_as_services /console/commands_as_services +/cookbook/console/console_command /console +/cookbook/console/index /console +/cookbook/console/logging /console +/cookbook/console/request_context /console/request_context +/cookbook/console/style /console/style +/cookbook/console/usage /console +/console/usage /console +/cookbook/controller/csrf_token_validation /security/csrf +/cookbook/controller/error_pages /controller/error_pages +/cookbook/controller/forwarding /controller/forwarding +/cookbook/controller/index /controller +/cookbook/controller/service /controller/service +/cookbook/controller/upload_file /controller/upload_file +/cookbook/debugging / +/debug/debugging / +/cookbook/deployment/tools /deployment/tools +/cookbook/doctrine/common_extensions /doctrine/common_extensions +/cookbook/doctrine/console /doctrine +/cookbook/doctrine/custom_dql_functions /doctrine/custom_dql_functions +/cookbook/doctrine/dbal /doctrine/dbal +/cookbook/doctrine/event_listeners_subscribers /doctrine/event_listeners_subscribers +/cookbook/doctrine/index /doctrine +/cookbook/doctrine/mapping_model_classes /doctrine +/doctrine/mapping_model_classes /doctrine +/cookbook/doctrine/mongodb_session_storage /doctrine/mongodb_session_storage +/cookbook/doctrine/multiple_entity_managers /doctrine/multiple_entity_managers +/cookbook/doctrine/pdo_session_storage /doctrine/pdo_session_storage +/cookbook/doctrine/registration_form /doctrine/registration_form +/cookbook/doctrine/resolve_target_entity /doctrine/resolve_target_entity +/cookbook/doctrine/reverse_engineering /doctrine/reverse_engineering +/doctrine/repository /doctrine +/doctrine/console /doctrine +/cookbook/email/cloud /email +/cookbook/email/dev_environment /email/dev_environment +/cookbook/email/email /email +/cookbook/email/gmail /email +/cookbook/email/index /email +/cookbook/email/spool /email/spool +/cookbook/email/testing /email/testing +/cookbook/event_dispatcher/before_after_filters /event_dispatcher#event-dispatcher-before-after-filters +/event_dispatcher/before_after_filters /event_dispatcher#event-dispatcher-before-after-filters +/cookbook/event_dispatcher/class_extension /event_dispatcher/class_extension +/cookbook/event_dispatcher/event_listener /event_dispatcher +/cookbook/event_dispatcher/index /event_dispatcher +/cookbook/event_dispatcher/method_behavior /event_dispatcher/method_behavior +/event_dispatcher/method_behavior /event_dispatcher#event-dispatcher-method-behavior +/cookbook/expressions /security/expressions +/expressions /security/expressions +/cookbook/form/create_custom_field_type /form/create_custom_field_type +/cookbook/form/create_form_type_extension /form/create_form_type_extension +/cookbook/form/data_transformers /form/data_transformers +/cookbook/form/direct_submit /form/direct_submit +/cookbook/form/dynamic_form_modification /form/dynamic_form_modification +/cookbook/form/form_collections /form/form_collections +/cookbook/form/form_customization /form/form_customization +/cookbook/form/index /forms +/cookbook/form/inherit_data_option /form/inherit_data_option +/cookbook/form/unit_testing /form/unit_testing +/cookbook/form/use_empty_data /form/use_empty_data +/cookbook/frontend/bower /frontend +/cookbook/frontend/index /frontend +/cookbook/install/unstable_versions /setup/unstable_versions +/cookbook/install/bundles /setup/bundles +/cookbook/install/index /setup +/cookbook/install/upgrade_major /setup/upgrade_major +/cookbook/install/upgrade_minor /setup/upgrade_minor +/cookbook/install/upgrade_patch /setup/upgrade_patch +/cookbook/logging/channels_handlers /logging/channels_handlers +/cookbook/logging/index /logging +/cookbook/logging/monolog /logging +/cookbook/logging/monolog_console /logging/monolog_console +/cookbook/logging/monolog_email /logging/monolog_email +/cookbook/logging/monolog_regex_based_excludes /logging/monolog_regex_based_excludes +/cookbook/profiler/data_collector /profiler#profiler-data-collector +/profiler/data_collector /profiler#profiler-data-collector +/cookbook/profiler/index /profiler +/cookbook/profiler/matchers /profiler/matchers +/cookbook/profiler/profiling_data /profiler/profiling_data +/cookbook/profiler/storage /profiler/storage +/cookbook/psr7 /components/psr7 +/cookbook/request/index /request +/cookbook/request/load_balancer_reverse_proxy /deployment/proxies +/cookbook/request/mime_type /reference/configuration/framework +/cookbook/routing/conditions /routing/conditions +/cookbook/routing/custom_route_loader /routing/custom_route_loader +/cookbook/routing/debug /routing/debug +/cookbook/routing/external_resources /routing/external_resources +/cookbook/routing/extra_information /routing/extra_information +/cookbook/routing/index /routing +/cookbook/routing/method_parameters /routing/requirements +/cookbook/routing/optional_placeholders /routing/optional_placeholders +/cookbook/routing/redirect_in_config /routing/redirect_in_config +/cookbook/routing/redirect_trailing_slash /routing/redirect_trailing_slash +/cookbook/routing/requirements /routing/requirements +/cookbook/routing/routing_from_database /routing/routing_from_database +/cookbook/routing/scheme /routing/scheme +/cookbook/routing/service_container_parameters /routing/service_container_parameters +/cookbook/routing/slash_in_parameter /routing/slash_in_parameter +/cookbook/security/access_control /security/access_control +/cookbook/security/acl /security/acl +/cookbook/security/acl_advanced /security/acl_advanced +/cookbook/security/api_key_authentication /security/api_key_authentication +/cookbook/security/csrf_in_login_form /security/csrf +/cookbook/security/custom_authentication_provider /security/custom_authentication_provider +/cookbook/security/custom_password_authenticator /security/custom_password_authenticator +/cookbook/security/custom_provider /security/custom_provider +/cookbook/security/entity_provider /security/entity_provider +/cookbook/security/firewall_restriction /security/firewall_restriction +/cookbook/security/force_https /security/force_https +/cookbook/security/form_login /security/form_login +/cookbook/security/form_login_setup /security/form_login_setup +/cookbook/security/guard-authentication /security/guard_authentication +/cookbook/security/host_restriction /security/host_restriction +/cookbook/security/impersonating_user /security/impersonating_user +/cookbook/security/ldap /security/ldap +/cookbook/security/multiple_guard_authenticators /security/multiple_guard_authenticators +/cookbook/security/index /security +/cookbook/security/multiple_user_providers /security/multiple_user_providers +/cookbook/security/named_encoders /security/named_encoders +/cookbook/security/pre_authenticated /security/pre_authenticated +/cookbook/security/remember_me /security/remember_me +/cookbook/security/securing_services /security/securing_services +/cookbook/security/target_path /security/target_path +/cookbook/security/user_checkers /security/user_checkers +/cookbook/security/voters /security/voters +/cookbook/serializer /serializer +/cookbook/service_container/compiler_passes /service_container/compiler_passes +/cookbook/service_container/index /service_container +/cookbook/service_container/scopes /service_container/scopes +/cookbook/service_container/shared /service_container/shared +/cookbook/session/avoid_session_start /session/avoid_session_start +/cookbook/session/index /session +/cookbook/session/limit_metadata_writes /reference/configuration/framework +/session/limit_metadata_writes /reference/configuration/framework +/cookbook/session/locale_sticky_session /session#locale-sticky-session +/cookbook/locale_sticky_session /session#locale-sticky-session +/cookbook/session/php_bridge /session/php_bridge +/cookbook/session/proxy_examples /session/proxy_examples +/cookbook/session/sessions_directory /session/sessions_directory +/cookbook/symfony1 /introduction/symfony1 +/cookbook/templating/global_variables /templating#templating-global-variables +/templating/global_variables /templating#templating-global-variables +/cookbook/templating/index /templating +/cookbook/templating/namespaced_paths /templating/namespaced_paths +/cookbook/templating/PHP /templating/PHP +/cookbook/templating/render_without_controller /templating/render_without_controller +/cookbook/templating/twig_extension /templating/twig_extension +/cookbook/testing/bootstrap /testing/bootstrap +/cookbook/testing/database /testing/database +/cookbook/testing/doctrine /testing/doctrine +/cookbook/testing/http_authentication /testing/http_authentication +/cookbook/testing/index /testing +/cookbook/testing/insulating_clients /testing/insulating_clients +/cookbook/testing/profiling /testing/profiling +/cookbook/testing/simulating_authentication /testing/simulating_authentication +/cookbook/upgrade/bundles /upgrade/patch_version +/cookbook/upgrade/index /setup/upgrade_major +/cookbook/upgrade/major_version /setup/upgrade_minor +/cookbook/upgrade/minor_version /setup/upgrade_major +/cookbook/upgrade/patch_version /upgrade/bundles +/cookbook/validation/custom_constraint /validation/custom_constraint +/cookbook/validation/group_service_resolver /form/validation_group_service_resolver +/cookbook/validation/index /validation +/cookbook/validation/severity /validation/severity +/cookbook/web_server/built_in /setup/built_in_web_server +/cookbook/web_server/index /setup/built_in_web_server +/cookbook/web_services/index /controller/soap_web_service +/cookbook/web_services/php_soap_extension /controller/soap_web_service +/cookbook/workflow/homestead /setup/homestead +/cookbook/workflow/index /setup +/cookbook/workflow/new_project_git /setup +/cookbook/workflow/new_project_svn /setup +/setup/new_project_git /setup +/setup/new_project_svn /setup +/components/asset/index /components/asset +/components/asset/introduction /components/asset +/components/browser_kit/index /components/browser_kit +/components/browser_kit/introduction /components/browser_kit +/components/class_loader/introduction https://github.com/symfony/class-loader +/components/class_loader/index https://github.com/symfony/class-loader +/components/class_loader/cache_class_loader https://github.com/symfony/class-loader +/components/class_loader/class_loader https://github.com/symfony/class-loader +/components/class_loader/class_map_generator https://github.com/symfony/class-loader +/components/class_loader/debug_class_loader https://github.com/symfony/class-loader +/components/class_loader/map_class_loader https://github.com/symfony/class-loader +/components/class_loader/map_class_loader https://github.com/symfony/class-loader +/components/class_loader/psr4_class_loader https://github.com/symfony/class-loader +/components/config/introduction /components/config +/components/config/index /components/config +/components/console/helpers/tablehelper /components/console/helpers/table +/components/console/helpers/progresshelper /components/console/helpers/progressbar +/components/console/helpers/dialoghelper /components/console/helpers/questionhelper +/components/console/introduction /components/console +/components/console/index /components/console +/components/debug/class_loader /components/debug +/components/debug/introduction /components/debug +/components/debug/index /components/debug +/components/dependency_injection/advanced /service_container/alias_private +/components/dependency_injection/autowiring /service_container/autowiring +/components/dependency_injection/definitions /service_container/definitions +/components/dependency_injection/introduction /components/dependency_injection +/components/dependency_injection/index /components/dependency_injection +/components/dependency_injection/factories /service_container/factories +/components/dependency_injection/lazy_services /service_container/lazy_services +/components/dependency_injection/parameters /service_container/parameters +/components/dependency_injection/parentservices /service_container/parent_services +/components/dependency_injection/parent_services /service_container/parent_services +/components/dependency_injection/synthetic_services /service_container/synthetic_services +/components/dependency_injection/tags /service_container/tags +/components/dependency_injection/types /service_container/injection_types +/components/event_dispatcher/index /components/event_dispatcher +/components/event_dispatcher/introduction /components/event_dispatcher +/components/expression_language/introduction /components/expression_language +/components/expression_language/index /components/expression_language +/components/filesystem/introduction /components/filesystem +/components/filesystem/index /components/filesystem +/components/form/form_events /form/events +/components/form/introduction /components/form +/components/form/index /components/form +/components/form/type_guesser /form/type_guesser +/components/http_foundation/index /components/http_foundation +/components/http_foundation/introduction /components/http_foundation +/request/load_balancer_reverse_proxy /deployment/proxies +/components/http_foundation/trusting_proxies /deployment/proxies +/components/http_kernel/introduction /components/http_kernel +/components/http_kernel/index /components/http_kernel +/components/property_access/introduction /components/property_access +/components/property_access/index /components/property_access +/components/routing/index https://github.com/symfony/routing +/components/routing/introduction https://github.com/symfony/routing +/components/routing/hostname_pattern /routing/hostname_pattern +/components/security/introduction /components/security +/components/security/index /components/security +/components/templating/introduction https://github.com/symfony/templating +/components/templating/index https://github.com/symfony/templating +/components/templating/helpers/assetshelper https://github.com/symfony/templating +/components/templating/helpers/slotshelper https://github.com/symfony/templating +/components/translation/introduction /components/translation +/components/translation/index /components/translation +/components/var_dumper/introduction /components/var_dumper +/components/var_dumper/index /components/var_dumper +/components/yaml/introduction /components/yaml +/components/yaml/index /components/yaml +/console/logging /console +/controller/csrf_token_validation /security/csrf +/deployment/tools /deployment +/form/csrf_protection /security/csrf +/install/bundles /setup/bundles +/email/gmail /email +/email/cloud /email +/event_dispatcher/class_extension /event_dispatcher +/form /forms +/form/use_virtual_forms /form/inherit_data_option +/frontend/assetic /frontend/assetic/index +/frontend/assetic/apply_to_option /frontend/assetic/index +/frontend/assetic/asset_management /frontend/assetic/index +/frontend/assetic/jpeg_optimize /frontend/assetic/index +/frontend/assetic/php /frontend/assetic/index +/frontend/assetic/uglifyjs /frontend/assetic/index +/frontend/assetic/yuicompressor /frontend/assetic/index +/reference/configuration/assetic /frontend/assetic/index +/security/target_path /security +/security/csrf_in_login_form /security/csrf +/service_container/service_locators /service_container/service_subscribers_locators +/service_container/third_party /service_container +/templating/templating_service /templates +/testing/simulating_authentication /testing/http_authentication +/validation/group_service_resolver /form/validation_group_service_resolver +/request/load_balancer_reverse_proxy /deployment/proxies +/quick_tour/the_controller /quick_tour/the_big_picture +/quick_tour/the_view /quick_tour/flex_recipes +/service_container/service_locators /service_container/service_subscribers_locators +/templating/overriding /bundles/override +/templating/twig_extension /templates#templates-twig-extension +/templating/hinclude /templates#templates-hinclude +/templating/PHP /templates +/security/custom_provider /security/user_provider +/security/multiple_user_providers /security/user_provider +/security/custom_password_authenticator /security/guard_authentication +/security/api_key_authentication /security/guard_authentication +/security/pre_authenticated /security/auth_providers +/security/host_restriction /security/firewall_restriction +/security/acl_advanced /security/acl +/security/password_encoding /security +/weblink /web_link +/components/weblink https://github.com/symfony/web-link +/frontend/encore/installation-no-flex /frontend/encore/installation +/http_cache/form_csrf_caching /security/csrf +/console/logging /console +/reference/forms/twig_reference /form/form_customization +/form/rendering /form/form_customization +/profiler/matchers /profiler +/profiler/profiling_data /profiler +/profiler/wdt_follow_ajax /profiler +/security/entity_provider /security/user_provider +/session/avoid_session_start /session +/session/sessions_directory /session +/session/configuring_ttl /session#session-configure-ttl +/frontend/encore/legacy-apps /frontend/encore/legacy-applications +/configuration/external_parameters /configuration/environment_variables +/contributing/code/patches /contributing/code/pull_requests +/workflow/state-machines /workflow/workflow-and-state-machine +/workflow/introduction /workflow/workflow-and-state-machine +/workflow/usage /workflow +/introduction/from_flat_php_to_symfony2 /introduction/from_flat_php_to_symfony +/configuration/environment_variables /configuration/env_var_processors +/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 +/contributing/code/core_team /contributing/core_team +/profiler/storage /profiler +/setup/composer /setup +/security/security_checker /setup +/setup/built_in_web_server /setup/symfony_server +/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#output-escaping +/templating/syntax /templates#linting-twig-templates +/templating/debug /templates#the-dump-twig-utilities +/templating/render_without_controller /templates#rendering-a-template-directly-from-a-route +/templating/app_variable /templates#the-app-global-variable +/templating/formats /templates +/templating/namespaced_paths /templates#template-namespaces +/templating/embedding_controllers /templates#embedding-controllers +/templating/inheritance /templates#template-inheritance-and-layouts +/testing/doctrine /testing/database +/translation/templates /translation#translation-in-templates +/translation/debug /translation#translation-debug +/translation/lint /translation#translation-lint +/translation/locale /translation#translation-locale +/doctrine/lifecycle_callbacks /doctrine/events +/doctrine/event_listeners_subscribers /doctrine/events +/doctrine/common_extensions /doctrine +/best_practices/index /best_practices +/best_practices/introduction /best_practices +/best_practices/creating-the-project /best_practices +/best_practices/configuration /best_practices +/best_practices/business-logic /best_practices +/best_practices/controllers /best_practices +/best_practices/templates /best_practices +/best_practices/forms /best_practices +/best_practices/i18n /best_practices +/best_practices/security /best_practices +/best_practices/web-assets /best_practices +/best_practices/tests /best_practices +/components/debug https://github.com/symfony/debug +/components/translation https://github.com/symfony/translation +/components/translation/usage /translation +/components/translation/custom_formats https://github.com/symfony/translation +/components/translation/custom_message_formatter https://github.com/symfony/translation +/components/notifier https://github.com/symfony/notifier +/components/routing https://github.com/symfony/routing +/session/database /session#session-database +/doctrine/pdo_session_storage /session#session-database-pdo +/doctrine/mongodb_session_storage /session#session-database-mongodb +/components/dotenv https://github.com/symfony/dotenv +/components/mercure /mercure +/components/polyfill_apcu https://github.com/symfony/polyfill-apcu +/components/polyfill_ctype https://github.com/symfony/polyfill-ctype +/components/polyfill_iconv https://github.com/symfony/polyfill-iconv +/components/polyfill_intl_grapheme https://github.com/symfony/polyfill_intl-grapheme +/components/polyfill_intl_icu https://github.com/symfony/polyfill_intl-icu +/components/polyfill_intl_idn https://github.com/symfony/polyfill_intl-idn +/components/polyfill_intl_normalizer https://github.com/symfony/polyfill_intl-normalizer +/components/polyfill_mbstring https://github.com/symfony/polyfill-mbstring +/components/polyfill_php54 https://github.com/symfony/polyfill-php54 +/components/polyfill_php55 https://github.com/symfony/polyfill-php55 +/components/polyfill_php56 https://github.com/symfony/polyfill-php56 +/components/polyfill_php70 https://github.com/symfony/polyfill-php70 +/components/polyfill_php71 https://github.com/symfony/polyfill-php71 +/components/polyfill_php72 https://github.com/symfony/polyfill-php72 +/components/polyfill_php73 https://github.com/symfony/polyfill-php73 +/components/polyfill_uuid https://github.com/symfony/polyfill-uuid +/components/web_link https://github.com/symfony/web-link +/components/templating https://github.com/symfony/templating +/components/error_handler https://github.com/symfony/error-handler +/components/class_loader https://github.com/symfony/class-loader +/frontend/encore/versus-assetic /frontend +/components/http_client /http_client +/components/mailer /mailer +/messenger/message-recorder /messenger/dispatch_after_current_bus +/components/stopwatch https://github.com/symfony/stopwatch +/service_container/3.3-di-changes https://symfony.com/doc/3.4/service_container/3.3-di-changes.html +/frontend/encore/shared-entry /frontend/encore/split-chunks +/frontend/encore/page-specific-assets /frontend/encore/simple-example#page-specific-javascript-or-css +/testing/functional_tests_assertions /testing#testing-application-assertions +/components https://symfony.com/components +/components/index https://symfony.com/components +/serializer/normalizers /serializer#serializer-built-in-normalizers +/logging/monolog_regex_based_excludes /logging/monolog_exclude_http_codes +/security/named_encoders /security/named_hashers +/components/inflector /string#inflector +/security/experimental_authenticators /security +/security/user_provider /security/user_providers +/security/reset_password /security/passwords#reset-password +/security/auth_providers /security#security-authenticators +/security/form_login /security#form-login +/security/form_login_setup /security#form-login +/security/json_login_setup /security#json-login +/security/named_hashers /security/passwords#named-password-hashers +/security/password_migration /security/passwords#security-password-migration +/security/acl https://github.com/symfony/acl-bundle/blob/main/src/Resources/doc/index.rst +/security/securing_services /security#securing-other-services +/security/authenticator_manager /security +/security/multiple_guard_authenticators /security/entry_point +/security/guard_authentication /security/custom_authenticator +/components/security/authentication /security#authenticating-users +/components/security/authorization /security#access-control-authorization +/components/security/firewall /security#the-firewall +/components/security/secure_tools /security/passwords +/components/security /security +/components/var_dumper/advanced /components/var_dumper#advanced-usage +/components/yaml/yaml_format /reference/formats/yaml +/components/expression_language/syntax /reference/formats/expression_language +/components/expression_language/ast /components/expression_language#expression-language-ast +/components/expression_language/caching /components/expression_language#expression-language-caching +/components/expression_language/extending /components/expression_language#expression-language-extending +/notifier/chatters /notifier#sending-chat-messages +/notifier/texters /notifier#sending-sms +/notifier/events /notifier#notifier-events +/email /mailer +/frontend/assetic /frontend +/frontend/assetic/index /frontend +/controller/argument_value_resolver /controller/value_resolver +/frontend/ux https://symfony.com/bundles/StimulusBundle/current/index.html +/messenger/handler_results /messenger#messenger-getting-handler-results +/messenger/dispatch_after_current_bus /messenger#messenger-transactional-messages +/messenger/multiple_buses /messenger#messenger-multiple-buses +/frontend/encore/server-data /frontend/server-data +/components/string /string +/testing/http_authentication /testing#testing_logging_in_users +/doctrine/registration_form /security#security-make-registration-form +/form/form_dependencies /form/create_custom_field_type +/doctrine/reverse_engineering /doctrine#doctrine-adding-mapping +/components/serializer /serializer +/serializer/custom_encoder /serializer/encoders#serializer-custom-encoder diff --git a/_images/components/console/completion.gif b/_images/components/console/completion.gif new file mode 100644 index 00000000000..18b3f5475c8 Binary files /dev/null and b/_images/components/console/completion.gif differ diff --git a/_images/components/console/cursor.gif b/_images/components/console/cursor.gif new file mode 100644 index 00000000000..71a74dd8637 Binary files /dev/null and b/_images/components/console/cursor.gif differ diff --git a/_images/components/console/debug_formatter.png b/_images/components/console/debug_formatter.png new file mode 100644 index 00000000000..4ba2c0c2b57 Binary files /dev/null and b/_images/components/console/debug_formatter.png differ diff --git a/_images/components/console/process-helper-debug.png b/_images/components/console/process-helper-debug.png new file mode 100644 index 00000000000..96c5c316739 Binary files /dev/null and b/_images/components/console/process-helper-debug.png differ diff --git a/_images/components/console/process-helper-error-debug.png b/_images/components/console/process-helper-error-debug.png new file mode 100644 index 00000000000..48f6c7258d4 Binary files /dev/null and b/_images/components/console/process-helper-error-debug.png differ diff --git a/_images/components/console/process-helper-verbose.png b/_images/components/console/process-helper-verbose.png new file mode 100644 index 00000000000..abdff9812b0 Binary files /dev/null and b/_images/components/console/process-helper-verbose.png differ diff --git a/_images/components/console/progressbar.gif b/_images/components/console/progressbar.gif new file mode 100644 index 00000000000..0746e399354 Binary files /dev/null and b/_images/components/console/progressbar.gif differ diff --git a/_images/components/http_kernel/http-workflow-exception.svg b/_images/components/http_kernel/http-workflow-exception.svg new file mode 100644 index 00000000000..3330010367a --- /dev/null +++ b/_images/components/http_kernel/http-workflow-exception.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/_images/components/http_kernel/http-workflow-subrequest.svg b/_images/components/http_kernel/http-workflow-subrequest.svg new file mode 100644 index 00000000000..4f4912dc5a1 --- /dev/null +++ b/_images/components/http_kernel/http-workflow-subrequest.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/_images/components/http_kernel/http-workflow.svg b/_images/components/http_kernel/http-workflow.svg new file mode 100644 index 00000000000..f3bc7a9ee8b --- /dev/null +++ b/_images/components/http_kernel/http-workflow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/_images/components/messenger/basic_cycle.png b/_images/components/messenger/basic_cycle.png new file mode 100644 index 00000000000..a0558968cbb Binary files /dev/null and b/_images/components/messenger/basic_cycle.png differ diff --git a/_images/components/messenger/overview.svg b/_images/components/messenger/overview.svg new file mode 100644 index 00000000000..4b82c203756 --- /dev/null +++ b/_images/components/messenger/overview.svg @@ -0,0 +1 @@ + diff --git a/_images/components/scheduler/generate_consume.png b/_images/components/scheduler/generate_consume.png new file mode 100644 index 00000000000..269281266a5 Binary files /dev/null and b/_images/components/scheduler/generate_consume.png differ diff --git a/_images/components/scheduler/scheduler_cycle.png b/_images/components/scheduler/scheduler_cycle.png new file mode 100644 index 00000000000..18addb37d91 Binary files /dev/null and b/_images/components/scheduler/scheduler_cycle.png differ diff --git a/_images/components/string/bytes-points-graphemes.png b/_images/components/string/bytes-points-graphemes.png new file mode 100644 index 00000000000..18d971cecf7 Binary files /dev/null and b/_images/components/string/bytes-points-graphemes.png differ diff --git a/_images/components/var_dumper/01-simple.png b/_images/components/var_dumper/01-simple.png new file mode 100644 index 00000000000..a4d03147667 Binary files /dev/null and b/_images/components/var_dumper/01-simple.png differ diff --git a/_images/components/var_dumper/02-multi-line-str.png b/_images/components/var_dumper/02-multi-line-str.png new file mode 100644 index 00000000000..b40949bd981 Binary files /dev/null and b/_images/components/var_dumper/02-multi-line-str.png differ diff --git a/_images/components/var_dumper/03-object.png b/_images/components/var_dumper/03-object.png new file mode 100644 index 00000000000..47fc5e5e245 Binary files /dev/null and b/_images/components/var_dumper/03-object.png differ diff --git a/_images/components/var_dumper/04-dynamic-property.png b/_images/components/var_dumper/04-dynamic-property.png new file mode 100644 index 00000000000..de7938c20cf Binary files /dev/null and b/_images/components/var_dumper/04-dynamic-property.png differ diff --git a/_images/components/var_dumper/05-soft-ref.png b/_images/components/var_dumper/05-soft-ref.png new file mode 100644 index 00000000000..964af97ffd3 Binary files /dev/null and b/_images/components/var_dumper/05-soft-ref.png differ diff --git a/_images/components/var_dumper/06-constants.png b/_images/components/var_dumper/06-constants.png new file mode 100644 index 00000000000..26c735bd613 Binary files /dev/null and b/_images/components/var_dumper/06-constants.png differ diff --git a/_images/components/var_dumper/07-hard-ref.png b/_images/components/var_dumper/07-hard-ref.png new file mode 100644 index 00000000000..02dc17c9c40 Binary files /dev/null and b/_images/components/var_dumper/07-hard-ref.png differ diff --git a/_images/components/var_dumper/08-virtual-property.png b/_images/components/var_dumper/08-virtual-property.png new file mode 100644 index 00000000000..564a2731ec1 Binary files /dev/null and b/_images/components/var_dumper/08-virtual-property.png differ diff --git a/_images/components/var_dumper/09-cut.png b/_images/components/var_dumper/09-cut.png new file mode 100644 index 00000000000..5229f48820c Binary files /dev/null and b/_images/components/var_dumper/09-cut.png differ diff --git a/_images/components/var_dumper/10-uninitialized.png b/_images/components/var_dumper/10-uninitialized.png new file mode 100644 index 00000000000..735731b83b5 Binary files /dev/null and b/_images/components/var_dumper/10-uninitialized.png differ diff --git a/_images/components/workflow/blogpost.png b/_images/components/workflow/blogpost.png new file mode 100644 index 00000000000..b7f51eabb43 Binary files /dev/null and b/_images/components/workflow/blogpost.png differ diff --git a/_images/components/workflow/blogpost_mermaid.png b/_images/components/workflow/blogpost_mermaid.png new file mode 100644 index 00000000000..7a4d3a57cfe Binary files /dev/null and b/_images/components/workflow/blogpost_mermaid.png differ diff --git a/_images/components/workflow/blogpost_metadata.png b/_images/components/workflow/blogpost_metadata.png new file mode 100644 index 00000000000..783f51c6ccf Binary files /dev/null and b/_images/components/workflow/blogpost_metadata.png differ diff --git a/_images/components/workflow/blogpost_puml.png b/_images/components/workflow/blogpost_puml.png new file mode 100644 index 00000000000..efe543a6f8e Binary files /dev/null and b/_images/components/workflow/blogpost_puml.png differ diff --git a/_images/components/workflow/job_application.png b/_images/components/workflow/job_application.png new file mode 100644 index 00000000000..9c5e6792ae9 Binary files /dev/null and b/_images/components/workflow/job_application.png differ diff --git a/_images/components/workflow/pull_request.png b/_images/components/workflow/pull_request.png new file mode 100644 index 00000000000..692a95345ae Binary files /dev/null and b/_images/components/workflow/pull_request.png differ 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/_images/components/workflow/simple.png b/_images/components/workflow/simple.png new file mode 100644 index 00000000000..ed158d5cc7a Binary files /dev/null and b/_images/components/workflow/simple.png differ diff --git a/_images/components/workflow/states_transitions.png b/_images/components/workflow/states_transitions.png new file mode 100644 index 00000000000..d1f54391afd Binary files /dev/null and b/_images/components/workflow/states_transitions.png differ diff --git a/_images/contributing/code/stack-trace.gif b/_images/contributing/code/stack-trace.gif new file mode 100644 index 00000000000..97a2043448d Binary files /dev/null and b/_images/contributing/code/stack-trace.gif differ diff --git a/_images/contributing/docs-github-create-pr.png b/_images/contributing/docs-github-create-pr.png new file mode 100644 index 00000000000..43b6842ffc2 Binary files /dev/null and b/_images/contributing/docs-github-create-pr.png differ diff --git a/_images/contributing/docs-github-edit-page.png b/_images/contributing/docs-github-edit-page.png new file mode 100644 index 00000000000..b739497f70f Binary files /dev/null and b/_images/contributing/docs-github-edit-page.png differ diff --git a/_images/contributing/docs-pull-request-change-base.png b/_images/contributing/docs-pull-request-change-base.png new file mode 100644 index 00000000000..791901b8ec6 Binary files /dev/null and b/_images/contributing/docs-pull-request-change-base.png differ diff --git a/_images/controller/error_pages/errors-in-prod-environment.png b/_images/controller/error_pages/errors-in-prod-environment.png new file mode 100644 index 00000000000..808d0d70028 Binary files /dev/null and b/_images/controller/error_pages/errors-in-prod-environment.png differ diff --git a/_images/controller/error_pages/exceptions-in-dev-environment.png b/_images/controller/error_pages/exceptions-in-dev-environment.png new file mode 100644 index 00000000000..e1fba2bebf9 Binary files /dev/null and b/_images/controller/error_pages/exceptions-in-dev-environment.png differ diff --git a/_images/deployment/azure-website/step-01.png b/_images/deployment/azure-website/step-01.png new file mode 100644 index 00000000000..ef60db66ab2 Binary files /dev/null and b/_images/deployment/azure-website/step-01.png differ diff --git a/_images/deployment/azure-website/step-02.png b/_images/deployment/azure-website/step-02.png new file mode 100644 index 00000000000..fe38cf45be3 Binary files /dev/null and b/_images/deployment/azure-website/step-02.png differ diff --git a/_images/deployment/azure-website/step-03.png b/_images/deployment/azure-website/step-03.png new file mode 100644 index 00000000000..6fc0789cac9 Binary files /dev/null and b/_images/deployment/azure-website/step-03.png differ diff --git a/_images/deployment/azure-website/step-04.png b/_images/deployment/azure-website/step-04.png new file mode 100644 index 00000000000..a16d8f07a86 Binary files /dev/null and b/_images/deployment/azure-website/step-04.png differ diff --git a/_images/deployment/azure-website/step-05.png b/_images/deployment/azure-website/step-05.png new file mode 100644 index 00000000000..8da32f7ab67 Binary files /dev/null and b/_images/deployment/azure-website/step-05.png differ diff --git a/_images/deployment/azure-website/step-06.png b/_images/deployment/azure-website/step-06.png new file mode 100644 index 00000000000..067ff4e767a Binary files /dev/null and b/_images/deployment/azure-website/step-06.png differ diff --git a/_images/deployment/azure-website/step-07.png b/_images/deployment/azure-website/step-07.png new file mode 100644 index 00000000000..7acffd2c782 Binary files /dev/null and b/_images/deployment/azure-website/step-07.png differ diff --git a/_images/deployment/azure-website/step-08.png b/_images/deployment/azure-website/step-08.png new file mode 100644 index 00000000000..cb106db5c02 Binary files /dev/null and b/_images/deployment/azure-website/step-08.png differ diff --git a/_images/deployment/azure-website/step-09.png b/_images/deployment/azure-website/step-09.png new file mode 100644 index 00000000000..5005531fb09 Binary files /dev/null and b/_images/deployment/azure-website/step-09.png differ diff --git a/_images/deployment/azure-website/step-10.png b/_images/deployment/azure-website/step-10.png new file mode 100644 index 00000000000..e9a7d8fdff8 Binary files /dev/null and b/_images/deployment/azure-website/step-10.png differ diff --git a/_images/deployment/azure-website/step-11.png b/_images/deployment/azure-website/step-11.png new file mode 100644 index 00000000000..48b1c2992e1 Binary files /dev/null and b/_images/deployment/azure-website/step-11.png differ diff --git a/_images/deployment/azure-website/step-12.png b/_images/deployment/azure-website/step-12.png new file mode 100644 index 00000000000..85f8f54d142 Binary files /dev/null and b/_images/deployment/azure-website/step-12.png differ diff --git a/_images/deployment/azure-website/step-13.png b/_images/deployment/azure-website/step-13.png new file mode 100644 index 00000000000..49aac465fd7 Binary files /dev/null and b/_images/deployment/azure-website/step-13.png differ diff --git a/_images/deployment/azure-website/step-14.png b/_images/deployment/azure-website/step-14.png new file mode 100644 index 00000000000..8e6c3ed3a5e Binary files /dev/null and b/_images/deployment/azure-website/step-14.png differ diff --git a/_images/deployment/azure-website/step-15.png b/_images/deployment/azure-website/step-15.png new file mode 100644 index 00000000000..c8d5bce96d3 Binary files /dev/null and b/_images/deployment/azure-website/step-15.png differ diff --git a/_images/deployment/azure-website/step-16.png b/_images/deployment/azure-website/step-16.png new file mode 100644 index 00000000000..da7d4bebde7 Binary files /dev/null and b/_images/deployment/azure-website/step-16.png differ diff --git a/_images/doctrine/doctrine_web_debug_toolbar.png b/_images/doctrine/doctrine_web_debug_toolbar.png new file mode 100644 index 00000000000..8103162e591 Binary files /dev/null and b/_images/doctrine/doctrine_web_debug_toolbar.png differ diff --git a/_images/doctrine/mapping_relations.svg b/_images/doctrine/mapping_relations.svg new file mode 100644 index 00000000000..7dc8979cb1a --- /dev/null +++ b/_images/doctrine/mapping_relations.svg @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/doctrine/mapping_relations_proxy.svg b/_images/doctrine/mapping_relations_proxy.svg new file mode 100644 index 00000000000..634d1b0add2 --- /dev/null +++ b/_images/doctrine/mapping_relations_proxy.svg @@ -0,0 +1,926 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/doctrine/mapping_single_entity.svg b/_images/doctrine/mapping_single_entity.svg new file mode 100644 index 00000000000..5d517c85fb1 --- /dev/null +++ b/_images/doctrine/mapping_single_entity.svg @@ -0,0 +1,469 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/form/data-transformer-types.svg b/_images/form/data-transformer-types.svg new file mode 100644 index 00000000000..9393b224f89 --- /dev/null +++ b/_images/form/data-transformer-types.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/form/form-custom-type-postal-address-fragment-names.svg b/_images/form/form-custom-type-postal-address-fragment-names.svg new file mode 100644 index 00000000000..db9463b8327 --- /dev/null +++ b/_images/form/form-custom-type-postal-address-fragment-names.svg @@ -0,0 +1 @@ + diff --git a/_images/form/form-custom-type-postal-address.svg b/_images/form/form-custom-type-postal-address.svg new file mode 100644 index 00000000000..42ffce4067f --- /dev/null +++ b/_images/form/form-custom-type-postal-address.svg @@ -0,0 +1 @@ + diff --git a/_images/form/form-field-parts.svg b/_images/form/form-field-parts.svg new file mode 100644 index 00000000000..c9856c89a99 --- /dev/null +++ b/_images/form/form-field-parts.svg @@ -0,0 +1 @@ + diff --git a/_images/form/form_prepopulation_workflow.svg b/_images/form/form_prepopulation_workflow.svg new file mode 100644 index 00000000000..c908f5c5a76 --- /dev/null +++ b/_images/form/form_prepopulation_workflow.svg @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/form/form_submission_workflow.svg b/_images/form/form_submission_workflow.svg new file mode 100644 index 00000000000..d6d138ee61a --- /dev/null +++ b/_images/form/form_submission_workflow.svg @@ -0,0 +1,334 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/form/form_workflow.svg b/_images/form/form_workflow.svg new file mode 100644 index 00000000000..2dbacbbf096 --- /dev/null +++ b/_images/form/form_workflow.svg @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/book/form-simple2.png b/_images/form/simple-form-2.png similarity index 100% rename from images/book/form-simple2.png rename to _images/form/simple-form-2.png diff --git a/_images/form/simple-form.png b/_images/form/simple-form.png new file mode 100644 index 00000000000..1dced444561 Binary files /dev/null and b/_images/form/simple-form.png differ diff --git a/_images/form/tailwindcss-form.png b/_images/form/tailwindcss-form.png new file mode 100644 index 00000000000..8a290749149 Binary files /dev/null and b/_images/form/tailwindcss-form.png differ diff --git a/_images/http/request-flow.svg b/_images/http/request-flow.svg new file mode 100644 index 00000000000..97061ada0d5 --- /dev/null +++ b/_images/http/request-flow.svg @@ -0,0 +1 @@ + diff --git a/_images/http/xkcd-full.svg b/_images/http/xkcd-full.svg new file mode 100644 index 00000000000..da590c2b97e --- /dev/null +++ b/_images/http/xkcd-full.svg @@ -0,0 +1,324 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/http/xkcd-request.svg b/_images/http/xkcd-request.svg new file mode 100644 index 00000000000..6a21280ca34 --- /dev/null +++ b/_images/http/xkcd-request.svg @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/install/deprecations-in-profiler.png b/_images/install/deprecations-in-profiler.png new file mode 100644 index 00000000000..3d3f9a98a4a Binary files /dev/null and b/_images/install/deprecations-in-profiler.png differ diff --git a/_images/mercure/chrome.png b/_images/mercure/chrome.png new file mode 100644 index 00000000000..8ccc55a0a88 Binary files /dev/null and b/_images/mercure/chrome.png differ diff --git a/_images/mercure/discovery.svg b/_images/mercure/discovery.svg new file mode 100644 index 00000000000..ed18381068a --- /dev/null +++ b/_images/mercure/discovery.svg @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/mercure/hub.svg b/_images/mercure/hub.svg new file mode 100644 index 00000000000..6b5e496e3c6 --- /dev/null +++ b/_images/mercure/hub.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/mercure/panel.png b/_images/mercure/panel.png new file mode 100644 index 00000000000..22b214f5ff2 Binary files /dev/null and b/_images/mercure/panel.png differ diff --git a/_images/notifier/microsoft_teams/message-card.png b/_images/notifier/microsoft_teams/message-card.png new file mode 100644 index 00000000000..05f505fb3e0 Binary files /dev/null and b/_images/notifier/microsoft_teams/message-card.png differ diff --git a/_images/notifier/microsoft_teams/message.png b/_images/notifier/microsoft_teams/message.png new file mode 100644 index 00000000000..5c4c7f11ed1 Binary files /dev/null and b/_images/notifier/microsoft_teams/message.png differ diff --git a/_images/notifier/slack/field-method.png b/_images/notifier/slack/field-method.png new file mode 100644 index 00000000000..d77a60e6a2e Binary files /dev/null and b/_images/notifier/slack/field-method.png differ diff --git a/_images/notifier/slack/message-reply.png b/_images/notifier/slack/message-reply.png new file mode 100644 index 00000000000..9a60e4573ab Binary files /dev/null and b/_images/notifier/slack/message-reply.png differ diff --git a/_images/notifier/slack/slack-footer.png b/_images/notifier/slack/slack-footer.png new file mode 100644 index 00000000000..a53952c78f6 Binary files /dev/null and b/_images/notifier/slack/slack-footer.png differ diff --git a/_images/notifier/slack/slack-header.png b/_images/notifier/slack/slack-header.png new file mode 100644 index 00000000000..a7caf915d8f Binary files /dev/null and b/_images/notifier/slack/slack-header.png differ diff --git a/_images/profiler/web-interface.png b/_images/profiler/web-interface.png new file mode 100644 index 00000000000..b107f6427d7 Binary files /dev/null and b/_images/profiler/web-interface.png differ diff --git a/_images/quick_tour/no_routes_page.png b/_images/quick_tour/no_routes_page.png new file mode 100644 index 00000000000..030953a17b1 Binary files /dev/null and b/_images/quick_tour/no_routes_page.png differ diff --git a/_images/rate_limiter/fixed_window.svg b/_images/rate_limiter/fixed_window.svg new file mode 100644 index 00000000000..83d5f6e79ac --- /dev/null +++ b/_images/rate_limiter/fixed_window.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + 10:00 + + + 10:30 + + + 11:00 + + + 11:30 + + + 12:00 + + + + + + + + 12:30 + + + 13:00 + + + + + + + + + + + + + + + + + + + + + + 1 hour window + + + 1 hour window + + + + + + 1 hour window + + + + + 13:15 + + + diff --git a/_images/rate_limiter/sliding_window.svg b/_images/rate_limiter/sliding_window.svg new file mode 100644 index 00000000000..2c565615441 --- /dev/null +++ b/_images/rate_limiter/sliding_window.svg @@ -0,0 +1,65 @@ + + + + + + + + + + 10:00 + + + 10:30 + + + 11:00 + + + 11:30 + + + 12:00 + + + + + + 12:30 + + + 13:00 + + + + + + + + + + + + + + + + + + + + + + 1 hour window + + + + + + 13:15 + + + + + + diff --git a/_images/rate_limiter/token_bucket.svg b/_images/rate_limiter/token_bucket.svg new file mode 100644 index 00000000000..29d6fc8f103 --- /dev/null +++ b/_images/rate_limiter/token_bucket.svg @@ -0,0 +1,83 @@ + + + + 10:00 + + + 10:30 + + + 11:00 + + + 11:30 + + + 12:00 + + + + + + + + 12:30 + + + 13:00 + + + + + + + + + + + + + + + + + + + + + + + + + + + 13:15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/reference/form/choice-example1.png b/_images/reference/form/choice-example1.png new file mode 100644 index 00000000000..00e47d0bb27 Binary files /dev/null and b/_images/reference/form/choice-example1.png differ diff --git a/_images/reference/form/choice-example2.png b/_images/reference/form/choice-example2.png new file mode 100644 index 00000000000..147d82bcfca Binary files /dev/null and b/_images/reference/form/choice-example2.png differ diff --git a/_images/reference/form/choice-example3.png b/_images/reference/form/choice-example3.png new file mode 100644 index 00000000000..232f8519fee Binary files /dev/null and b/_images/reference/form/choice-example3.png differ diff --git a/_images/reference/form/choice-example4.png b/_images/reference/form/choice-example4.png new file mode 100644 index 00000000000..7f6071d3532 Binary files /dev/null and b/_images/reference/form/choice-example4.png differ diff --git a/_images/reference/form/choice-example5.png b/_images/reference/form/choice-example5.png new file mode 100644 index 00000000000..188eeeec234 Binary files /dev/null and b/_images/reference/form/choice-example5.png differ diff --git a/_images/security/anonymous_wdt.png b/_images/security/anonymous_wdt.png new file mode 100644 index 00000000000..80736afce39 Binary files /dev/null and b/_images/security/anonymous_wdt.png differ diff --git a/_images/security/authentication-guard-methods.svg b/_images/security/authentication-guard-methods.svg new file mode 100644 index 00000000000..cc042656212 --- /dev/null +++ b/_images/security/authentication-guard-methods.svg @@ -0,0 +1 @@ + diff --git a/_images/security/login_link_email.png b/_images/security/login_link_email.png new file mode 100644 index 00000000000..8331b878f68 Binary files /dev/null and b/_images/security/login_link_email.png differ diff --git a/_images/security/profiler-badges.png b/_images/security/profiler-badges.png new file mode 100644 index 00000000000..a19f8539581 Binary files /dev/null and b/_images/security/profiler-badges.png differ diff --git a/_images/security/security_events.svg b/_images/security/security_events.svg new file mode 100644 index 00000000000..f1b93923da6 --- /dev/null +++ b/_images/security/security_events.svg @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/security/symfony_loggedin_wdt.png b/_images/security/symfony_loggedin_wdt.png new file mode 100644 index 00000000000..b51e1cafba1 Binary files /dev/null and b/_images/security/symfony_loggedin_wdt.png differ diff --git a/_images/serializer/serializer_workflow.svg b/_images/serializer/serializer_workflow.svg new file mode 100644 index 00000000000..b6e9c254778 --- /dev/null +++ b/_images/serializer/serializer_workflow.svg @@ -0,0 +1,283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/sources/README.md b/_images/sources/README.md new file mode 100644 index 00000000000..84810a9783d --- /dev/null +++ b/_images/sources/README.md @@ -0,0 +1,102 @@ +How to Create Symfony Images +============================ + +Creating Diagrams +----------------- + +* Use [Dia][1] as the diagramming application; +* Use [PT Sans Narrow][2] as the only font in all diagrams (if possible, use + only the "normal" weight for all contents); +* Use 36pt as the base font size; +* Use 0.10 cm width for lines and shape borders; +* Use the following color palette: + * Text, lines and shape borders: black (#000000) + * Shape backgrounds: + * Grays: dark (#4d4d4d), medium (#b3b3b3), light (#f2f2f2) + * Blue: #b2d4eb + * Red: #ecbec0 + * Green: #b2dec7 + * Orange: #fddfbb + +In case of doubt, check the existing diagrams or ask to the +[Symfony Documentation Team][3]. + +### Saving and Exporting the Diagram + +* Save the original diagram in `*.dia` format in `_images/sources/`; +* Export the diagram to SVG format and save it in `_images/`. + +Important: choose "Cairo Scalable Vector Graphics (.svg)" format instead of +plain " Scalable Vector Graphics (.svg)" because the former is the only format +that transforms text into vector shapes (resulting file is larger in size, but +it's truly portable because text is displayed the same even if you don't have +some fonts installed). + +### Including the Diagram in the Symfony Docs + +Use the following snippet to embed the diagram in the docs: + +``` +.. raw:: html + + +``` + +### Reasoning + +* Dia was chosen because it's one of the few applications which are free, open + source and compatible with Linux, macOS and Windows. +* Font, colors and line widths were chosen to be similar to the diagrams used + in the best tech books. + +### Troubleshooting + +* On some macOS systems, Dia cannot be executed as a regular application and + you must run the following console command instead: + `export DISPLAY=:0 && /Applications/Dia.app/Contents/Resources/bin/dia` + +Creating Console Screenshots +---------------------------- + +* Use [Asciinema][4] to record the console session locally: + + ``` + $ asciinema rec -c bash recording.cast + ``` +* Use `$ ` as the prompt in recordings. E.g. if you're using Bash, add the + following lines to your ``.bashrc``: + + ``` + if [ "$ASCIINEMA_REC" = "1" ]; then + PS1="\e[37m$ \e[0m" + fi + ``` +* Save the generated asciicast in `_images/sources/`. + +### Rendering the Recording + +Rendering the recording can be a difficult task. The [documentation team][3] +is always ready to help you with this task (e.g. you can open a PR with +only the asciicast file). + +* Use [agg][5] to generated a GIF file from the recording; +* Install the [JetBrains Mono][6] font; +* Use the ``_images/sources/ascii-render.sh`` file to call agg: + + ``` + AGG_PATH=/path/to/agg ./_images/sources/ascii-render.sh recording.cast --cols 45 --rows 20 + ``` + + This utility configures a predefined theme; +* Always configure `--cols`` (width) and ``--rows`` (height), try to use as + low as possible numbers. Do not exceed 70 columns; +* Save the generated GIF file in `_images/`. + +[1]: http://dia-installer.de/ +[2]: https://fonts.google.com/specimen/PT+Sans+Narrow +[3]: https://symfony.com/doc/current/contributing/core_team.html +[4]: https://github.com/asciinema/asciinema +[5]: https://github.com/asciinema/agg +[6]: https://www.jetbrains.com/lp/mono/ diff --git a/_images/sources/ascii-render.sh b/_images/sources/ascii-render.sh new file mode 100755 index 00000000000..e72be572390 --- /dev/null +++ b/_images/sources/ascii-render.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env sh +case "$1" in + ''|help|-h) + echo "ansi-render.sh RECORDING [options]" + echo "" + echo " RECORDING: path to the .cast file generated by asciinema" + echo " [options]: optional options to be passed to agg" + ;; + *) + recording=$1 + extra_options= + if [ $# -gt 1 ]; then + shift + extra_options=$@ + fi + + # optionally, use this green color: 1f4631 + ${AGG_PATH:-agg} \ + --theme 18202a,f9fafb,f9fafb,ff7b72,7ee787,ffa657,79c0ff,d2a8ff,a5d6ff,f9fafb,8b949e,ff7b72,00c300,ffa657,79c0ff,d2a8ff,a5d6ff,f9fafb --line-height 1.6 \ + --font-family 'JetBrains Mono' \ + $extra_options \ + $recording $(echo $recording | sed "s/cast/gif/") + ;; +esac diff --git a/_images/sources/components/console/completion.cast b/_images/sources/components/console/completion.cast new file mode 100644 index 00000000000..c268863e9b0 --- /dev/null +++ b/_images/sources/components/console/completion.cast @@ -0,0 +1,37 @@ +{"version": 2, "width": 76, "height": 30, "timestamp": 1663253713, "env": {"SHELL": "/usr/bin/fish", "TERM": "st-256color"}} +[0.00798, "o", "\u001b[?2004h\u001b[90m$ \u001b[0m"] +[0.614685, "o", "b"] +[0.776549, "o", "i"] +[0.86682, "o", "n"] +[1.092426, "o", "/"] +[1.332671, "o", "c"] +[1.55068, "o", "o"] +[1.630651, "o", "n"] +[1.784584, "o", "s"] +[1.873108, "o", "o"] +[2.074652, "o", "l"] +[2.180433, "o", "e"] +[2.260475, "o", " "] +[2.696628, "o", "\u0007"] +[2.947263, "o", "\r\nabout debug:event-dispatcher\r\nassets:install debug:router\r\ncache:clear help\r\ncache:pool:clear lint:container\r\ncache:pool:delete lint:yaml\r\ncache:pool:list list\r\ncache:pool:prune router:match\r\ncache:warmup secrets:decrypt-to-local\r\ncompletion secrets:encrypt-from-local\r\nconfig:dump-reference secrets:generate-keys\r\ndebug:autowiring secrets:list\r\ndebug:config secrets:remove\r\ndebug:container secrets:set\r\ndebug:dotenv \r\n\u001b[37m$ \u001b[0mbin/console "] +[3.614479, "o", "s"] +[3.802449, "o", "e"] +[4.205631, "o", "\u0007crets:"] +[4.520435, "o", "r"] +[4.598031, "o", "e"] +[5.026287, "o", "move "] +[5.47041, "o", "\u0007SOME_"] +[5.673941, "o", "\u0007"] +[6.024086, "o", "\r\nSOME_OTHER_SECRET SOME_SECRET \r\n\u001b[37m$ \u001b[0mbin/console secrets:remove SOME_"] +[6.770627, "o", "O"] +[7.14335, "o", "THER_SECRET "] +[7.724482, "o", "\r\n\u001b[?2004l\r"] +[7.776657, "o", "\r\n"] +[7.779108, "o", "\u001b[30;42m \u001b[39;49m\r\n\u001b[30;42m [OK] Secret \"SOME_OTHER_SECRET\" removed from \"config/secrets/dev/\". \u001b[39;49m\r\n\u001b[30;42m \u001b[39;49m\r\n\r\n"] +[7.782993, "o", "\u001b[?2004h\u001b[37m$ \u001b[0m"] +[9.214537, "o", "e"] +[9.522429, "o", "x"] +[9.690371, "o", "i"] +[9.85446, "o", "t"] +[10.292412, "o", "\r\n\u001b[?2004l\r"] +[10.292526, "o", "exit\r\n"] diff --git a/_images/sources/components/console/cursor.cast b/_images/sources/components/console/cursor.cast new file mode 100644 index 00000000000..be2f2f6c351 --- /dev/null +++ b/_images/sources/components/console/cursor.cast @@ -0,0 +1,49 @@ +{"version": 2, "width": 191, "height": 30, "timestamp": 1663251833, "env": {"SHELL": "/usr/bin/fish", "TERM": "st-256color"}} +[0.007941, "o", "\u001b[?2004h\u001b[90m$ \u001b[0m"] +[0.566363, "o", "c"] +[0.643353, "o", "l"] +[0.762325, "o", "e"] +[0.952363, "o", "a"] +[0.995878, "o", "r"] +[1.107784, "o", "\r\n\u001b[?2004l\r"] +[1.109766, "o", "\u001b[H\u001b[2J"] +[1.109946, "o", "\u001b[?2004h\u001b[30m$ \u001b[0m"] +[1.653461, "o", "p"] +[1.772323, "o", "h"] +[1.856444, "o", "p"] +[1.980339, "o", " "] +[2.15827, "o", "c"] +[2.273242, "o", "u"] +[2.402231, "o", "r"] +[2.563066, "o", "s"] +[2.760266, "o", "o"] +[2.900252, "o", "r"] +[3.020537, "o", "."] +[3.316404, "o", "p"] +[3.403213, "o", "h"] +[3.483391, "o", "p"] +[3.820273, "o", "\r\n\u001b[?2004l\r"] +[3.845697, "o", "\u001b[6;9H#"] +[4.045942, "o", "\u001b[8;9H#"] +[4.246327, "o", "\u001b[8;2H#####"] +[4.446737, "o", "\u001b[2;9H#######"] +[4.647128, "o", "\u001b[7;7H#"] +[4.84749, "o", "\u001b[3;9H#"] +[5.047857, "o", "\u001b[7;9H#"] +[5.248246, "o", "\u001b[4;9H#"] +[5.448622, "o", "\u001b[2;2H#####"] +[5.648999, "o", "\u001b[3;7H#"] +[5.849378, "o", "\u001b[5;9H#####"] +[6.049711, "o", "\u001b[3;1H#"] +[6.250118, "o", "\u001b[7;1H#"] +[6.45056, "o", "\u001b[5;2H#####"] +[6.650897, "o", "\u001b[4;1H#"] +[6.851281, "o", "\u001b[6;7H#"] +[7.051644, "o", "\u001b[9;1H"] +[7.058802, "o", "\u001b[?2004h\u001b[30m$ \u001b[0m"] +[7.657612, "o", "e"] +[7.846956, "o", "x"] +[7.949451, "o", "i"] +[8.0893, "o", "t"] +[8.201144, "o", "\r\n\u001b[?2004l\r"] +[8.201227, "o", "exit\r\n"] diff --git a/_images/sources/components/console/progress.cast b/_images/sources/components/console/progress.cast new file mode 100644 index 00000000000..9c5244b37e2 --- /dev/null +++ b/_images/sources/components/console/progress.cast @@ -0,0 +1,57 @@ +{"version": 2, "width": 191, "height": 17, "timestamp": 1663423221, "env": {"SHELL": "/usr/bin/fish", "TERM": "st-256color"}} +[0.008171, "o", "\u001b[?2004h\u001b[90m$ \u001b[0m"] +[0.385858, "o", "p"] +[0.577979, "o", "h"] +[0.768282, "o", "p"] +[0.96433, "o", " "] +[1.133645, "o", "p"] +[1.262693, "o", "r"] +[1.385832, "o", "o"] +[1.476876, "o", "g"] +[1.652322, "o", "r"] +[1.722357, "o", "e"] +[1.935395, "o", "s"] +[2.083915, "o", "s"] +[2.200109, "o", "."] +[2.403686, "o", "p"] +[2.510201, "o", "h"] +[2.602756, "o", "p"] +[2.909974, "o", "\r\n\u001b[?2004l\r"] +[2.935647, "o", "\u001b[34m Starting the demo... fingers crossed \u001b[39m\r\n 0/15 \u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 0%\r\n  < 1 sec 4.0 MiB"] +[3.418022, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K"] +[3.419196, "o", "\u001b[34m Starting the demo... fingers crossed \u001b[39m\r\n 2/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 13%\r\n  < 1 sec 6.0 MiB"] +[3.66102, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G"] +[3.661071, "o", "\u001b[2K"] +[3.661731, "o", "\u001b[34m Starting the demo... fingers crossed \u001b[39m\r\n 3/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 20%\r\n  5 secs 6.0 MiB"] +[4.143554, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K"] +[4.14385, "o", "\u001b[34m Starting the demo... fingers crossed \u001b[39m\r\n 5/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 33%\r\n  3 secs 6.5 MiB"] +[4.385367, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K"] +[4.38612, "o", "\u001b[34m Looks good to me... \u001b[39m\r\n 6/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 40%\r\n  3 secs 7.1 MiB"] +[4.868053, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K"] +[4.86852, "o", "\u001b[34m Looks good to me... \u001b[39m\r\n 8/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 53%\r\n  4 secs 8.1 MiB"] +[5.110341, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K"] +[5.11133, "o", "\u001b[34m Looks good to me... \u001b[39m\r\n 9/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 60%\r\n  3 secs 8.6 MiB"] +[5.593851, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G"] +[5.593924, "o", "\u001b[2K"] +[5.594818, "o", "\u001b[34m Looks good to me... \u001b[39m\r\n11/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 73%\r\n  4 secs 9.6 MiB"] +[5.836301, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K"] +[5.836831, "o", "\u001b[34m Looks good to me... \u001b[39m\r\n12/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 80%\r\n  4 secs 10.1 MiB"] +[6.31877, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A"] +[6.318814, "o", "\u001b[1G\u001b[2K"] +[6.319403, "o", "\u001b[34m Looks good to me... \u001b[39m\r\n14/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m 93%\r\n  3 secs 11.1 MiB"] +[6.561359, "o", "\u001b[1G\u001b[2K\u001b[1A"] +[6.561561, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K"] +[6.562504, "o", "\u001b[34m Looks good to me... \u001b[39m\r\n15/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m 100%\r\n  4 secs 11.6 MiB"] +[6.563772, "o", "\u001b[1G"] +[6.563824, "o", "\u001b[2K\u001b[1A"] +[6.563875, "o", "\u001b[1G\u001b[2K"] +[6.563926, "o", "\u001b[1A\u001b[1G\u001b[2K"] +[6.564766, "o", "\u001b[34m Thanks bye! \u001b[39m\r\n15/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m 100%\r\n  4 secs 11.6 MiB"] +[6.564805, "o", "\r\n\r\n"] +[6.570516, "o", "\u001b[?2004h"] +[6.570537, "o", "\u001b[90m$ \u001b[0m"] +[8.441927, "o", "e"] +[8.646449, "o", "x"] +[8.76668, "o", "i"] +[8.897799, "o", "t"] +[9.091614, "o", "\r\n\u001b[?2004l\rexit\r\n"] diff --git a/_images/sources/components/messenger/overview.dia b/_images/sources/components/messenger/overview.dia new file mode 100644 index 00000000000..b0e2edaeab2 Binary files /dev/null and b/_images/sources/components/messenger/overview.dia differ diff --git a/_images/sources/doctrine/mapping_relations.dia b/_images/sources/doctrine/mapping_relations.dia new file mode 100644 index 00000000000..5703e1b781c Binary files /dev/null and b/_images/sources/doctrine/mapping_relations.dia differ diff --git a/_images/sources/doctrine/mapping_relations_proxy.dia b/_images/sources/doctrine/mapping_relations_proxy.dia new file mode 100644 index 00000000000..1f491e9e2ef Binary files /dev/null and b/_images/sources/doctrine/mapping_relations_proxy.dia differ diff --git a/_images/sources/doctrine/mapping_single_entity.dia b/_images/sources/doctrine/mapping_single_entity.dia new file mode 100644 index 00000000000..5a9dc21889c Binary files /dev/null and b/_images/sources/doctrine/mapping_single_entity.dia differ diff --git a/_images/sources/form/data-transformer-types.dia b/_images/sources/form/data-transformer-types.dia new file mode 100644 index 00000000000..972b973a36d Binary files /dev/null and b/_images/sources/form/data-transformer-types.dia differ diff --git a/_images/sources/form/form-custom-type-postal-address-fragment-names.dia b/_images/sources/form/form-custom-type-postal-address-fragment-names.dia new file mode 100644 index 00000000000..ca12fcdeadc Binary files /dev/null and b/_images/sources/form/form-custom-type-postal-address-fragment-names.dia differ diff --git a/_images/sources/form/form-custom-type-postal-address.dia b/_images/sources/form/form-custom-type-postal-address.dia new file mode 100644 index 00000000000..1b7c6226315 Binary files /dev/null and b/_images/sources/form/form-custom-type-postal-address.dia differ diff --git a/_images/sources/form/form-field-parts.dia b/_images/sources/form/form-field-parts.dia new file mode 100644 index 00000000000..d6ed2dfc3fe Binary files /dev/null and b/_images/sources/form/form-field-parts.dia differ diff --git a/_images/sources/form/form_events.dia b/_images/sources/form/form_events.dia new file mode 100644 index 00000000000..8e7afb1cb83 Binary files /dev/null and b/_images/sources/form/form_events.dia differ diff --git a/_images/sources/form/form_prepopulation_workflow.dia b/_images/sources/form/form_prepopulation_workflow.dia new file mode 100644 index 00000000000..1d6d450fed1 Binary files /dev/null and b/_images/sources/form/form_prepopulation_workflow.dia differ diff --git a/_images/sources/form/form_submission_workflow.dia b/_images/sources/form/form_submission_workflow.dia new file mode 100644 index 00000000000..cc08f117878 Binary files /dev/null and b/_images/sources/form/form_submission_workflow.dia differ diff --git a/_images/sources/form/form_workflow.dia b/_images/sources/form/form_workflow.dia new file mode 100644 index 00000000000..30f9acabe2b Binary files /dev/null and b/_images/sources/form/form_workflow.dia differ diff --git a/_images/sources/http/request-flow.dia b/_images/sources/http/request-flow.dia new file mode 100644 index 00000000000..ca09a05504e Binary files /dev/null and b/_images/sources/http/request-flow.dia differ diff --git a/_images/sources/http/xkcd-full.dia b/_images/sources/http/xkcd-full.dia new file mode 100644 index 00000000000..a730d01c3ef Binary files /dev/null and b/_images/sources/http/xkcd-full.dia differ diff --git a/_images/sources/http/xkcd-request.dia b/_images/sources/http/xkcd-request.dia new file mode 100644 index 00000000000..3796228bf1d Binary files /dev/null and b/_images/sources/http/xkcd-request.dia differ diff --git a/_images/sources/http_kernel/http-workflow.dia b/_images/sources/http_kernel/http-workflow.dia new file mode 100644 index 00000000000..2b84bc46aec Binary files /dev/null and b/_images/sources/http_kernel/http-workflow.dia differ diff --git a/_images/sources/mercure/discovery.dia b/_images/sources/mercure/discovery.dia new file mode 100644 index 00000000000..3db5c86f020 Binary files /dev/null and b/_images/sources/mercure/discovery.dia differ diff --git a/_images/sources/mercure/hub.dia b/_images/sources/mercure/hub.dia new file mode 100644 index 00000000000..b0dfb9d88d2 Binary files /dev/null and b/_images/sources/mercure/hub.dia differ diff --git a/_images/sources/rate_limiter/fixed_window.dia b/_images/sources/rate_limiter/fixed_window.dia new file mode 100644 index 00000000000..16282a2dcce Binary files /dev/null and b/_images/sources/rate_limiter/fixed_window.dia differ diff --git a/_images/sources/rate_limiter/sliding_window.dia b/_images/sources/rate_limiter/sliding_window.dia new file mode 100644 index 00000000000..e16275d8995 Binary files /dev/null and b/_images/sources/rate_limiter/sliding_window.dia differ diff --git a/_images/sources/rate_limiter/token_bucket.dia b/_images/sources/rate_limiter/token_bucket.dia new file mode 100644 index 00000000000..16761971337 Binary files /dev/null and b/_images/sources/rate_limiter/token_bucket.dia differ diff --git a/_images/sources/security/authentication-guard-methods.dia b/_images/sources/security/authentication-guard-methods.dia new file mode 100644 index 00000000000..d655be780fe Binary files /dev/null and b/_images/sources/security/authentication-guard-methods.dia differ diff --git a/_images/sources/security/security_events.dia b/_images/sources/security/security_events.dia new file mode 100644 index 00000000000..0a8afa73179 Binary files /dev/null and b/_images/sources/security/security_events.dia differ diff --git a/_images/sources/serializer/serializer_workflow.dia b/_images/sources/serializer/serializer_workflow.dia new file mode 100644 index 00000000000..3e2ea62558f Binary files /dev/null and b/_images/sources/serializer/serializer_workflow.dia differ diff --git a/_images/translation/pseudolocalization-interface-original.png b/_images/translation/pseudolocalization-interface-original.png new file mode 100644 index 00000000000..d89f4e63a24 Binary files /dev/null and b/_images/translation/pseudolocalization-interface-original.png differ diff --git a/_images/translation/pseudolocalization-interface-translated.png b/_images/translation/pseudolocalization-interface-translated.png new file mode 100644 index 00000000000..496d5a0f86f Binary files /dev/null and b/_images/translation/pseudolocalization-interface-translated.png differ diff --git a/_images/translation/pseudolocalization-symfony-demo-disabled.png b/_images/translation/pseudolocalization-symfony-demo-disabled.png new file mode 100644 index 00000000000..1a7472bd41f Binary files /dev/null and b/_images/translation/pseudolocalization-symfony-demo-disabled.png differ diff --git a/_images/translation/pseudolocalization-symfony-demo-enabled.png b/_images/translation/pseudolocalization-symfony-demo-enabled.png new file mode 100644 index 00000000000..a23300a7271 Binary files /dev/null and b/_images/translation/pseudolocalization-symfony-demo-enabled.png differ diff --git a/_includes/_rewrite_rule_tip.rst.inc b/_includes/_rewrite_rule_tip.rst.inc new file mode 100644 index 00000000000..fe69882c4f7 --- /dev/null +++ b/_includes/_rewrite_rule_tip.rst.inc @@ -0,0 +1,6 @@ +.. tip:: + + By using rewrite rules in your + :doc:`web server configuration `, + the ``index.php`` won't be needed and you will have beautiful, clean URLs + (e.g. ``/show``). diff --git a/best_practices.rst b/best_practices.rst new file mode 100644 index 00000000000..2c393cae9c6 --- /dev/null +++ b/best_practices.rst @@ -0,0 +1,460 @@ +The Symfony Framework Best Practices +==================================== + +This article describes the **best practices for developing web applications with +Symfony** that fit the philosophy envisioned by the original Symfony creators. + +If you don't agree with some of these recommendations, they might be a good +**starting point** that you can then **extend and fit to your specific needs**. +You can even ignore them completely and continue using your own best practices +and methodologies. Symfony is flexible enough to adapt to your needs. + +This article assumes that you already have experience developing Symfony +applications. If you don't, read first the :doc:`Getting Started ` +section of the documentation. + +.. tip:: + + Symfony provides a sample application called `Symfony Demo`_ that follows + all these best practices, so you can experience them in practice. + +Creating the Project +-------------------- + +Use the Symfony Binary to Create Symfony Applications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Symfony binary is an executable command created in your machine when you +`download Symfony`_. It provides multiple utilities, including the simplest way +to create new Symfony applications: + +.. code-block:: terminal + + $ symfony new my_project_directory + +Under the hood, this Symfony binary command executes the needed `Composer`_ +command to :ref:`create a new Symfony application ` +based on the current stable version. + +Use the Default Directory Structure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Unless your project follows a development practice that imposes a certain +directory structure, follow the default Symfony directory structure. It's flat, +self-explanatory and not coupled to Symfony: + +.. code-block:: text + + your_project/ + ├─ assets/ + ├─ bin/ + │ └─ console + ├─ config/ + │ ├─ packages/ + │ ├─ routes/ + │ └─ services.yaml + ├─ migrations/ + ├─ public/ + │ ├─ build/ + │ └─ index.php + ├─ src/ + │ ├─ Kernel.php + │ ├─ Command/ + │ ├─ Controller/ + │ ├─ DataFixtures/ + │ ├─ Entity/ + │ ├─ EventSubscriber/ + │ ├─ Form/ + │ ├─ Repository/ + │ ├─ Security/ + │ └─ Twig/ + ├─ templates/ + ├─ tests/ + ├─ translations/ + ├─ var/ + │ ├─ cache/ + │ └─ log/ + └─ vendor/ + +Configuration +------------- + +Use Environment Variables for Infrastructure Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The values of these options change from one machine to another (e.g. from your +development machine to the production server), but they don't modify the +application behavior. + +:ref:`Use env vars in your project ` to define these options +and create multiple ``.env`` files to :ref:`configure env vars per environment `. + +.. _use-secret-for-sensitive-information: + +Use Secrets for Sensitive Information +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When your application has sensitive configuration, like an API key, you should +store those securely via :doc:`Symfony’s secrets management system `. + +Use Parameters for Application Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These are the options used to modify the application behavior, such as the sender +of email notifications, or the enabled `feature toggles`_. Their value doesn't +change per machine, so don't define them as environment variables. + +Define these options as :ref:`parameters ` in the +``config/services.yaml`` file. You can override these options per +:ref:`environment ` in the ``config/services_dev.yaml`` +and ``config/services_prod.yaml`` files. + +Unless the application configuration is reused multiple times and needs +rigid validation, do *not* use the :doc:`Config component ` +to define the options. + +Use Short and Prefixed Parameter Names +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Consider using ``app.`` as the prefix of your :ref:`parameters ` +to avoid collisions with Symfony and third-party bundles/libraries parameters. +Then, use just one or two words to describe the purpose of the parameter: + +.. code-block:: yaml + + # config/services.yaml + parameters: + # don't do this: 'dir' is too generic, and it doesn't convey any meaning + app.dir: '...' + # do this: short but easy to understand names + app.contents_dir: '...' + # it's OK to use dots, underscores, dashes or nothing, but always + # be consistent and use the same format for all the parameters + app.dir.contents: '...' + app.contents-dir: '...' + +Use Constants to Define Options that Rarely Change +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Configuration options like the number of items to display in some listing rarely +change. Instead of defining them as :ref:`configuration parameters `, +define them as PHP constants in the related classes. Example:: + + // src/Entity/Post.php + namespace App\Entity; + + class Post + { + public const NUMBER_OF_ITEMS = 10; + + // ... + } + +The main advantage of constants is that you can use them everywhere, including +Twig templates and Doctrine entities, whereas parameters are only available +from places with access to the :doc:`service container `. + +The only notable disadvantage of using constants for this kind of configuration +values is that it's complicated to redefine their values in your tests. + +Business Logic +-------------- + +.. _best-practice-no-application-bundles: + +Don't Create any Bundle to Organize your Application Logic +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When Symfony 2.0 was released, applications used :doc:`bundles ` to +divide their code into logical features: UserBundle, ProductBundle, +InvoiceBundle, etc. However, a bundle is meant to be something that can be +reused as a stand-alone piece of software. + +If you need to reuse some feature in your projects, create a bundle for it (in a +private repository, do not make it publicly available). For the rest of your +application code, use PHP namespaces to organize code instead of bundles. + +Use Autowiring to Automate the Configuration of Application Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:doc:`Service autowiring ` is a feature that +reads the type-hints on your constructor (or other methods) and automatically +passes the correct services to each method, making it unnecessary to configure +services explicitly and simplifying the application maintenance. + +Use it in combination with :ref:`service autoconfiguration ` +to also add :doc:`service tags ` to the services +needing them, such as Twig extensions, event subscribers, etc. + +Services Should be Private Whenever Possible +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:ref:`Make services private ` to prevent you from accessing +those services via ``$container->get()``. Instead, you will need to use proper +dependency injection. + +Use the YAML Format to Configure your own Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you use the :ref:`default services.yaml configuration `, +most services will be configured automatically. However, in some edge cases +you'll need to configure services (or parts of them) manually. + +YAML is the format recommended configuring services because it's friendly to +newcomers and concise, but Symfony also supports XML and PHP configuration. + +Use Attributes to Define the Doctrine Entity Mapping +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Doctrine entities are plain PHP objects that you store in some "database". +Doctrine only knows about your entities through the mapping metadata configured +for your model classes. + +Doctrine supports several metadata formats, but it's recommended to use PHP +attributes because they are by far the most convenient and agile way of setting +up and looking for mapping information. + +Controllers +----------- + +Make your Controller Extend the ``AbstractController`` Base Controller +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides a :ref:`base controller ` +which includes shortcuts for the most common needs such as rendering templates +or checking security permissions. + +Extending your controllers from this base controller couples your application +to Symfony. Coupling is generally wrong, but it may be OK in this case because +controllers shouldn't contain any business logic. Controllers should contain +nothing more than a few lines of *glue-code*, so you are not coupling the +important parts of your application. + +.. _best-practice-controller-annotations: +.. _best-practice-controller-attributes: + +Use Attributes to Configure Routing, Caching, and Security +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using attributes for routing, caching, and security simplifies +configuration. You don't need to browse several files created with different +formats (YAML, XML, PHP): all the configuration is just where you require it, +and it only uses one format. + +Use Dependency Injection to Get Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you extend the base ``AbstractController``, you can only get access to the most +common services (e.g ``twig``, ``router``, ``doctrine``, etc.), directly from the +container via ``$this->container->get()``. +Instead, you must use dependency injection to fetch services by +:ref:`type-hinting action method arguments ` or +constructor arguments. + +Use Entity Value Resolvers If They Are Convenient +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you're using :doc:`Doctrine `, then you can *optionally* use +the :ref:`EntityValueResolver ` to +automatically query for an entity and pass it as an argument to your +controller. It will also show a 404 page if no entity can be found. + +If the logic to get an entity from a route variable is more complex, instead of +configuring the EntityValueResolver, it's better to make the Doctrine query +inside the controller (e.g. by calling to a :doc:`Doctrine repository method `). + +Templates +--------- + +Use Snake Case for Template Names and Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use lowercase snake_case for template names, directories, and variables (e.g. +``user_profile`` instead of ``userProfile`` and ``product/edit_form.html.twig`` +instead of ``Product/EditForm.html.twig``). + +Prefix Template Fragments with an Underscore +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Template fragments, also called *"partial templates"*, allow to +:ref:`reuse template contents `. Prefix their names +with an underscore to better differentiate them from complete templates (e.g. +``_user_metadata.html.twig`` or ``_caution_message.html.twig``). + +Forms +----- + +Define your Forms as PHP Classes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Creating :ref:`forms in classes ` allows reusing +them in different parts of the application. Besides, not creating forms in +controllers simplifies the code and maintenance of the controllers. + +Add Form Buttons in Templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Form classes should be agnostic to where they will be used. For example, the +button of a form used to both create and edit items should change from "Add new" +to "Save changes" depending on where it's used. + +Instead of adding buttons in form classes or the controllers, it's recommended +to add buttons in the templates. This also improves the separation of concerns +because the button styling (CSS class and other attributes) is defined in the +template instead of in a PHP class. + +However, if you create a :doc:`form with multiple submit buttons ` +you should define them in the controller instead of the template. Otherwise, you +won't be able to check which button was clicked when handling the form in the controller. + +Define Validation Constraints on the Underlying Object +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Attaching :doc:`validation constraints ` to form fields +instead of to the mapped object prevents the validation from being reused in +other forms or other places where the object is used. + +.. _best-practice-handle-form: + +Use a Single Action to Render and Process the Form +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:ref:`Rendering forms ` and :ref:`processing forms ` +are two of the main tasks when handling forms. Both are too similar (most of the +time, almost identical), so it's much simpler to let a single controller action +handle both. + +.. _best-practice-internationalization: + +Internationalization +-------------------- + +Use the XLIFF Format for Your Translation Files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Of all the translation formats supported by Symfony (PHP, Qt, ``.po``, ``.mo``, +JSON, CSV, INI, etc.), ``XLIFF`` and ``gettext`` have the best support in the tools used +by professional translators. And since it's based on XML, you can validate ``XLIFF`` +file contents as you write them. + +Symfony also supports notes in XLIFF files, making them more user-friendly for +translators. At the end, good translations are all about context, and these +XLIFF notes allow you to define that context. + +Use Keys for Translations Instead of Content Strings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using keys simplifies the management of the translation files because you can +change the original contents in templates, controllers, and services without +having to update all the translation files. + +Keys should always describe their *purpose* and *not* their location. For +example, if a form has a field with the label "Username", then a nice key +would be ``label.username``, *not* ``edit_form.label.username``. + +Security +-------- + +Define a Single Firewall +~~~~~~~~~~~~~~~~~~~~~~~~ + +Unless you have two legitimately different authentication systems and users +(e.g. form login for the main site and a token system for your API only), it's +recommended to have only one firewall to keep things simple. + +Additionally, you should use the ``anonymous`` key under your firewall. If you +require users to be logged in for different sections of your site, use the +:doc:`access_control ` option. + +Use the ``auto`` Password Hasher +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :ref:`auto password hasher ` automatically +selects the best possible encoder/hasher depending on your PHP installation. +Currently, the default auto hasher is ``bcrypt``. + +Use Voters to Implement Fine-grained Security Restrictions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your security logic is complex, you should create custom +:doc:`security voters ` instead of defining long expressions +inside the ``#[Security]`` attribute. + +Web Assets +---------- + +.. _use-webpack-encore-to-process-web-assets: + +Use AssetMapper to Manage Web Assets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Web assets are the CSS, JavaScript, and image files that make the frontend of +your site look and work great. :doc:`AssetMapper ` lets +you write modern JavaScript and CSS without the complexity of using a bundler +such as `Webpack`_ (directly or via :doc:`Webpack Encore `). + +Tests +----- + +Smoke Test your URLs +~~~~~~~~~~~~~~~~~~~~ + +In software engineering, `smoke testing`_ consists of *"preliminary testing to +reveal simple failures severe enough to reject a prospective software release"*. +Using `PHPUnit data providers`_ you can define a functional test that +checks that all application URLs load successfully:: + + // tests/ApplicationAvailabilityFunctionalTest.php + namespace App\Tests; + + use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + + class ApplicationAvailabilityFunctionalTest extends WebTestCase + { + /** + * @dataProvider urlProvider + */ + public function testPageIsSuccessful($url): void + { + $client = self::createClient(); + $client->request('GET', $url); + + $this->assertResponseIsSuccessful(); + } + + public function urlProvider(): \Generator + { + yield ['/']; + yield ['/posts']; + yield ['/post/fixture-post-1']; + yield ['/blog/category/fixture-category']; + yield ['/archives']; + // ... + } + } + +Add this test while creating your application because it requires little effort +and checks that none of your pages returns an error. Later, you'll add more +specific tests for each page. + +.. _hardcode-urls-in-a-functional-test: + +Hard-code URLs in a Functional Test +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In Symfony applications, it's recommended to :ref:`generate URLs ` +using routes to automatically update all links when a URL changes. However, if a +public URL changes, users won't be able to browse it unless you set up a +redirection to the new URL. + +That's why it's recommended to use raw URLs in tests instead of generating them +from routes. Whenever a route changes, tests will fail, and you'll know that +you must set up a redirection. + +.. _`Symfony Demo`: https://github.com/symfony/demo +.. _`download Symfony`: https://symfony.com/download +.. _`Composer`: https://getcomposer.org/ +.. _`feature toggles`: https://en.wikipedia.org/wiki/Feature_toggle +.. _`smoke testing`: https://en.wikipedia.org/wiki/Smoke_testing_(software) +.. _`Webpack`: https://webpack.js.org/ +.. _`PHPUnit data providers`: https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#data-providers diff --git a/book/controller.rst b/book/controller.rst deleted file mode 100644 index 06422cf8351..00000000000 --- a/book/controller.rst +++ /dev/null @@ -1,788 +0,0 @@ -.. index:: - single: Controller - -Controller -========== - -A controller is a PHP function you create that takes information from the -HTTP request and constructs and returns an HTTP response (as a Symfony2 -``Response`` object). The response could be an HTML page, an XML document, -a serialized JSON array, an image, a redirect, a 404 error or anything else -you can dream up. The controller contains whatever arbitrary logic *your -application* needs to render the content of a page. - -See how simple this is by looking at a Symfony2 controller in action. -The following controller would render a page that simply prints ``Hello world!``:: - - use Symfony\Component\HttpFoundation\Response; - - public function helloAction() - { - return new Response('Hello world!'); - } - -The goal of a controller is always the same: create and return a ``Response`` -object. Along the way, it might read information from the request, load a -database resource, send an email, or set information on the user's session. -But in all cases, the controller will eventually return the ``Response`` object -that will be delivered back to the client. - -There's no magic and no other requirements to worry about! Here are a few -common examples: - -* *Controller A* prepares a ``Response`` object representing the content - for the homepage of the site. - -* *Controller B* reads the ``slug`` parameter from the request to load a - blog entry from the database and create a ``Response`` object displaying - that blog. If the ``slug`` can't be found in the database, it creates and - returns a ``Response`` object with a 404 status code. - -* *Controller C* handles the form submission of a contact form. It reads - the form information from the request, saves the contact information to - the database and emails the contact information to the webmaster. Finally, - it creates a ``Response`` object that redirects the client's browser to - the contact form "thank you" page. - -.. index:: - single: Controller; Request-controller-response lifecycle - -Requests, Controller, Response Lifecycle ----------------------------------------- - -Every request handled by a Symfony2 project goes through the same simple lifecycle. -The framework takes care of the repetitive tasks and ultimately executes a -controller, which houses your custom application code: - -#. Each request is handled by a single front controller file (e.g. ``app.php`` - or ``app_dev.php``) that bootstraps the application; - -#. The ``Router`` reads information from the request (e.g. the URI), finds - a route that matches that information, and reads the ``_controller`` parameter - from the route; - -#. The controller from the matched route is executed and the code inside the - controller creates and returns a ``Response`` object; - -#. The HTTP headers and content of the ``Response`` object are sent back to - the client. - -Creating a page is as easy as creating a controller (#3) and making a route that -maps a URL to that controller (#2). - -.. note:: - - Though similarly named, a "front controller" is different from the - "controllers" talked about in this chapter. A front controller - is a short PHP file that lives in your web directory and through which - all requests are directed. A typical application will have a production - front controller (e.g. ``app.php``) and a development front controller - (e.g. ``app_dev.php``). You'll likely never need to edit, view or worry - about the front controllers in your application. - -.. index:: - single: Controller; Simple example - -A Simple Controller -------------------- - -While a controller can be any PHP callable (a function, method on an object, -or a ``Closure``), in Symfony2, a controller is usually a single method inside -a controller object. Controllers are also called *actions*. - -.. code-block:: php - :linenos: - - // src/Acme/HelloBundle/Controller/HelloController.php - namespace Acme\HelloBundle\Controller; - - use Symfony\Component\HttpFoundation\Response; - - class HelloController - { - public function indexAction($name) - { - return new Response('Hello '.$name.'!'); - } - } - -.. tip:: - - Note that the *controller* is the ``indexAction`` method, which lives - inside a *controller class* (``HelloController``). Don't be confused - by the naming: a *controller class* is simply a convenient way to group - several controllers/actions together. Typically, the controller class - will house several controllers/actions (e.g. ``updateAction``, ``deleteAction``, - etc). - -This controller is pretty straightforward: - -* *line 4*: Symfony2 takes advantage of PHP 5.3 namespace functionality to - namespace the entire controller class. The ``use`` keyword imports the - ``Response`` class, which the controller must return. - -* *line 6*: The class name is the concatenation of a name for the controller - class (i.e. ``Hello``) and the word ``Controller``. This is a convention - that provides consistency to controllers and allows them to be referenced - only by the first part of the name (i.e. ``Hello``) in the routing configuration. - -* *line 8*: Each action in a controller class is suffixed with ``Action`` - and is referenced in the routing configuration by the action's name (``index``). - In the next section, you'll create a route that maps a URI to this action. - You'll learn how the route's placeholders (``{name}``) become arguments - to the action method (``$name``). - -* *line 10*: The controller creates and returns a ``Response`` object. - -.. index:: - single: Controller; Routes and controllers - -Mapping a URL to a Controller ------------------------------ - -The new controller returns a simple HTML page. To actually view this page -in your browser, you need to create a route, which maps a specific URL path -to the controller: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - hello: - path: /hello/{name} - defaults: { _controller: AcmeHelloBundle:Hello:index } - - .. code-block:: xml - - - - AcmeHelloBundle:Hello:index - - - .. code-block:: php - - // app/config/routing.php - $collection->add('hello', new Route('/hello/{name}', array( - '_controller' => 'AcmeHelloBundle:Hello:index', - ))); - -Going to ``/hello/ryan`` now executes the ``HelloController::indexAction()`` -controller and passes in ``ryan`` for the ``$name`` variable. Creating a -"page" means simply creating a controller method and associated route. - -Notice the syntax used to refer to the controller: ``AcmeHelloBundle:Hello:index``. -Symfony2 uses a flexible string notation to refer to different controllers. -This is the most common syntax and tells Symfony2 to look for a controller -class called ``HelloController`` inside a bundle named ``AcmeHelloBundle``. The -method ``indexAction()`` is then executed. - -For more details on the string format used to reference different controllers, -see :ref:`controller-string-syntax`. - -.. note:: - - This example places the routing configuration directly in the ``app/config/`` - directory. A better way to organize your routes is to place each route - in the bundle it belongs to. For more information on this, see - :ref:`routing-include-external-resources`. - -.. tip:: - - You can learn much more about the routing system in the :doc:`Routing chapter`. - -.. index:: - single: Controller; Controller arguments - -.. _route-parameters-controller-arguments: - -Route Parameters as Controller Arguments -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You already know that the ``_controller`` parameter ``AcmeHelloBundle:Hello:index`` -refers to a ``HelloController::indexAction()`` method that lives inside the -``AcmeHelloBundle`` bundle. What's more interesting is the arguments that are -passed to that method:: - - // src/Acme/HelloBundle/Controller/HelloController.php - namespace Acme\HelloBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - - class HelloController extends Controller - { - public function indexAction($name) - { - // ... - } - } - -The controller has a single argument, ``$name``, which corresponds to the -``{name}`` parameter from the matched route (``ryan`` in the example). In -fact, when executing your controller, Symfony2 matches each argument of -the controller with a parameter from the matched route. Take the following -example: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - hello: - path: /hello/{first_name}/{last_name} - defaults: { _controller: AcmeHelloBundle:Hello:index, color: green } - - .. code-block:: xml - - - - AcmeHelloBundle:Hello:index - green - - - .. code-block:: php - - // app/config/routing.php - $collection->add('hello', new Route('/hello/{first_name}/{last_name}', array( - '_controller' => 'AcmeHelloBundle:Hello:index', - 'color' => 'green', - ))); - -The controller for this can take several arguments:: - - public function indexAction($first_name, $last_name, $color) - { - // ... - } - -Notice that both placeholder variables (``{first_name}``, ``{last_name}``) -as well as the default ``color`` variable are available as arguments in the -controller. When a route is matched, the placeholder variables are merged -with the ``defaults`` to make one array that's available to your controller. - -Mapping route parameters to controller arguments is easy and flexible. Keep -the following guidelines in mind while you develop. - -* **The order of the controller arguments does not matter** - - Symfony is able to match the parameter names from the route to the variable - names in the controller method's signature. In other words, it realizes that - the ``{last_name}`` parameter matches up with the ``$last_name`` argument. - The arguments of the controller could be totally reordered and still work - perfectly:: - - public function indexAction($last_name, $color, $first_name) - { - // ... - } - -* **Each required controller argument must match up with a routing parameter** - - The following would throw a ``RuntimeException`` because there is no ``foo`` - parameter defined in the route:: - - public function indexAction($first_name, $last_name, $color, $foo) - { - // ... - } - - Making the argument optional, however, is perfectly ok. The following - example would not throw an exception:: - - public function indexAction($first_name, $last_name, $color, $foo = 'bar') - { - // ... - } - -* **Not all routing parameters need to be arguments on your controller** - - If, for example, the ``last_name`` weren't important for your controller, - you could omit it entirely:: - - public function indexAction($first_name, $color) - { - // ... - } - -.. tip:: - - Every route also has a special ``_route`` parameter, which is equal to - the name of the route that was matched (e.g. ``hello``). Though not usually - useful, this is equally available as a controller argument. - -.. _book-controller-request-argument: - -The ``Request`` as a Controller Argument -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For convenience, you can also have Symfony pass you the ``Request`` object -as an argument to your controller. This is especially convenient when you're -working with forms, for example:: - - use Symfony\Component\HttpFoundation\Request; - - public function updateAction(Request $request) - { - $form = $this->createForm(...); - - $form->handleRequest($request); - // ... - } - -.. index:: - single: Controller; Base controller class - -Creating Static Pages ---------------------- - -You can create a static page without even creating a controller (only a route -and template are needed). - -Use it! See :doc:`/cookbook/templating/render_without_controller`. - -The Base Controller Class -------------------------- - -For convenience, Symfony2 comes with a base ``Controller`` class that assists -with some of the most common controller tasks and gives your controller class -access to any resource it might need. By extending this ``Controller`` class, -you can take advantage of several helper methods. - -Add the ``use`` statement atop the ``Controller`` class and then modify the -``HelloController`` to extend it:: - - // src/Acme/HelloBundle/Controller/HelloController.php - namespace Acme\HelloBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\HttpFoundation\Response; - - class HelloController extends Controller - { - public function indexAction($name) - { - return new Response('Hello '.$name.'!'); - } - } - -This doesn't actually change anything about how your controller works. In -the next section, you'll learn about the helper methods that the base controller -class makes available. These methods are just shortcuts to using core Symfony2 -functionality that's available to you with or without the use of the base -``Controller`` class. A great way to see the core functionality in action -is to look in the -:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` class -itself. - -.. tip:: - - Extending the base class is *optional* in Symfony; it contains useful - shortcuts but nothing mandatory. You can also extend - :class:`Symfony\\Component\\DependencyInjection\\ContainerAware`. The service - container object will then be accessible via the ``container`` property. - -.. note:: - - You can also define your :doc:`Controllers as Services`. - -.. index:: - single: Controller; Common tasks - -Common Controller Tasks ------------------------ - -Though a controller can do virtually anything, most controllers will perform -the same basic tasks over and over again. These tasks, such as redirecting, -forwarding, rendering templates and accessing core services, are very easy -to manage in Symfony2. - -.. index:: - single: Controller; Redirecting - -Redirecting -~~~~~~~~~~~ - -If you want to redirect the user to another page, use the ``redirect()`` method:: - - public function indexAction() - { - return $this->redirect($this->generateUrl('homepage')); - } - -The ``generateUrl()`` method is just a helper function that generates the URL -for a given route. For more information, see the :doc:`Routing ` -chapter. - -By default, the ``redirect()`` method performs a 302 (temporary) redirect. To -perform a 301 (permanent) redirect, modify the second argument:: - - public function indexAction() - { - return $this->redirect($this->generateUrl('homepage'), 301); - } - -.. tip:: - - The ``redirect()`` method is simply a shortcut that creates a ``Response`` - object that specializes in redirecting the user. It's equivalent to:: - - use Symfony\Component\HttpFoundation\RedirectResponse; - - return new RedirectResponse($this->generateUrl('homepage')); - -.. index:: - single: Controller; Forwarding - -Forwarding -~~~~~~~~~~ - -You can also easily forward to another controller internally with the ``forward()`` -method. Instead of redirecting the user's browser, it makes an internal sub-request, -and calls the specified controller. The ``forward()`` method returns the ``Response`` -object that's returned from that controller:: - - public function indexAction($name) - { - $response = $this->forward('AcmeHelloBundle:Hello:fancy', array( - 'name' => $name, - 'color' => 'green', - )); - - // ... further modify the response or return it directly - - return $response; - } - -Notice that the `forward()` method uses the same string representation of -the controller used in the routing configuration. In this case, the target -controller class will be ``HelloController`` inside some ``AcmeHelloBundle``. -The array passed to the method becomes the arguments on the resulting controller. -This same interface is used when embedding controllers into templates (see -:ref:`templating-embedding-controller`). The target controller method should -look something like the following:: - - public function fancyAction($name, $color) - { - // ... create and return a Response object - } - -And just like when creating a controller for a route, the order of the arguments -to ``fancyAction`` doesn't matter. Symfony2 matches the index key names -(e.g. ``name``) with the method argument names (e.g. ``$name``). If you -change the order of the arguments, Symfony2 will still pass the correct -value to each variable. - -.. tip:: - - Like other base ``Controller`` methods, the ``forward`` method is just - a shortcut for core Symfony2 functionality. A forward can be accomplished - directly via the ``http_kernel`` service. A forward returns a ``Response`` - object:: - - $httpKernel = $this->container->get('http_kernel'); - $response = $httpKernel->forward( - 'AcmeHelloBundle:Hello:fancy', - array( - 'name' => $name, - 'color' => 'green', - ) - ); - -.. index:: - single: Controller; Rendering templates - -.. _controller-rendering-templates: - -Rendering Templates -~~~~~~~~~~~~~~~~~~~ - -Though not a requirement, most controllers will ultimately render a template -that's responsible for generating the HTML (or other format) for the controller. -The ``renderView()`` method renders a template and returns its content. The -content from the template can be used to create a ``Response`` object:: - - use Symfony\Component\HttpFoundation\Response; - - $content = $this->renderView( - 'AcmeHelloBundle:Hello:index.html.twig', - array('name' => $name) - ); - - return new Response($content); - -This can even be done in just one step with the ``render()`` method, which -returns a ``Response`` object containing the content from the template:: - - return $this->render( - 'AcmeHelloBundle:Hello:index.html.twig', - array('name' => $name) - ); - -In both cases, the ``Resources/views/Hello/index.html.twig`` template inside -the ``AcmeHelloBundle`` will be rendered. - -The Symfony templating engine is explained in great detail in the -:doc:`Templating ` chapter. - -.. tip:: - - You can even avoid calling the ``render`` method by using the ``@Template`` - annotation. See the :doc:`FrameworkExtraBundle documentation` - more details. - -.. tip:: - - The ``renderView`` method is a shortcut to direct use of the ``templating`` - service. The ``templating`` service can also be used directly:: - - $templating = $this->get('templating'); - $content = $templating->render( - 'AcmeHelloBundle:Hello:index.html.twig', - array('name' => $name) - ); - -.. note:: - - It is possible to render templates in deeper subdirectories as well, however - be careful to avoid the pitfall of making your directory structure unduly - elaborate:: - - $templating->render( - 'AcmeHelloBundle:Hello/Greetings:index.html.twig', - array('name' => $name) - ); - // index.html.twig found in Resources/views/Hello/Greetings is rendered. - -.. index:: - single: Controller; Accessing services - -Accessing other Services -~~~~~~~~~~~~~~~~~~~~~~~~ - -When extending the base controller class, you can access any Symfony2 service -via the ``get()`` method. Here are several common services you might need:: - - $request = $this->getRequest(); - - $templating = $this->get('templating'); - - $router = $this->get('router'); - - $mailer = $this->get('mailer'); - -There are countless other services available and you are encouraged to define -your own. To list all available services, use the ``container:debug`` console -command: - -.. code-block:: bash - - $ php app/console container:debug - -For more information, see the :doc:`/book/service_container` chapter. - -.. index:: - single: Controller; Managing errors - single: Controller; 404 pages - -Managing Errors and 404 Pages ------------------------------ - -When things are not found, you should play well with the HTTP protocol and -return a 404 response. To do this, you'll throw a special type of exception. -If you're extending the base controller class, do the following:: - - public function indexAction() - { - // retrieve the object from database - $product = ...; - if (!$product) { - throw $this->createNotFoundException('The product does not exist'); - } - - return $this->render(...); - } - -The ``createNotFoundException()`` method creates a special ``NotFoundHttpException`` -object, which ultimately triggers a 404 HTTP response inside Symfony. - -Of course, you're free to throw any ``Exception`` class in your controller - -Symfony2 will automatically return a 500 HTTP response code. - -.. code-block:: php - - throw new \Exception('Something went wrong!'); - -In every case, a styled error page is shown to the end user and a full debug -error page is shown to the developer (when viewing the page in debug mode). -Both of these error pages can be customized. For details, read the -":doc:`/cookbook/controller/error_pages`" cookbook recipe. - -.. index:: - single: Controller; The session - single: Session - -Managing the Session --------------------- - -Symfony2 provides a nice session object that you can use to store information -about the user (be it a real person using a browser, a bot, or a web service) -between requests. By default, Symfony2 stores the attributes in a cookie -by using the native PHP sessions. - -Storing and retrieving information from the session can be easily achieved -from any controller:: - - $session = $this->getRequest()->getSession(); - - // store an attribute for reuse during a later user request - $session->set('foo', 'bar'); - - // in another controller for another request - $foo = $session->get('foo'); - - // use a default value if the key doesn't exist - $filters = $session->get('filters', array()); - -These attributes will remain on the user for the remainder of that user's -session. - -.. index:: - single: Session; Flash messages - -Flash Messages -~~~~~~~~~~~~~~ - -You can also store small messages that will be stored on the user's session -for exactly one additional request. This is useful when processing a form: -you want to redirect and have a special message shown on the *next* request. -These types of messages are called "flash" messages. - -For example, imagine you're processing a form submit:: - - public function updateAction() - { - $form = $this->createForm(...); - - $form->handleRequest($this->getRequest()); - - if ($form->isValid()) { - // do some sort of processing - - $this->get('session')->getFlashBag()->add('notice', 'Your changes were saved!'); - - return $this->redirect($this->generateUrl(...)); - } - - return $this->render(...); - } - -After processing the request, the controller sets a ``notice`` flash message -and then redirects. The name (``notice``) isn't significant - it's just what -you're using to identify the type of the message. - -In the template of the next action, the following code could be used to render -the ``notice`` message: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% for flashMessage in app.session.flashbag.get('notice') %} -
- {{ flashMessage }} -
- {% endfor %} - - .. code-block:: html+php - - getFlashBag()->get('notice') as $message): ?> -
- $message
" ?> - - - -By design, flash messages are meant to live for exactly one request (they're -"gone in a flash"). They're designed to be used across redirects exactly as -you've done in this example. - -.. index:: - single: Controller; Response object - -The Response Object -------------------- - -The only requirement for a controller is to return a ``Response`` object. The -:class:`Symfony\\Component\\HttpFoundation\\Response` class is a PHP -abstraction around the HTTP response - the text-based message filled with HTTP -headers and content that's sent back to the client:: - - use Symfony\Component\HttpFoundation\Response; - - // create a simple Response with a 200 status code (the default) - $response = new Response('Hello '.$name, 200); - - // create a JSON-response with a 200 status code - $response = new Response(json_encode(array('name' => $name))); - $response->headers->set('Content-Type', 'application/json'); - -.. tip:: - - The ``headers`` property is a - :class:`Symfony\\Component\\HttpFoundation\\HeaderBag` object with several - useful methods for reading and mutating the ``Response`` headers. The - header names are normalized so that using ``Content-Type`` is equivalent - to ``content-type`` or even ``content_type``. - -.. tip:: - - There are also special classes to make certain kinds of responses easier: - - - For JSON, there is :class:`Symfony\\Component\\HttpFoundation\\JsonResponse`. - See :ref:`component-http-foundation-json-response`. - - For files, there is :class:`Symfony\\Component\\HttpFoundation\\BinaryFileResponse`. - See :ref:`component-http-foundation-serving-files`. - -.. index:: - single: Controller; Request object - -The Request Object ------------------- - -Besides the values of the routing placeholders, the controller also has access -to the ``Request`` object when extending the base ``Controller`` class:: - - $request = $this->getRequest(); - - $request->isXmlHttpRequest(); // is it an Ajax request? - - $request->getPreferredLanguage(array('en', 'fr')); - - $request->query->get('page'); // get a $_GET parameter - - $request->request->get('page'); // get a $_POST parameter - -Like the ``Response`` object, the request headers are stored in a ``HeaderBag`` -object and are easily accessible. - -Final Thoughts --------------- - -Whenever you create a page, you'll ultimately need to write some code that -contains the logic for that page. In Symfony, this is called a controller, -and it's a PHP function that can do anything it needs in order to return -the final ``Response`` object that will be returned to the user. - -To make life easier, you can choose to extend a base ``Controller`` class, -which contains shortcut methods for many common controller tasks. For example, -since you don't want to put HTML code in your controller, you can use -the ``render()`` method to render and return the content from a template. - -In other chapters, you'll see how the controller can be used to persist and -fetch objects from a database, process form submissions, handle caching and -more. - -Learn more from the Cookbook ----------------------------- - -* :doc:`/cookbook/controller/error_pages` -* :doc:`/cookbook/controller/service` diff --git a/book/doctrine.rst b/book/doctrine.rst deleted file mode 100644 index dc99a0b24a1..00000000000 --- a/book/doctrine.rst +++ /dev/null @@ -1,1572 +0,0 @@ -.. index:: - single: Doctrine - -Databases and Doctrine -====================== - -One of the most common and challenging tasks for any application -involves persisting and reading information to and from a database. Fortunately, -Symfony comes integrated with `Doctrine`_, a library whose sole goal is to -give you powerful tools to make this easy. In this chapter, you'll learn the -basic philosophy behind Doctrine and see how easy working with a database can -be. - -.. note:: - - Doctrine is totally decoupled from Symfony and using it is optional. - This chapter is all about the Doctrine ORM, which aims to let you map - objects to a relational database (such as *MySQL*, *PostgreSQL* or - *Microsoft SQL*). If you prefer to use raw database queries, this is - easy, and explained in the ":doc:`/cookbook/doctrine/dbal`" cookbook entry. - - You can also persist data to `MongoDB`_ using Doctrine ODM library. For - more information, read the ":doc:`/bundles/DoctrineMongoDBBundle/index`" - documentation. - -A Simple Example: A Product ---------------------------- - -The easiest way to understand how Doctrine works is to see it in action. -In this section, you'll configure your database, create a ``Product`` object, -persist it to the database and fetch it back out. - -.. sidebar:: Code along with the example - - If you want to follow along with the example in this chapter, create - an ``AcmeStoreBundle`` via: - - .. code-block:: bash - - $ php app/console generate:bundle --namespace=Acme/StoreBundle - -Configuring the Database -~~~~~~~~~~~~~~~~~~~~~~~~ - -Before you really begin, you'll need to configure your database connection -information. By convention, this information is usually configured in an -``app/config/parameters.yml`` file: - -.. code-block:: yaml - - # app/config/parameters.yml - parameters: - database_driver: pdo_mysql - database_host: localhost - database_name: test_project - database_user: root - database_password: password - - # ... - -.. note:: - - Defining the configuration via ``parameters.yml`` is just a convention. - The parameters defined in that file are referenced by the main configuration - file when setting up Doctrine: - - .. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - doctrine: - dbal: - driver: "%database_driver%" - host: "%database_host%" - dbname: "%database_name%" - user: "%database_user%" - password: "%database_password%" - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $configuration->loadFromExtension('doctrine', array( - 'dbal' => array( - 'driver' => '%database_driver%', - 'host' => '%database_host%', - 'dbname' => '%database_name%', - 'user' => '%database_user%', - 'password' => '%database_password%', - ), - )); - - By separating the database information into a separate file, you can - easily keep different versions of the file on each server. You can also - easily store database configuration (or any sensitive information) outside - of your project, like inside your Apache configuration, for example. For - more information, see :doc:`/cookbook/configuration/external_parameters`. - -Now that Doctrine knows about your database, you can have it create the database -for you: - -.. code-block:: bash - - $ php app/console doctrine:database:create - -.. sidebar:: Setting Up The Database to be UTF8 - - One mistake even seasoned developers make when starting a Symfony2 project - is forgetting to setup default charset and collation on their database, - ending up with latin type collations, which are default for most databases. - They might even remember to do it the very first time, but forget that - it's all gone after running a relatively common command during development: - - .. code-block:: bash - - $ php app/console doctrine:database:drop --force - $ php app/console doctrine:database:create - - There's no way to configure these defaults inside Doctrine, as it tries to be - as agnostic as possible in terms of environment configuration. One way to solve - this problem is to configure server-level defaults. - - Setting UTF8 defaults for MySQL is as simple as adding a few lines to - your configuration file (typically ``my.cnf``): - - .. code-block:: ini - - [mysqld] - collation-server = utf8_general_ci - character-set-server = utf8 - -.. note:: - - If you want to use SQLite as your database, you need to set the path - where your database file should be stored: - - .. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - doctrine: - dbal: - driver: pdo_sqlite - path: "%kernel.root_dir%/sqlite.db" - charset: UTF8 - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('doctrine', array( - 'dbal' => array( - 'driver' => 'pdo_sqlite', - 'path' => '%kernel.root_dir%/sqlite.db', - 'charset' => 'UTF-8', - ), - )); - -Creating an Entity Class -~~~~~~~~~~~~~~~~~~~~~~~~ - -Suppose you're building an application where products need to be displayed. -Without even thinking about Doctrine or databases, you already know that -you need a ``Product`` object to represent those products. Create this class -inside the ``Entity`` directory of your ``AcmeStoreBundle``:: - - // src/Acme/StoreBundle/Entity/Product.php - namespace Acme\StoreBundle\Entity; - - class Product - { - protected $name; - - protected $price; - - protected $description; - } - -The class - often called an "entity", meaning *a basic class that holds data* - -is simple and helps fulfill the business requirement of needing products -in your application. This class can't be persisted to a database yet - it's -just a simple PHP class. - -.. tip:: - - Once you learn the concepts behind Doctrine, you can have Doctrine create - simple entity classes for you: - - .. code-block:: bash - - $ php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Product" --fields="name:string(255) price:float description:text" - -.. index:: - single: Doctrine; Adding mapping metadata - -.. _book-doctrine-adding-mapping: - -Add Mapping Information -~~~~~~~~~~~~~~~~~~~~~~~ - -Doctrine allows you to work with databases in a much more interesting way -than just fetching rows of a column-based table into an array. Instead, Doctrine -allows you to persist entire *objects* to the database and fetch entire objects -out of the database. This works by mapping a PHP class to a database table, -and the properties of that PHP class to columns on the table: - -.. image:: /images/book/doctrine_image_1.png - :align: center - -For Doctrine to be able to do this, you just have to create "metadata", or -configuration that tells Doctrine exactly how the ``Product`` class and its -properties should be *mapped* to the database. This metadata can be specified -in a number of different formats including YAML, XML or directly inside the -``Product`` class via annotations: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Acme/StoreBundle/Entity/Product.php - namespace Acme\StoreBundle\Entity; - - use Doctrine\ORM\Mapping as ORM; - - /** - * @ORM\Entity - * @ORM\Table(name="product") - */ - class Product - { - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue(strategy="AUTO") - */ - protected $id; - - /** - * @ORM\Column(type="string", length=100) - */ - protected $name; - - /** - * @ORM\Column(type="decimal", scale=2) - */ - protected $price; - - /** - * @ORM\Column(type="text") - */ - protected $description; - } - - .. code-block:: yaml - - # src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml - Acme\StoreBundle\Entity\Product: - type: entity - table: product - id: - id: - type: integer - generator: { strategy: AUTO } - fields: - name: - type: string - length: 100 - price: - type: decimal - scale: 2 - description: - type: text - - .. code-block:: xml - - - - - - - - - - - - - - -.. note:: - - A bundle can accept only one metadata definition format. For example, it's - not possible to mix YAML metadata definitions with annotated PHP entity - class definitions. - -.. tip:: - - The table name is optional and if omitted, will be determined automatically - based on the name of the entity class. - -Doctrine allows you to choose from a wide variety of different field types, -each with their own options. For information on the available field types, -see the :ref:`book-doctrine-field-types` section. - -.. seealso:: - - You can also check out Doctrine's `Basic Mapping Documentation`_ for - all details about mapping information. If you use annotations, you'll - need to prepend all annotations with ``ORM\`` (e.g. ``ORM\Column(..)``), - which is not shown in Doctrine's documentation. You'll also need to include - the ``use Doctrine\ORM\Mapping as ORM;`` statement, which *imports* the - ``ORM`` annotations prefix. - -.. caution:: - - Be careful that your class name and properties aren't mapped to a protected - SQL keyword (such as ``group`` or ``user``). For example, if your entity - class name is ``Group``, then, by default, your table name will be ``group``, - which will cause an SQL error in some engines. See Doctrine's - `Reserved SQL keywords documentation`_ on how to properly escape these - names. Alternatively, if you're free to choose your database schema, - simply map to a different table name or column name. See Doctrine's - `Persistent classes`_ and `Property Mapping`_ documentation. - -.. note:: - - When using another library or program (ie. Doxygen) that uses annotations, - you should place the ``@IgnoreAnnotation`` annotation on the class to - indicate which annotations Symfony should ignore. - - For example, to prevent the ``@fn`` annotation from throwing an exception, - add the following:: - - /** - * @IgnoreAnnotation("fn") - */ - class Product - // ... - -Generating Getters and Setters -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Even though Doctrine now knows how to persist a ``Product`` object to the -database, the class itself isn't really useful yet. Since ``Product`` is just -a regular PHP class, you need to create getter and setter methods (e.g. ``getName()``, -``setName()``) in order to access its properties (since the properties are -``protected``). Fortunately, Doctrine can do this for you by running: - -.. code-block:: bash - - $ php app/console doctrine:generate:entities Acme/StoreBundle/Entity/Product - -This command makes sure that all of the getters and setters are generated -for the ``Product`` class. This is a safe command - you can run it over and -over again: it only generates getters and setters that don't exist (i.e. it -doesn't replace your existing methods). - -.. caution:: - - Keep in mind that Doctrine's entity generator produces simple getters/setters. - You should check generated entities and adjust getter/setter logic to your own - needs. - -.. sidebar:: More about ``doctrine:generate:entities`` - - With the ``doctrine:generate:entities`` command you can: - - * generate getters and setters; - - * generate repository classes configured with the - ``@ORM\Entity(repositoryClass="...")`` annotation; - - * generate the appropriate constructor for 1:n and n:m relations. - - The ``doctrine:generate:entities`` command saves a backup of the original - ``Product.php`` named ``Product.php~``. In some cases, the presence of - this file can cause a "Cannot redeclare class" error. It can be safely - removed. - - Note that you don't *need* to use this command. Doctrine doesn't rely - on code generation. Like with normal PHP classes, you just need to make - sure that your protected/private properties have getter and setter methods. - Since this is a common thing to do when using Doctrine, this command - was created. - -You can also generate all known entities (i.e. any PHP class with Doctrine -mapping information) of a bundle or an entire namespace: - -.. code-block:: bash - - $ php app/console doctrine:generate:entities AcmeStoreBundle - $ php app/console doctrine:generate:entities Acme - -.. note:: - - Doctrine doesn't care whether your properties are ``protected`` or ``private``, - or whether or not you have a getter or setter function for a property. - The getters and setters are generated here only because you'll need them - to interact with your PHP object. - -Creating the Database Tables/Schema -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You now have a usable ``Product`` class with mapping information so that -Doctrine knows exactly how to persist it. Of course, you don't yet have the -corresponding ``product`` table in your database. Fortunately, Doctrine can -automatically create all the database tables needed for every known entity -in your application. To do this, run: - -.. code-block:: bash - - $ php app/console doctrine:schema:update --force - -.. tip:: - - Actually, this command is incredibly powerful. It compares what - your database *should* look like (based on the mapping information of - your entities) with how it *actually* looks, and generates the SQL statements - needed to *update* the database to where it should be. In other words, if you add - a new property with mapping metadata to ``Product`` and run this task - again, it will generate the "alter table" statement needed to add that - new column to the existing ``product`` table. - - An even better way to take advantage of this functionality is via - :doc:`migrations`, which allow you to - generate these SQL statements and store them in migration classes that - can be run systematically on your production server in order to track - and migrate your database schema safely and reliably. - -Your database now has a fully-functional ``product`` table with columns that -match the metadata you've specified. - -Persisting Objects to the Database -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Now that you have a mapped ``Product`` entity and corresponding ``product`` -table, you're ready to persist data to the database. From inside a controller, -this is pretty easy. Add the following method to the ``DefaultController`` -of the bundle: - -.. code-block:: php - :linenos: - - // src/Acme/StoreBundle/Controller/DefaultController.php - - // ... - use Acme\StoreBundle\Entity\Product; - use Symfony\Component\HttpFoundation\Response; - - public function createAction() - { - $product = new Product(); - $product->setName('A Foo Bar'); - $product->setPrice('19.99'); - $product->setDescription('Lorem ipsum dolor'); - - $em = $this->getDoctrine()->getManager(); - $em->persist($product); - $em->flush(); - - return new Response('Created product id '.$product->getId()); - } - -.. note:: - - If you're following along with this example, you'll need to create a - route that points to this action to see it work. - -Take a look at the previous example in more detail: - -* **lines 9-12** In this section, you instantiate and work with the ``$product`` - object like any other, normal PHP object. - -* **line 14** This line fetches Doctrine's *entity manager* object, which is - responsible for handling the process of persisting and fetching objects - to and from the database. - -* **line 15** The ``persist()`` method tells Doctrine to "manage" the ``$product`` - object. This does not actually cause a query to be made to the database (yet). - -* **line 16** When the ``flush()`` method is called, Doctrine looks through - all of the objects that it's managing to see if they need to be persisted - to the database. In this example, the ``$product`` object has not been - persisted yet, so the entity manager executes an ``INSERT`` query and a - row is created in the ``product`` table. - -.. note:: - - In fact, since Doctrine is aware of all your managed entities, when you - call the ``flush()`` method, it calculates an overall changeset and executes - the most efficient query/queries possible. For example, if you persist a - total of 100 ``Product`` objects and then subsequently call ``flush()``, - Doctrine will create a *single* prepared statement and re-use it for each - insert. This pattern is called *Unit of Work*, and it's used because it's - fast and efficient. - -When creating or updating objects, the workflow is always the same. In the -next section, you'll see how Doctrine is smart enough to automatically issue -an ``UPDATE`` query if the record already exists in the database. - -.. tip:: - - Doctrine provides a library that allows you to programmatically load testing - data into your project (i.e. "fixture data"). For information, see - :doc:`/bundles/DoctrineFixturesBundle/index`. - -Fetching Objects from the Database -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Fetching an object back out of the database is even easier. For example, -suppose you've configured a route to display a specific ``Product`` based -on its ``id`` value:: - - public function showAction($id) - { - $product = $this->getDoctrine() - ->getRepository('AcmeStoreBundle:Product') - ->find($id); - - if (!$product) { - throw $this->createNotFoundException( - 'No product found for id '.$id - ); - } - - // ... do something, like pass the $product object into a template - } - -.. tip:: - - You can achieve the equivalent of this without writing any code by using - the ``@ParamConverter`` shortcut. See the - :doc:`FrameworkExtraBundle documentation` - for more details. - -When you query for a particular type of object, you always use what's known -as its "repository". You can think of a repository as a PHP class whose only -job is to help you fetch entities of a certain class. You can access the -repository object for an entity class via:: - - $repository = $this->getDoctrine() - ->getRepository('AcmeStoreBundle:Product'); - -.. note:: - - The ``AcmeStoreBundle:Product`` string is a shortcut you can use anywhere - in Doctrine instead of the full class name of the entity (i.e. ``Acme\StoreBundle\Entity\Product``). - As long as your entity lives under the ``Entity`` namespace of your bundle, - this will work. - -Once you have your repository, you have access to all sorts of helpful methods:: - - // query by the primary key (usually "id") - $product = $repository->find($id); - - // dynamic method names to find based on a column value - $product = $repository->findOneById($id); - $product = $repository->findOneByName('foo'); - - // find *all* products - $products = $repository->findAll(); - - // find a group of products based on an arbitrary column value - $products = $repository->findByPrice(19.99); - -.. note:: - - Of course, you can also issue complex queries, which you'll learn more - about in the :ref:`book-doctrine-queries` section. - -You can also take advantage of the useful ``findBy`` and ``findOneBy`` methods -to easily fetch objects based on multiple conditions:: - - // query for one product matching be name and price - $product = $repository->findOneBy(array('name' => 'foo', 'price' => 19.99)); - - // query for all products matching the name, ordered by price - $products = $repository->findBy( - array('name' => 'foo'), - array('price' => 'ASC') - ); - -.. tip:: - - When you render any page, you can see how many queries were made in the - bottom right corner of the web debug toolbar. - - .. image:: /images/book/doctrine_web_debug_toolbar.png - :align: center - :scale: 50 - :width: 350 - - If you click the icon, the profiler will open, showing you the exact - queries that were made. - -Updating an Object -~~~~~~~~~~~~~~~~~~ - -Once you've fetched an object from Doctrine, updating it is easy. Suppose -you have a route that maps a product id to an update action in a controller:: - - public function updateAction($id) - { - $em = $this->getDoctrine()->getManager(); - $product = $em->getRepository('AcmeStoreBundle:Product')->find($id); - - if (!$product) { - throw $this->createNotFoundException( - 'No product found for id '.$id - ); - } - - $product->setName('New product name!'); - $em->flush(); - - return $this->redirect($this->generateUrl('homepage')); - } - -Updating an object involves just three steps: - -#. fetching the object from Doctrine; -#. modifying the object; -#. calling ``flush()`` on the entity manager - -Notice that calling ``$em->persist($product)`` isn't necessary. Recall that -this method simply tells Doctrine to manage or "watch" the ``$product`` object. -In this case, since you fetched the ``$product`` object from Doctrine, it's -already managed. - -Deleting an Object -~~~~~~~~~~~~~~~~~~ - -Deleting an object is very similar, but requires a call to the ``remove()`` -method of the entity manager:: - - $em->remove($product); - $em->flush(); - -As you might expect, the ``remove()`` method notifies Doctrine that you'd -like to remove the given entity from the database. The actual ``DELETE`` query, -however, isn't actually executed until the ``flush()`` method is called. - -.. _`book-doctrine-queries`: - -Querying for Objects --------------------- - -You've already seen how the repository object allows you to run basic queries -without any work:: - - $repository->find($id); - - $repository->findOneByName('Foo'); - -Of course, Doctrine also allows you to write more complex queries using the -Doctrine Query Language (DQL). DQL is similar to SQL except that you should -imagine that you're querying for one or more objects of an entity class (e.g. ``Product``) -instead of querying for rows on a table (e.g. ``product``). - -When querying in Doctrine, you have two options: writing pure Doctrine queries -or using Doctrine's Query Builder. - -Querying for Objects with DQL -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Imagine that you want to query for products, but only return products that -cost more than ``19.99``, ordered from cheapest to most expensive. From inside -a controller, do the following:: - - $em = $this->getDoctrine()->getManager(); - $query = $em->createQuery( - 'SELECT p FROM AcmeStoreBundle:Product p WHERE p.price > :price ORDER BY p.price ASC' - )->setParameter('price', '19.99'); - - $products = $query->getResult(); - -If you're comfortable with SQL, then DQL should feel very natural. The biggest -difference is that you need to think in terms of "objects" instead of rows -in a database. For this reason, you select *from* ``AcmeStoreBundle:Product`` -and then alias it as ``p``. - -The ``getResult()`` method returns an array of results. If you're querying -for just one object, you can use the ``getSingleResult()`` method instead:: - - $product = $query->getSingleResult(); - -.. caution:: - - The ``getSingleResult()`` method throws a ``Doctrine\ORM\NoResultException`` - exception if no results are returned and a ``Doctrine\ORM\NonUniqueResultException`` - if *more* than one result is returned. If you use this method, you may - need to wrap it in a try-catch block and ensure that only one result is - returned (if you're querying on something that could feasibly return - more than one result):: - - $query = $em->createQuery('SELECT ...') - ->setMaxResults(1); - - try { - $product = $query->getSingleResult(); - } catch (\Doctrine\Orm\NoResultException $e) { - $product = null; - } - // ... - -The DQL syntax is incredibly powerful, allowing you to easily join between -entities (the topic of :ref:`relations` will be -covered later), group, etc. For more information, see the official Doctrine -`Doctrine Query Language`_ documentation. - -.. sidebar:: Setting Parameters - - Take note of the ``setParameter()`` method. When working with Doctrine, - it's always a good idea to set any external values as "placeholders", - which was done in the above query: - - .. code-block:: text - - ... WHERE p.price > :price ... - - You can then set the value of the ``price`` placeholder by calling the - ``setParameter()`` method:: - - ->setParameter('price', '19.99') - - Using parameters instead of placing values directly in the query string - is done to prevent SQL injection attacks and should *always* be done. - If you're using multiple parameters, you can set their values at once - using the ``setParameters()`` method:: - - ->setParameters(array( - 'price' => '19.99', - 'name' => 'Foo', - )) - -Using Doctrine's Query Builder -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Instead of writing the queries directly, you can alternatively use Doctrine's -``QueryBuilder`` to do the same job using a nice, object-oriented interface. -If you use an IDE, you can also take advantage of auto-completion as you -type the method names. From inside a controller:: - - $repository = $this->getDoctrine() - ->getRepository('AcmeStoreBundle:Product'); - - $query = $repository->createQueryBuilder('p') - ->where('p.price > :price') - ->setParameter('price', '19.99') - ->orderBy('p.price', 'ASC') - ->getQuery(); - - $products = $query->getResult(); - -The ``QueryBuilder`` object contains every method necessary to build your -query. By calling the ``getQuery()`` method, the query builder returns a -normal ``Query`` object, which is the same object you built directly in the -previous section. - -For more information on Doctrine's Query Builder, consult Doctrine's -`Query Builder`_ documentation. - -Custom Repository Classes -~~~~~~~~~~~~~~~~~~~~~~~~~ - -In the previous sections, you began constructing and using more complex queries -from inside a controller. In order to isolate, test and reuse these queries, -it's a good idea to create a custom repository class for your entity and -add methods with your query logic there. - -To do this, add the name of the repository class to your mapping definition. - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Acme/StoreBundle/Entity/Product.php - namespace Acme\StoreBundle\Entity; - - use Doctrine\ORM\Mapping as ORM; - - /** - * @ORM\Entity(repositoryClass="Acme\StoreBundle\Entity\ProductRepository") - */ - class Product - { - //... - } - - .. code-block:: yaml - - # src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml - Acme\StoreBundle\Entity\Product: - type: entity - repositoryClass: Acme\StoreBundle\Entity\ProductRepository - # ... - - .. code-block:: xml - - - - - - - - - - - -Doctrine can generate the repository class for you by running the same command -used earlier to generate the missing getter and setter methods: - -.. code-block:: bash - - $ php app/console doctrine:generate:entities Acme - -Next, add a new method - ``findAllOrderedByName()`` - to the newly generated -repository class. This method will query for all of the ``Product`` entities, -ordered alphabetically. - -.. code-block:: php - - // src/Acme/StoreBundle/Entity/ProductRepository.php - namespace Acme\StoreBundle\Entity; - - use Doctrine\ORM\EntityRepository; - - class ProductRepository extends EntityRepository - { - public function findAllOrderedByName() - { - return $this->getEntityManager() - ->createQuery('SELECT p FROM AcmeStoreBundle:Product p ORDER BY p.name ASC') - ->getResult(); - } - } - -.. tip:: - - The entity manager can be accessed via ``$this->getEntityManager()`` - from inside the repository. - -You can use this new method just like the default finder methods of the repository:: - - $em = $this->getDoctrine()->getManager(); - $products = $em->getRepository('AcmeStoreBundle:Product') - ->findAllOrderedByName(); - -.. note:: - - When using a custom repository class, you still have access to the default - finder methods such as ``find()`` and ``findAll()``. - -.. _`book-doctrine-relations`: - -Entity Relationships/Associations ---------------------------------- - -Suppose that the products in your application all belong to exactly one "category". -In this case, you'll need a ``Category`` object and a way to relate a ``Product`` -object to a ``Category`` object. Start by creating the ``Category`` entity. -Since you know that you'll eventually need to persist the class through Doctrine, -you can let Doctrine create the class for you. - -.. code-block:: bash - - $ php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Category" --fields="name:string(255)" - -This task generates the ``Category`` entity for you, with an ``id`` field, -a ``name`` field and the associated getter and setter functions. - -Relationship Mapping Metadata -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To relate the ``Category`` and ``Product`` entities, start by creating a -``products`` property on the ``Category`` class: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Acme/StoreBundle/Entity/Category.php - - // ... - use Doctrine\Common\Collections\ArrayCollection; - - class Category - { - // ... - - /** - * @ORM\OneToMany(targetEntity="Product", mappedBy="category") - */ - protected $products; - - public function __construct() - { - $this->products = new ArrayCollection(); - } - } - - .. code-block:: yaml - - # src/Acme/StoreBundle/Resources/config/doctrine/Category.orm.yml - Acme\StoreBundle\Entity\Category: - type: entity - # ... - oneToMany: - products: - targetEntity: Product - mappedBy: category - # don't forget to init the collection in entity __construct() method - - .. code-block:: xml - - - - - - - - - - - - -First, since a ``Category`` object will relate to many ``Product`` objects, -a ``products`` array property is added to hold those ``Product`` objects. -Again, this isn't done because Doctrine needs it, but instead because it -makes sense in the application for each ``Category`` to hold an array of -``Product`` objects. - -.. note:: - - The code in the ``__construct()`` method is important because Doctrine - requires the ``$products`` property to be an ``ArrayCollection`` object. - This object looks and acts almost *exactly* like an array, but has some - added flexibility. If this makes you uncomfortable, don't worry. Just - imagine that it's an ``array`` and you'll be in good shape. - -.. tip:: - - The targetEntity value in the decorator used above can reference any entity - with a valid namespace, not just entities defined in the same class. To - relate to an entity defined in a different class or bundle, enter a full - namespace as the targetEntity. - -Next, since each ``Product`` class can relate to exactly one ``Category`` -object, you'll want to add a ``$category`` property to the ``Product`` class: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Acme/StoreBundle/Entity/Product.php - - // ... - class Product - { - // ... - - /** - * @ORM\ManyToOne(targetEntity="Category", inversedBy="products") - * @ORM\JoinColumn(name="category_id", referencedColumnName="id") - */ - protected $category; - } - - .. code-block:: yaml - - # src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml - Acme\StoreBundle\Entity\Product: - type: entity - # ... - manyToOne: - category: - targetEntity: Category - inversedBy: products - joinColumn: - name: category_id - referencedColumnName: id - - .. code-block:: xml - - - - - - - - - - - - -Finally, now that you've added a new property to both the ``Category`` and -``Product`` classes, tell Doctrine to generate the missing getter and setter -methods for you: - -.. code-block:: bash - - $ php app/console doctrine:generate:entities Acme - -Ignore the Doctrine metadata for a moment. You now have two classes - ``Category`` -and ``Product`` with a natural one-to-many relationship. The ``Category`` -class holds an array of ``Product`` objects and the ``Product`` object can -hold one ``Category`` object. In other words - you've built your classes -in a way that makes sense for your needs. The fact that the data needs to -be persisted to a database is always secondary. - -Now, look at the metadata above the ``$category`` property on the ``Product`` -class. The information here tells doctrine that the related class is ``Category`` -and that it should store the ``id`` of the category record on a ``category_id`` -field that lives on the ``product`` table. In other words, the related ``Category`` -object will be stored on the ``$category`` property, but behind the scenes, -Doctrine will persist this relationship by storing the category's id value -on a ``category_id`` column of the ``product`` table. - -.. image:: /images/book/doctrine_image_2.png - :align: center - -The metadata above the ``$products`` property of the ``Category`` object -is less important, and simply tells Doctrine to look at the ``Product.category`` -property to figure out how the relationship is mapped. - -Before you continue, be sure to tell Doctrine to add the new ``category`` -table, and ``product.category_id`` column, and new foreign key: - -.. code-block:: bash - - $ php app/console doctrine:schema:update --force - -.. note:: - - This task should only be really used during development. For a more robust - method of systematically updating your production database, read about - :doc:`Doctrine migrations`. - -Saving Related Entities -~~~~~~~~~~~~~~~~~~~~~~~ - -Now you can see this new code in action! Imagine you're inside a controller:: - - // ... - - use Acme\StoreBundle\Entity\Category; - use Acme\StoreBundle\Entity\Product; - use Symfony\Component\HttpFoundation\Response; - - class DefaultController extends Controller - { - public function createProductAction() - { - $category = new Category(); - $category->setName('Main Products'); - - $product = new Product(); - $product->setName('Foo'); - $product->setPrice(19.99); - // relate this product to the category - $product->setCategory($category); - - $em = $this->getDoctrine()->getManager(); - $em->persist($category); - $em->persist($product); - $em->flush(); - - return new Response( - 'Created product id: '.$product->getId().' and category id: '.$category->getId() - ); - } - } - -Now, a single row is added to both the ``category`` and ``product`` tables. -The ``product.category_id`` column for the new product is set to whatever -the ``id`` is of the new category. Doctrine manages the persistence of this -relationship for you. - -Fetching Related Objects -~~~~~~~~~~~~~~~~~~~~~~~~ - -When you need to fetch associated objects, your workflow looks just like it -did before. First, fetch a ``$product`` object and then access its related -``Category``:: - - public function showAction($id) - { - $product = $this->getDoctrine() - ->getRepository('AcmeStoreBundle:Product') - ->find($id); - - $categoryName = $product->getCategory()->getName(); - - // ... - } - -In this example, you first query for a ``Product`` object based on the product's -``id``. This issues a query for *just* the product data and hydrates the -``$product`` object with that data. Later, when you call ``$product->getCategory()->getName()``, -Doctrine silently makes a second query to find the ``Category`` that's related -to this ``Product``. It prepares the ``$category`` object and returns it to -you. - -.. image:: /images/book/doctrine_image_3.png - :align: center - -What's important is the fact that you have easy access to the product's related -category, but the category data isn't actually retrieved until you ask for -the category (i.e. it's "lazily loaded"). - -You can also query in the other direction:: - - public function showProductAction($id) - { - $category = $this->getDoctrine() - ->getRepository('AcmeStoreBundle:Category') - ->find($id); - - $products = $category->getProducts(); - - // ... - } - -In this case, the same things occurs: you first query out for a single ``Category`` -object, and then Doctrine makes a second query to retrieve the related ``Product`` -objects, but only once/if you ask for them (i.e. when you call ``->getProducts()``). -The ``$products`` variable is an array of all ``Product`` objects that relate -to the given ``Category`` object via their ``category_id`` value. - -.. sidebar:: Relationships and Proxy Classes - - This "lazy loading" is possible because, when necessary, Doctrine returns - a "proxy" object in place of the true object. Look again at the above - example:: - - $product = $this->getDoctrine() - ->getRepository('AcmeStoreBundle:Product') - ->find($id); - - $category = $product->getCategory(); - - // prints "Proxies\AcmeStoreBundleEntityCategoryProxy" - echo get_class($category); - - This proxy object extends the true ``Category`` object, and looks and - acts exactly like it. The difference is that, by using a proxy object, - Doctrine can delay querying for the real ``Category`` data until you - actually need that data (e.g. until you call ``$category->getName()``). - - The proxy classes are generated by Doctrine and stored in the cache directory. - And though you'll probably never even notice that your ``$category`` - object is actually a proxy object, it's important to keep in mind. - - In the next section, when you retrieve the product and category data - all at once (via a *join*), Doctrine will return the *true* ``Category`` - object, since nothing needs to be lazily loaded. - -Joining to Related Records -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In the above examples, two queries were made - one for the original object -(e.g. a ``Category``) and one for the related object(s) (e.g. the ``Product`` -objects). - -.. tip:: - - Remember that you can see all of the queries made during a request via - the web debug toolbar. - -Of course, if you know up front that you'll need to access both objects, you -can avoid the second query by issuing a join in the original query. Add the -following method to the ``ProductRepository`` class:: - - // src/Acme/StoreBundle/Entity/ProductRepository.php - public function findOneByIdJoinedToCategory($id) - { - $query = $this->getEntityManager() - ->createQuery(' - SELECT p, c FROM AcmeStoreBundle:Product p - JOIN p.category c - WHERE p.id = :id' - )->setParameter('id', $id); - - try { - return $query->getSingleResult(); - } catch (\Doctrine\ORM\NoResultException $e) { - return null; - } - } - -Now, you can use this method in your controller to query for a ``Product`` -object and its related ``Category`` with just one query:: - - public function showAction($id) - { - $product = $this->getDoctrine() - ->getRepository('AcmeStoreBundle:Product') - ->findOneByIdJoinedToCategory($id); - - $category = $product->getCategory(); - - // ... - } - -More Information on Associations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This section has been an introduction to one common type of entity relationship, -the one-to-many relationship. For more advanced details and examples of how -to use other types of relations (e.g. ``one-to-one``, ``many-to-many``), see -Doctrine's `Association Mapping Documentation`_. - -.. note:: - - If you're using annotations, you'll need to prepend all annotations with - ``ORM\`` (e.g. ``ORM\OneToMany``), which is not reflected in Doctrine's - documentation. You'll also need to include the ``use Doctrine\ORM\Mapping as ORM;`` - statement, which *imports* the ``ORM`` annotations prefix. - -Configuration -------------- - -Doctrine is highly configurable, though you probably won't ever need to worry -about most of its options. To find out more about configuring Doctrine, see -the Doctrine section of the :doc:`reference manual`. - -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 - - /** - * @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 ``created`` date column to -the current date, only when the entity is first persisted (i.e. inserted): - -.. configuration-block:: - - .. code-block:: php-annotations - - /** - * @ORM\PrePersist - */ - public function setCreatedValue() - { - $this->created = new \DateTime(); - } - - .. code-block:: yaml - - # src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml - Acme\StoreBundle\Entity\Product: - type: entity - # ... - lifecycleCallbacks: - prePersist: [setCreatedValue] - - .. code-block:: xml - - - - - - - - - - - - - - -.. note:: - - The above example assumes that you've created and mapped a ``created`` - property (not shown here). - -Now, right before the entity is first persisted, Doctrine will automatically -call this method and the ``created`` field will be set to the current date. - -This can be repeated for any of the other lifecycle events, which include: - -* ``preRemove`` -* ``postRemove`` -* ``prePersist`` -* ``postPersist`` -* ``preUpdate`` -* ``postUpdate`` -* ``postLoad`` -* ``loadClassMetadata`` - -For more information on what these lifecycle events mean and lifecycle callbacks -in general, see Doctrine's `Lifecycle Events documentation`_ - -.. sidebar:: Lifecycle Callbacks and Event Listeners - - Notice that the ``setCreatedValue()`` 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 perform logging or send - 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:`/cookbook/doctrine/event_listeners_subscribers`. - -Doctrine Extensions: Timestampable, Sluggable, etc. ---------------------------------------------------- - -Doctrine is quite flexible, and a number of third-party extensions are available -that allow you to easily perform repeated and common tasks on your entities. -These include thing such as *Sluggable*, *Timestampable*, *Loggable*, *Translatable*, -and *Tree*. - -For more information on how to find and use these extensions, see the cookbook -article about :doc:`using common Doctrine extensions`. - -.. _book-doctrine-field-types: - -Doctrine Field Types Reference ------------------------------- - -Doctrine comes with a large number of field types available. Each of these -maps a PHP data type to a specific column type in whatever database you're -using. The following types are supported in Doctrine: - -* **Strings** - - * ``string`` (used for shorter strings) - * ``text`` (used for larger strings) - -* **Numbers** - - * ``integer`` - * ``smallint`` - * ``bigint`` - * ``decimal`` - * ``float`` - -* **Dates and Times** (use a `DateTime`_ object for these fields in PHP) - - * ``date`` - * ``time`` - * ``datetime`` - -* **Other Types** - - * ``boolean`` - * ``object`` (serialized and stored in a ``CLOB`` field) - * ``array`` (serialized and stored in a ``CLOB`` field) - -For more information, see Doctrine's `Mapping Types documentation`_. - -Field Options -~~~~~~~~~~~~~ - -Each field can have a set of options applied to it. The available options -include ``type`` (defaults to ``string``), ``name``, ``length``, ``unique`` -and ``nullable``. Take a few examples: - -.. configuration-block:: - - .. code-block:: php-annotations - - /** - * A string field with length 255 that cannot be null - * (reflecting the default values for the "type", "length" - * and *nullable* options) - * - * @ORM\Column() - */ - protected $name; - - /** - * A string field of length 150 that persists to an "email_address" column - * and has a unique index. - * - * @ORM\Column(name="email_address", unique=true, length=150) - */ - protected $email; - - .. code-block:: yaml - - fields: - # A string field length 255 that cannot be null - # (reflecting the default values for the "length" and *nullable* options) - # type attribute is necessary in yaml definitions - name: - type: string - - # A string field of length 150 that persists to an "email_address" column - # and has a unique index. - email: - type: string - column: email_address - length: 150 - unique: true - - .. code-block:: xml - - - - - -.. note:: - - There are a few more options not listed here. For more details, see - Doctrine's `Property Mapping documentation`_ - -.. index:: - single: Doctrine; ORM console commands - single: CLI; Doctrine ORM - -Console Commands ----------------- - -The Doctrine2 ORM integration offers several console commands under the -``doctrine`` namespace. To view the command list you can run the console -without any arguments: - -.. code-block:: bash - - $ php app/console - -A list of available commands will print out, many of which start with the -``doctrine:`` prefix. You can find out more information about any of these -commands (or any Symfony command) by running the ``help`` command. For example, -to get details about the ``doctrine:database:create`` task, run: - -.. code-block:: bash - - $ php app/console help doctrine:database:create - -Some notable or interesting tasks include: - -* ``doctrine:ensure-production-settings`` - checks to see if the current - environment is configured efficiently for production. This should always - be run in the ``prod`` environment: - - .. code-block:: bash - - $ php app/console doctrine:ensure-production-settings --env=prod - -* ``doctrine:mapping:import`` - allows Doctrine to introspect an existing - database and create mapping information. For more information, see - :doc:`/cookbook/doctrine/reverse_engineering`. - -* ``doctrine:mapping:info`` - tells you all of the entities that Doctrine - is aware of and whether or not there are any basic errors with the mapping. - -* ``doctrine:query:dql`` and ``doctrine:query:sql`` - allow you to execute - DQL or SQL queries directly from the command line. - -.. note:: - - To be able to load data fixtures to your database, you will need to have - the ``DoctrineFixturesBundle`` bundle installed. To learn how to do it, - read the ":doc:`/bundles/DoctrineFixturesBundle/index`" entry of the - documentation. - -.. tip:: - - This page shows working with Doctrine within a controller. You may also - want to work with Doctrine elsewhere in your application. The - :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::getDoctrine` - method of the controller returns the ``doctrine`` service, you can work with - this in the same way elsewhere by injecting this into your own - services. See :doc:`/book/service_container` for more on creating - your own services. - -Summary -------- - -With Doctrine, you can focus on your objects and how they're useful in your -application and worry about database persistence second. This is because -Doctrine allows you to use any PHP object to hold your data and relies on -mapping metadata information to map an object's data to a particular database -table. - -And even though Doctrine revolves around a simple concept, it's incredibly -powerful, allowing you to create complex queries and subscribe to events -that allow you to take different actions as objects go through their persistence -lifecycle. - -For more information about Doctrine, see the *Doctrine* section of the -:doc:`cookbook`, which includes the following articles: - -* :doc:`/bundles/DoctrineFixturesBundle/index` -* :doc:`/cookbook/doctrine/common_extensions` - -.. _`Doctrine`: http://www.doctrine-project.org/ -.. _`MongoDB`: http://www.mongodb.org/ -.. _`Basic Mapping 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 -.. _`Association Mapping Documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html -.. _`DateTime`: http://php.net/manual/en/class.datetime.php -.. _`Mapping Types Documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#doctrine-mapping-types -.. _`Property Mapping documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#property-mapping -.. _`Lifecycle Events documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-events -.. _`Reserved SQL keywords documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#quoting-reserved-words -.. _`Persistent classes`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#persistent-classes -.. _`Property Mapping`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#property-mapping diff --git a/book/forms.rst b/book/forms.rst deleted file mode 100644 index 4cdd6ba5f5e..00000000000 --- a/book/forms.rst +++ /dev/null @@ -1,1673 +0,0 @@ -.. index:: - single: Forms - -Forms -===== - -Dealing with HTML forms is one of the most common - and challenging - tasks for -a web developer. Symfony2 integrates a Form component that makes dealing with -forms easy. In this chapter, you'll build a complex form from the ground-up, -learning the most important features of the form library along the way. - -.. note:: - - The Symfony form component is a standalone library that can be used outside - of Symfony2 projects. For more information, see the `Symfony2 Form Component`_ - on Github. - -.. index:: - single: Forms; Create a simple form - -Creating a Simple Form ----------------------- - -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:: - - // src/Acme/TaskBundle/Entity/Task.php - namespace Acme\TaskBundle\Entity; - - class Task - { - protected $task; - - protected $dueDate; - - public function getTask() - { - return $this->task; - } - public function setTask($task) - { - $this->task = $task; - } - - public function getDueDate() - { - return $this->dueDate; - } - public function setDueDate(\DateTime $dueDate = null) - { - $this->dueDate = $dueDate; - } - } - -.. note:: - - If you're coding along with this example, create the ``AcmeTaskBundle`` - first by running the following command (and accepting all of the default - options): - - .. code-block:: bash - - $ php app/console generate:bundle --namespace=Acme/TaskBundle - -This class is a "plain-old-PHP-object" because, so far, it has nothing -to do with Symfony or any other library. It's quite simply a normal PHP object -that directly solves a problem inside *your* application (i.e. the need to -represent a task in your application). Of course, by the end of this chapter, -you'll be able to submit data to a ``Task`` instance (via an HTML form), validate -its data, and persist it to the database. - -.. index:: - single: Forms; Create a form in a controller - -Building the Form -~~~~~~~~~~~~~~~~~ - -Now that you've created a ``Task`` class, the next step is to create and -render the actual HTML form. In Symfony2, 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:: - - // src/Acme/TaskBundle/Controller/DefaultController.php - namespace Acme\TaskBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Acme\TaskBundle\Entity\Task; - use Symfony\Component\HttpFoundation\Request; - - class DefaultController extends Controller - { - public function newAction(Request $request) - { - // create a task and give it some dummy data for this example - $task = new Task(); - $task->setTask('Write a blog post'); - $task->setDueDate(new \DateTime('tomorrow')); - - $form = $this->createFormBuilder($task) - ->add('task', 'text') - ->add('dueDate', 'date') - ->getForm(); - - return $this->render('AcmeTaskBundle:Default:new.html.twig', array( - 'form' => $form->createView(), - )); - } - } - -.. tip:: - - This example shows you how to build your form directly in the controller. - Later, in the ":ref:`book-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. - -Creating a form requires relatively little code because Symfony2 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. - -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. ``text``, ``date``), which, among -other things, determines which HTML form tag(s) is rendered for that field. - -Symfony2 comes with many built-in types that will be discussed shortly -(see :ref:`book-forms-type-reference`). - -.. index:: - single: Forms; Basic template rendering - -Rendering the Form -~~~~~~~~~~~~~~~~~~ - -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 form -helper functions: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #} - {{ form(form) }} - - .. code-block:: html+php - - - form($form) ?> - -.. image:: /images/book/form-simple.png - :align: center - -.. note:: - - This example assumes that you submit the form in a "POST" request and to - the same URL that it was displayed in. You will learn later how to - change the request method and the target URL of the form. - -That's it! By printing ``form(form)``, each field in the form is rendered, along -with a label and error message (if there is one). The ``form`` function also -surrounds everything in the necessary HTML ``form`` tag. As easy as this is, -it's not very flexible (yet). Usually, you'll want to render each form field -individually so you can control how the form looks. You'll learn how to do -that in the ":ref:`form-rendering-template`" section. - -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. - -.. 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. - ``getPublished()`` or ``getReminder()``). - - .. versionadded:: 2.1 - Support for "hasser" methods was added in Symfony 2.1. - -.. index:: - single: Forms; Handling form submission - -.. _book-form-handling-form-submissions: - -Handling Form Submissions -~~~~~~~~~~~~~~~~~~~~~~~~~ - -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. Add the following functionality to your -controller:: - - // ... - use Symfony\Component\HttpFoundation\Request; - - public function newAction(Request $request) - { - // just setup a fresh $task object (remove the dummy data) - $task = new Task(); - - $form = $this->createFormBuilder($task) - ->add('task', 'text') - ->add('dueDate', 'date') - ->getForm(); - - $form->handleRequest($request); - - if ($form->isValid()) { - // perform some action, such as saving the task to the database - - return $this->redirect($this->generateUrl('task_success')); - } - - // ... - } - -.. versionadded:: 2.3 - The :method:`Symfony\Component\Form\FormInterface::handleRequest` method was - added in Symfony 2.3. Previously, the ``$request`` was passed to the - ``submit`` method - a strategy which is deprecated and will be removed - in Symfony 3.0. For details on that method, see :ref:`cookbook-form-submit-request`. - -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 simply created and - rendered. :method:`Symfony\Component\Form\FormInterface::handleRequest` - recognizes that the form was not submitted and does nothing. - :method:`Symfony\Component\Form\FormInterface::isValid` returns ``false`` - if the form was not submitted. - -#. 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), - :method:`Symfony\Component\Form\FormInterface::isValid` returns ``false`` - again, so the form is rendered together with all validation errors; - - .. note:: - - You can use the method :method:`Symfony\Component\Form\FormInterface::isSubmitted` - to check whether a form was submitted, regardless of whether or not the - submitted data is actually valid. - -#. When the user submits the form with valid data, the submitted data is again - 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). - - .. note:: - - Redirecting a user after a successful form submission prevents the user - from being able to hit "refresh" and re-post the data. - -.. index:: - single: Forms; Validation - -Form Validation ---------------- - -In the previous section, you learned how a form can be submitted with valid -or invalid data. In Symfony2, 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. - -Validation is done by adding a set of rules (called constraints) to a class. To -see this in action, add validation constraints so that the ``task`` field cannot -be empty and the ``dueDate`` field cannot be empty and must be a valid \DateTime -object. - -.. configuration-block:: - - .. code-block:: yaml - - # Acme/TaskBundle/Resources/config/validation.yml - Acme\TaskBundle\Entity\Task: - properties: - task: - - NotBlank: ~ - dueDate: - - NotBlank: ~ - - Type: \DateTime - - .. code-block:: php-annotations - - // Acme/TaskBundle/Entity/Task.php - use Symfony\Component\Validator\Constraints as Assert; - - class Task - { - /** - * @Assert\NotBlank() - */ - public $task; - - /** - * @Assert\NotBlank() - * @Assert\Type("\DateTime") - */ - protected $dueDate; - } - - .. code-block:: xml - - - - - - - - - \DateTime - - - - .. code-block:: php - - // Acme/TaskBundle/Entity/Task.php - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\NotBlank; - use Symfony\Component\Validator\Constraints\Type; - - class Task - { - // ... - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('task', new NotBlank()); - - $metadata->addPropertyConstraint('dueDate', new NotBlank()); - $metadata->addPropertyConstraint('dueDate', new Type('\DateTime')); - } - } - -That's it! If you re-submit the form with invalid data, you'll see the -corresponding errors printed out with the form. - -.. _book-forms-html5-validation-disable: - -.. sidebar:: HTML5 Validation - - As of 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. - - 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. - -Validation is a very powerful feature of Symfony2 and has its own -:doc:`dedicated chapter`. - -.. index:: - single: Forms; Validation groups - -.. _book-forms-validation-groups: - -Validation Groups -~~~~~~~~~~~~~~~~~ - -.. tip:: - - If you're not using :ref:`validation groups `, - then you can skip this section. - -If your object takes advantage of :ref:`validation groups `, -you'll need to specify which validation group(s) your form should use:: - - $form = $this->createFormBuilder($users, array( - 'validation_groups' => array('registration'), - ))->add(...); - -If you're creating :ref:`form classes` (a -good practice), then you'll need to add the following to the ``setDefaultOptions()`` -method:: - - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'validation_groups' => array('registration'), - )); - } - -In both of these cases, *only* the ``registration`` validation group will -be used to validate the underlying object. - -Groups based on Submitted Data -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.1 - The ability to specify a callback or Closure in ``validation_groups`` - is new to version 2.1 - -If you need some advanced logic to determine the validation groups (e.g. -based on submitted data), you can set the ``validation_groups`` option -to an array callback, or a ``Closure``:: - - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'validation_groups' => array('Acme\\AcmeBundle\\Entity\\Client', 'determineValidationGroups'), - )); - } - -This will call the static method ``determineValidationGroups()`` on the -``Client`` class after the form is submitted, but before validation is executed. -The Form object is passed as an argument to that method (see next example). -You can also define whole logic inline by using a Closure:: - - use Symfony\Component\Form\FormInterface; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'validation_groups' => function(FormInterface $form) { - $data = $form->getData(); - if (Entity\Client::TYPE_PERSON == $data->getType()) { - return array('person'); - } else { - return array('company'); - } - }, - )); - } - -.. index:: - single: Forms; Built-in field types - -.. _book-forms-type-reference: - -Built-in Field Types --------------------- - -Symfony comes standard with a large group of field types that cover all of -the common form fields and data types you'll encounter: - -.. include:: /reference/forms/types/map.rst.inc - -You can also create your own custom field types. This topic is covered in -the ":doc:`/cookbook/form/create_custom_field_type`" article of the cookbook. - -.. index:: - single: Forms; Field type options - -Field Type Options -~~~~~~~~~~~~~~~~~~ - -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:`date field` can be -configured to be rendered as a single text box (where the user would enter -the date as a string in the box):: - - ->add('dueDate', 'date', array('widget' => 'single_text')) - -.. image:: /images/book/form-simple2.png - :align: center - -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. - -.. sidebar:: 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 set the ``required`` - option on your field to ``false`` or :ref:`disable HTML5 validation`. - - 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. - - In other words, the ``required`` option is "nice", but true server-side - validation should *always* be used. - -.. sidebar:: The ``label`` option - - The label for the form field can be set using the ``label`` option, - which can be applied to any field:: - - ->add('dueDate', 'date', array( - 'widget' => 'single_text', - 'label' => 'Due Date', - )) - - The label for a field can also be set in the template rendering the - form, see below. - -.. index:: - single: Forms; Field type guessing - -.. _book-forms-field-guessing: - -Field Type Guessing -------------------- - -Now that you've added validation metadata to the ``Task`` class, Symfony -already knows a bit about your fields. If you allow it, Symfony can "guess" -the type of your field and set it up for you. In this example, Symfony can -guess from the validation rules that both the ``task`` field is a normal -``text`` field and the ``dueDate`` field is a ``date`` field:: - - public function newAction() - { - $task = new Task(); - - $form = $this->createFormBuilder($task) - ->add('task') - ->add('dueDate', null, array('widget' => 'single_text')) - ->getForm(); - } - -The "guessing" is activated when you omit the second argument to the ``add()`` -method (or if you pass ``null`` to it). If you pass an options array as the -third argument (done for ``dueDate`` above), these options are applied to -the guessed field. - -.. caution:: - - If your form uses a specific validation group, the field type guesser - will still consider *all* validation constraints when guessing your - field types (including constraints that are not part of the validation - group(s) being used). - -.. index:: - single: Forms; Field type guessing - -Field Type Options Guessing -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In addition to guessing the "type" for a field, Symfony can also try to guess -the correct values of a number of field options. - -.. tip:: - - When these options are set, the field will be rendered with special HTML - attributes that provide for HTML5 client-side validation. However, it - doesn't generate the equivalent server-side constraints (e.g. ``Assert\Length``). - And though you'll need to manually add your server-side validation, these - field type options can then be guessed from that information. - -* ``required``: The ``required`` option can be guessed based on the validation - rules (i.e. is the field ``NotBlank`` or ``NotNull``) or the Doctrine metadata - (i.e. is the field ``nullable``). This is very useful, as your client-side - validation will automatically match your validation rules. - -* ``max_length``: If the field is some sort of text field, then the ``max_length`` - option can be guessed from the validation constraints (if ``Length`` or - ``Range`` is used) or from the Doctrine metadata (via the field's length). - -.. note:: - - These field options are *only* guessed if you're using Symfony to guess - the field type (i.e. omit or pass ``null`` as the second argument to ``add()``). - -If you'd like to change one of the guessed values, you can override it by -passing the option in the options field array:: - - ->add('task', null, array('max_length' => 4)) - -.. index:: - single: Forms; Rendering in a template - -.. _form-rendering-template: - -Rendering a Form in a Template ------------------------------- - -So far, you've seen how an entire form can be rendered with just one line -of code. Of course, you'll usually need much more flexibility when rendering: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #} - {{ form_start(form) }} - {{ form_errors(form) }} - - {{ form_row(form.task) }} - {{ form_row(form.dueDate) }} - - - {{ form_end(form) }} - - .. code-block:: html+php - - - start($form) ?> - errors($form) ?> - - row($form['task']) ?> - row($form['dueDate']) ?> - - - end($form) ?> - -Take a look at each part: - -* ``form_start(form)`` - Renders the start tag of the form. - -* ``form_errors(form)`` - Renders any errors global to the whole form - (field-specific errors are displayed next to each field); - -* ``form_row(form.dueDate)`` - Renders the label, any errors, and the HTML - form widget for the given field (e.g. ``dueDate``) inside, by default, a - ``div`` element; - -* ``form_end()`` - Renders the end tag of the form and any fields that have not - yet been rendered. This is useful for rendering hidden fields and taking - advantage of the automatic :ref:`CSRF Protection`. - -The majority of the work is done by the ``form_row`` helper, which renders -the label, errors and HTML form widget of each field inside a ``div`` tag -by default. In the :ref:`form-theming` section, you'll learn how the ``form_row`` -output can be customized on many different levels. - -.. tip:: - - You can access the current data of your form via ``form.vars.value``: - - .. configuration-block:: - - .. code-block:: jinja - - {{ form.vars.value.task }} - - .. code-block:: html+php - - get('value')->getTask() ?> - -.. index:: - single: Forms; Rendering each field by hand - -Rendering each Field by Hand -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``form_row`` helper is great because you can very quickly render each -field of your form (and the markup used for the "row" can be customized as -well). But since life isn't always so simple, you can also render each field -entirely by hand. The end-product of the following is the same as when you -used the ``form_row`` helper: - -.. configuration-block:: - - .. code-block:: html+jinja - - {{ form_start(form) }} - {{ form_errors(form) }} - -
- {{ form_label(form.task) }} - {{ form_errors(form.task) }} - {{ form_widget(form.task) }} -
- -
- {{ form_label(form.dueDate) }} - {{ form_errors(form.dueDate) }} - {{ form_widget(form.dueDate) }} -
- - - - {{ form_end(form) }} - - .. code-block:: html+php - - start($form) ?> - - errors($form) ?> - -
- label($form['task']) ?> - errors($form['task']) ?> - widget($form['task']) ?> -
- -
- label($form['dueDate']) ?> - errors($form['dueDate']) ?> - widget($form['dueDate']) ?> -
- - - - end($form) ?> - -If the auto-generated label for a field isn't quite right, you can explicitly -specify it: - -.. configuration-block:: - - .. code-block:: html+jinja - - {{ form_label(form.task, 'Task Description') }} - - .. code-block:: html+php - - label($form['task'], 'Task Description') ?> - -Some field types have additional rendering options that can be passed -to the widget. These options are documented with each type, but one common -options is ``attr``, which allows you to modify attributes on the form element. -The following would add the ``task_field`` class to the rendered input text -field: - -.. configuration-block:: - - .. code-block:: html+jinja - - {{ form_widget(form.task, {'attr': {'class': 'task_field'}}) }} - - .. code-block:: html+php - - widget($form['task'], array( - 'attr' => array('class' => 'task_field'), - )) ?> - -If you need to render form fields "by hand" then you can access individual -values for fields such as the ``id``, ``name`` and ``label``. For example -to get the ``id``: - -.. configuration-block:: - - .. code-block:: html+jinja - - {{ form.task.vars.id }} - - .. code-block:: html+php - - get('id') ?> - -To get the value used for the form field's name attribute you need to use -the ``full_name`` value: - -.. configuration-block:: - - .. code-block:: html+jinja - - {{ form.task.vars.full_name }} - - .. code-block:: html+php - - get('full_name') ?> - -Twig Template Function Reference -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you're using Twig, a full reference of the form rendering functions is -available in the :doc:`reference manual`. -Read this to know everything about the helpers available and the options -that can be used with each. - -.. index:: - single: Forms; Changing the action and method - -.. _book-forms-changing-action-and-method: - -Changing the Action and Method of a Form ----------------------------------------- - -So far, the ``form_start()`` helper has been used to render the form's start -tag and we assumed that each form is submitted to the same URL in a POST request. -Sometimes you want to change these parameters. You can do so in a few different -ways. If you build your form in the controller, you can use ``setAction()`` and -``setMethod()``:: - - $form = $this->createFormBuilder($task) - ->setAction($this->generateUrl('target_route')) - ->setMethod('GET') - ->add('task', 'text') - ->add('dueDate', 'date') - ->getForm(); - -.. note:: - - This example assumes that you've created a route called ``target_route`` - that points to the controller that processes the form. - -In :ref:`book-form-creating-form-classes` you will learn how to move the -form building code into separate classes. When using an external form class -in the controller, you can pass the action and method as form options:: - - $form = $this->createForm(new TaskType(), $task, array( - 'action' => $this->generateUrl('target_route'), - 'method' => 'GET', - )); - -Finally, you can override the action and method in the template by passing them -to the ``form()`` or the ``form_start()`` helper: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #} - {{ form(form, {'action': path('target_route'), 'method': 'GET'}) }} - - {{ form_start(form, {'action': path('target_route'), 'method': 'GET'}) }} - - .. code-block:: html+php - - - form($form, array( - 'action' => $view['router']->generate('target_route'), - 'method' => 'GET', - )) ?> - - start($form, array( - 'action' => $view['router']->generate('target_route'), - 'method' => 'GET', - )) ?> - -.. note:: - - If the form's method is not GET or POST, but PUT, PATCH or DELETE, Symfony2 - will insert a hidden field with the name "_method" that stores this method. - The form will be submitted in a normal POST request, but Symfony2's router - is capable of detecting the "_method" parameter and will interpret the - request as PUT, PATCH or DELETE request. Read the cookbook chapter - ":doc:`/cookbook/routing/method_parameters`" for more information. - -.. index:: - single: Forms; Creating form classes - -.. _book-form-creating-form-classes: - -Creating Form Classes ---------------------- - -As you've seen, a form can be created and used directly in a controller. -However, a better practice is to build the form in a separate, standalone PHP -class, which can then be reused anywhere in your application. Create a new class -that will house the logic for building the task form:: - - // src/Acme/TaskBundle/Form/Type/TaskType.php - namespace Acme\TaskBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - - class TaskType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('task'); - $builder->add('dueDate', null, array('widget' => 'single_text')); - } - - public function getName() - { - return 'task'; - } - } - -This new class contains all the directions needed to create the task form -(note that the ``getName()`` method should return a unique identifier for this -form "type"). It can be used to quickly build a form object in the controller:: - - // src/Acme/TaskBundle/Controller/DefaultController.php - - // add this new use statement at the top of the class - use Acme\TaskBundle\Form\Type\TaskType; - - public function newAction() - { - $task = ...; - $form = $this->createForm(new TaskType(), $task); - - // ... - } - -Placing the form logic into its own class means that the form can be easily -reused elsewhere in your project. This is the best way to create forms, but -the choice is ultimately up to you. - -.. _book-forms-data-class: - -.. sidebar:: Setting the ``data_class`` - - Every form needs to know the name of the class that holds the underlying - data (e.g. ``Acme\TaskBundle\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 embedding forms, this will no - longer be sufficient. 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:: - - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'data_class' => 'Acme\TaskBundle\Entity\Task', - )); - } - -.. tip:: - - When mapping forms to objects, all fields are mapped. Any fields on the - form that do not exist on the mapped object will cause an exception to - be thrown. - - In cases where you need extra fields in the form (for example: a "do you - agree with these terms" checkbox) that will not be mapped to the underlying - object, you need to set the ``mapped`` option to ``false``:: - - use Symfony\Component\Form\FormBuilderInterface; - - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('task'); - $builder->add('dueDate', null, array('mapped' => false)); - } - - Additionally, if there are any fields on the form that aren't included in - the submitted data, those fields will be explicitly set to ``null``. - - The field data can be accessed in a controller with:: - - $form->get('dueDate')->getData(); - -.. index:: - pair: Forms; Doctrine - -Forms and Doctrine ------------------- - -The goal of a form is to translate data from an object (e.g. ``Task``) to an -HTML form and then translate user-submitted data back to the original object. As -such, the topic of persisting the ``Task`` object to the database is entirely -unrelated to the topic of forms. But, if you've configured the ``Task`` class -to be persisted via Doctrine (i.e. you've added -:ref:`mapping metadata` for it), then persisting -it after a form submission can be done when the form is valid:: - - if ($form->isValid()) { - $em = $this->getDoctrine()->getManager(); - $em->persist($task); - $em->flush(); - - return $this->redirect($this->generateUrl('task_success')); - } - -If, for some reason, you don't have access to your original ``$task`` object, -you can fetch it from the form:: - - $task = $form->getData(); - -For more information, see the :doc:`Doctrine ORM chapter`. - -The key thing to understand is that when the form is submitted, the submitted -data is transferred to the underlying object immediately. If you want to -persist that data, you simply need to persist the object itself (which already -contains the submitted data). - -.. index:: - single: Forms; Embedded forms - -Embedded Forms --------------- - -Often, you'll want to build a form that will include fields from many different -objects. For example, a registration form may contain data belonging to -a ``User`` object as well as many ``Address`` objects. Fortunately, this -is easy and natural with the form component. - -Embedding a Single Object -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Suppose that each ``Task`` belongs to a simple ``Category`` object. Start, -of course, by creating the ``Category`` object:: - - // src/Acme/TaskBundle/Entity/Category.php - namespace Acme\TaskBundle\Entity; - - use Symfony\Component\Validator\Constraints as Assert; - - class Category - { - /** - * @Assert\NotBlank() - */ - public $name; - } - -Next, add a new ``category`` property to the ``Task`` class:: - - // ... - - class Task - { - // ... - - /** - * @Assert\Type(type="Acme\TaskBundle\Entity\Category") - */ - protected $category; - - // ... - - public function getCategory() - { - return $this->category; - } - - public function setCategory(Category $category = null) - { - $this->category = $category; - } - } - -Now that your application has been updated to reflect the new requirements, -create a form class so that a ``Category`` object can be modified by the user:: - - // src/Acme/TaskBundle/Form/Type/CategoryType.php - namespace Acme\TaskBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - class CategoryType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('name'); - } - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'data_class' => 'Acme\TaskBundle\Entity\Category', - )); - } - - public function getName() - { - return 'category'; - } - } - -The end goal is to allow the ``Category`` of a ``Task`` to be modified right -inside the task form itself. To accomplish this, add a ``category`` field -to the ``TaskType`` object whose type is an instance of the new ``CategoryType`` -class: - -.. code-block:: php - - use Symfony\Component\Form\FormBuilderInterface; - - public function buildForm(FormBuilderInterface $builder, array $options) - { - // ... - - $builder->add('category', new CategoryType()); - } - -The fields from ``CategoryType`` can now be rendered alongside those from -the ``TaskType`` class. To activate validation on CategoryType, add -the ``cascade_validation`` option to ``TaskType``:: - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'data_class' => 'Acme\TaskBundle\Entity\Task', - 'cascade_validation' => true, - )); - } - -Render the ``Category`` fields in the same way -as the original ``Task`` fields: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# ... #} - -

Category

-
- {{ form_row(form.category.name) }} -
- - {# ... #} - - .. code-block:: html+php - - - -

Category

-
- row($form['category']['name']) ?> -
- - - -When the user submits the form, the submitted data for the ``Category`` fields -are used to construct an instance of ``Category``, which is then set on the -``category`` field of the ``Task`` instance. - -The ``Category`` instance is accessible naturally via ``$task->getCategory()`` -and can be persisted to the database or used however you need. - -Embedding a Collection of Forms -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can also embed a collection of forms into one form (imagine a ``Category`` -form with many ``Product`` sub-forms). This is done by using the ``collection`` -field type. - -For more information see the ":doc:`/cookbook/form/form_collections`" cookbook -entry and the :doc:`collection` field type reference. - -.. index:: - single: Forms; Theming - single: Forms; Customizing fields - -.. _form-theming: - -Form Theming ------------- - -Every part of how a form is rendered can be customized. You're free to change -how each form "row" renders, change the markup used to render errors, or -even customize how a ``textarea`` tag should be rendered. Nothing is off-limits, -and different customizations can be used in different places. - -Symfony uses templates to render each and every part of a form, such as -``label`` tags, ``input`` tags, error messages and everything else. - -In Twig, each form "fragment" is represented by a Twig block. To customize -any part of how a form renders, you just need to override the appropriate block. - -In PHP, each form "fragment" is rendered via an individual template file. -To customize any part of how a form renders, you just need to override the -existing template by creating a new one. - -To understand how this works, customize the ``form_row`` fragment and -add a class attribute to the ``div`` element that surrounds each row. To -do this, create a new template file that will store the new markup: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/TaskBundle/Resources/views/Form/fields.html.twig #} - {% block form_row %} - {% spaceless %} -
- {{ form_label(form) }} - {{ form_errors(form) }} - {{ form_widget(form) }} -
- {% endspaceless %} - {% endblock form_row %} - - .. code-block:: html+php - - -
- label($form, $label) ?> - errors($form) ?> - widget($form, $parameters) ?> -
- -The ``form_row`` form fragment is used when rendering most fields via the -``form_row`` function. To tell the form component to use your new ``form_row`` -fragment defined above, add the following to the top of the template that -renders the form: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #} - {% form_theme form 'AcmeTaskBundle:Form:fields.html.twig' %} - - {% form_theme form 'AcmeTaskBundle:Form:fields.html.twig' 'AcmeTaskBundle:Form:fields2.html.twig' %} - - {{ form(form) }} - - .. code-block:: html+php - - - setTheme($form, array('AcmeTaskBundle:Form')) ?> - - setTheme($form, array('AcmeTaskBundle:Form', 'AcmeTaskBundle:Form')) ?> - - form($form) ?> - -The ``form_theme`` tag (in Twig) "imports" the fragments defined in the given -template and uses them when rendering the form. In other words, when the -``form_row`` function is called later in this template, it will use the ``form_row`` -block from your custom theme (instead of the default ``form_row`` block -that ships with Symfony). - -Your custom theme does not have to override all the blocks. When rendering a block -which is not overridden in your custom theme, the theming engine will fall back -to the global theme (defined at the bundle level). - -If several custom themes are provided they will be searched in the listed order -before falling back to the global theme. - -To customize any portion of a form, you just need to override the appropriate -fragment. Knowing exactly which block or file to override is the subject of -the next section. - -.. versionadded:: 2.1 - An alternate Twig syntax for ``form_theme`` has been introduced in 2.1. It accepts - any valid Twig expression (the most noticeable difference is using an array when - using multiple themes). - - .. code-block:: html+jinja - - {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #} - - {% form_theme form with 'AcmeTaskBundle:Form:fields.html.twig' %} - - {% form_theme form with ['AcmeTaskBundle:Form:fields.html.twig', 'AcmeTaskBundle:Form:fields2.html.twig'] %} - -For a more extensive discussion, see :doc:`/cookbook/form/form_customization`. - -.. index:: - single: Forms; Template fragment naming - -.. _form-template-blocks: - -Form Fragment Naming -~~~~~~~~~~~~~~~~~~~~ - -In Symfony, every part of a form that is rendered - HTML form elements, errors, -labels, etc - is defined in a base theme, which is a collection of blocks -in Twig and a collection of template files in PHP. - -In Twig, every block needed is defined in a single template file (`form_div_layout.html.twig`_) -that lives inside the `Twig Bridge`_. Inside this file, you can see every block -needed to render a form and every default field type. - -In PHP, the fragments are individual template files. By default they are located in -the `Resources/views/Form` directory of the framework bundle (`view on GitHub`_). - -Each fragment name follows the same basic pattern and is broken up into two pieces, -separated by a single underscore character (``_``). A few examples are: - -* ``form_row`` - used by ``form_row`` to render most fields; -* ``textarea_widget`` - used by ``form_widget`` to render a ``textarea`` field - type; -* ``form_errors`` - used by ``form_errors`` to render errors for a field; - -Each fragment follows the same basic pattern: ``type_part``. The ``type`` portion -corresponds to the field *type* being rendered (e.g. ``textarea``, ``checkbox``, -``date``, etc) whereas the ``part`` portion corresponds to *what* is being -rendered (e.g. ``label``, ``widget``, ``errors``, etc). By default, there -are 4 possible *parts* of a form that can be rendered: - -+-------------+--------------------------+---------------------------------------------------------+ -| ``label`` | (e.g. ``form_label``) | renders the field's label | -+-------------+--------------------------+---------------------------------------------------------+ -| ``widget`` | (e.g. ``form_widget``) | renders the field's HTML representation | -+-------------+--------------------------+---------------------------------------------------------+ -| ``errors`` | (e.g. ``form_errors``) | renders the field's errors | -+-------------+--------------------------+---------------------------------------------------------+ -| ``row`` | (e.g. ``form_row``) | renders the field's entire row (label, widget & errors) | -+-------------+--------------------------+---------------------------------------------------------+ - -.. note:: - - There are actually 2 other *parts* - ``rows`` and ``rest`` - - but you should rarely if ever need to worry about overriding them. - -By knowing the field type (e.g. ``textarea``) and which part you want to -customize (e.g. ``widget``), you can construct the fragment name that needs -to be overridden (e.g. ``textarea_widget``). - -.. index:: - single: Forms; Template fragment inheritance - -Template Fragment Inheritance -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In some cases, the fragment you want to customize will appear to be missing. -For example, there is no ``textarea_errors`` fragment in the default themes -provided with Symfony. So how are the errors for a textarea field rendered? - -The answer is: via the ``form_errors`` fragment. When Symfony renders the errors -for a textarea type, it looks first for a ``textarea_errors`` fragment before -falling back to the ``form_errors`` fragment. Each field type has a *parent* -type (the parent type of ``textarea`` is ``text``, its parent is ``form``), -and Symfony uses the fragment for the parent type if the base fragment doesn't -exist. - -So, to override the errors for *only* ``textarea`` fields, copy the -``form_errors`` fragment, rename it to ``textarea_errors`` and customize it. To -override the default error rendering for *all* fields, copy and customize the -``form_errors`` fragment directly. - -.. tip:: - - The "parent" type of each field type is available in the - :doc:`form type reference` for each field type. - -.. index:: - single: Forms; Global Theming - -Global Form Theming -~~~~~~~~~~~~~~~~~~~ - -In the above example, you used the ``form_theme`` helper (in Twig) to "import" -the custom form fragments into *just* that form. You can also tell Symfony -to import form customizations across your entire project. - -Twig -.... - -To automatically include the customized blocks from the ``fields.html.twig`` -template created earlier in *all* templates, modify your application configuration -file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - twig: - form: - resources: - - 'AcmeTaskBundle:Form:fields.html.twig' - # ... - - .. code-block:: xml - - - - - AcmeTaskBundle:Form:fields.html.twig - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('twig', array( - 'form' => array( - 'resources' => array( - 'AcmeTaskBundle:Form:fields.html.twig', - ), - ), - // ... - )); - -Any blocks inside the ``fields.html.twig`` template are now used globally -to define form output. - -.. sidebar:: Customizing Form Output all in a Single File with Twig - - In Twig, you can also customize a form block right inside the template - where that customization is needed: - - .. code-block:: html+jinja - - {% extends '::base.html.twig' %} - - {# import "_self" as the form theme #} - {% form_theme form _self %} - - {# make the form fragment customization #} - {% block form_row %} - {# custom field row output #} - {% endblock form_row %} - - {% block content %} - {# ... #} - - {{ form_row(form.task) }} - {% endblock %} - - The ``{% form_theme form _self %}`` tag allows form blocks to be customized - directly inside the template that will use those customizations. Use - this method to quickly make form output customizations that will only - ever be needed in a single template. - - .. caution:: - - This ``{% form_theme form _self %}`` functionality will *only* work - if your template extends another. If your template does not, you - must point ``form_theme`` to a separate template. - -PHP -... - -To automatically include the customized templates from the ``Acme/TaskBundle/Resources/views/Form`` -directory created earlier in *all* templates, modify your application configuration -file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - templating: - form: - resources: - - 'AcmeTaskBundle:Form' - # ... - - - .. code-block:: xml - - - - - - AcmeTaskBundle:Form - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - 'templating' => array( - 'form' => array( - 'resources' => array( - 'AcmeTaskBundle:Form', - ), - ), - ) - // ... - )); - -Any fragments inside the ``Acme/TaskBundle/Resources/views/Form`` directory -are now used globally to define form output. - -.. index:: - single: Forms; CSRF protection - -.. _forms-csrf: - -CSRF Protection ---------------- - -CSRF - or `Cross-site request forgery`_ - is a method by which a malicious -user attempts to make your legitimate users unknowingly submit data that -they don't intend to submit. Fortunately, CSRF attacks can be prevented by -using a CSRF token inside your forms. - -The good news is that, by default, Symfony embeds and validates CSRF tokens -automatically for you. This means that you can take advantage of the CSRF -protection without doing anything. In fact, every form in this chapter has -taken advantage of the CSRF protection! - -CSRF protection works by adding a hidden field to your form - called ``_token`` -by default - that contains a value that only you and your user knows. This -ensures that the user - not some other entity - is submitting the given data. -Symfony automatically validates the presence and accuracy of this token. - -The ``_token`` field is a hidden field and will be automatically rendered -if you include the ``form_end()`` function in your template, which ensures -that all un-rendered fields are output. - -The CSRF token can be customized on a form-by-form basis. For example:: - - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - class TaskType extends AbstractType - { - // ... - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'data_class' => 'Acme\TaskBundle\Entity\Task', - 'csrf_protection' => true, - 'csrf_field_name' => '_token', - // a unique key to help generate the secret token - 'intention' => 'task_item', - )); - } - - // ... - } - -To disable CSRF protection, set the ``csrf_protection`` option to false. -Customizations can also be made globally in your project. For more information, -see the :ref:`form configuration reference ` -section. - -.. note:: - - The ``intention`` option is optional but greatly enhances the security of - the generated token by making it different for each form. - -.. index:: - single: Forms; With no class - -Using a Form without a Class ----------------------------- - -In most cases, a form is tied to an object, and the fields of the form get -and store their data on the properties of that object. This is exactly what -you've seen so far in this chapter with the `Task` class. - -But sometimes, you may just want to use a form without a class, and get back -an array of the submitted data. This is actually really easy:: - - // make sure you've imported the Request namespace above the class - use Symfony\Component\HttpFoundation\Request; - // ... - - public function contactAction(Request $request) - { - $defaultData = array('message' => 'Type your message here'); - $form = $this->createFormBuilder($defaultData) - ->add('name', 'text') - ->add('email', 'email') - ->add('message', 'textarea') - ->getForm(); - - $form->handleRequest($request); - - if ($form->isValid()) { - // data is an array with "name", "email", and "message" keys - $data = $form->getData(); - } - - // ... render the form - } - -By default, a form actually assumes that you want to work with arrays of -data, instead of an object. There are exactly two ways that you can change -this behavior and tie the form to an object instead: - -#. Pass an object when creating the form (as the first argument to ``createFormBuilder`` - or the second argument to ``createForm``); - -#. Declare the ``data_class`` option on your form. - -If you *don't* do either of these, then the form will return the data as -an array. In this example, since ``$defaultData`` is not an object (and -no ``data_class`` option is set), ``$form->getData()`` ultimately returns -an array. - -.. tip:: - - You can also access POST values (in this case "name") directly through - the request object, like so:: - - $this->get('request')->request->get('name'); - - Be advised, however, that in most cases using the getData() method is - a better choice, since it returns the data (usually an object) after - it's been transformed by the form framework. - -Adding Validation -~~~~~~~~~~~~~~~~~ - -The only missing piece is validation. Usually, when you call ``$form->isValid()``, -the object is validated by reading the constraints that you applied to that -class. If your form is mapped to an object (i.e. you're using the ``data_class`` -option or passing an object to your form), this is almost always the approach -you want to use. See :doc:`/book/validation` for more details. - -.. _form-option-constraints: - -But if the form is not mapped to an object and you instead want to retrieve a -simple array of your submitted data, how can you add constraints to the data of -your form? - -The answer is to setup the constraints yourself, and attach them to the individual -fields. The overall approach is covered a bit more in the :ref:`validation chapter`, -but here's a short example: - -.. versionadded:: 2.1 - The ``constraints`` option, which accepts a single constraint or an array - of constraints (before 2.1, the option was called ``validation_constraint``, - and only accepted a single constraint) is new to Symfony 2.1. - -.. code-block:: php - - use Symfony\Component\Validator\Constraints\Length; - use Symfony\Component\Validator\Constraints\NotBlank; - - $builder - ->add('firstName', 'text', array( - 'constraints' => new Length(array('min' => 3)), - )) - ->add('lastName', 'text', array( - 'constraints' => array( - new NotBlank(), - new Length(array('min' => 3)), - ), - )) - ; - -.. tip:: - - If you are using Validation Groups, you need to either reference the - ``Default`` group when creating the form, or set the correct group on - the constraint you are adding. - -.. code-block:: php - - new NotBlank(array('groups' => array('create', 'update')) - - -Final Thoughts --------------- - -You now know all of the building blocks necessary to build complex and -functional forms for your application. When building forms, keep in mind that -the first goal of a form is to translate data from an object (``Task``) to an -HTML form so that the user can modify that data. The second goal of a form is to -take the data submitted by the user and to re-apply it to the object. - -There's still much more to learn about the powerful world of forms, such as -how to handle :doc:`file uploads with Doctrine -` or how to create a form where a dynamic -number of sub-forms can be added (e.g. a todo list where you can keep adding -more fields via Javascript before submitting). See the cookbook for these -topics. Also, be sure to lean on the -:doc:`field type reference documentation`, which -includes examples of how to use each field type and its options. - -Learn more from the Cookbook ----------------------------- - -* :doc:`/cookbook/doctrine/file_uploads` -* :doc:`File Field Reference ` -* :doc:`Creating Custom Field Types ` -* :doc:`/cookbook/form/form_customization` -* :doc:`/cookbook/form/dynamic_form_modification` -* :doc:`/cookbook/form/data_transformers` - -.. _`Symfony2 Form Component`: https://github.com/symfony/Form -.. _`DateTime`: http://php.net/manual/en/class.datetime.php -.. _`Twig Bridge`: https://github.com/symfony/symfony/tree/2.2/src/Symfony/Bridge/Twig -.. _`form_div_layout.html.twig`: https://github.com/symfony/symfony/blob/2.2/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig -.. _`Cross-site request forgery`: http://en.wikipedia.org/wiki/Cross-site_request_forgery -.. _`view on GitHub`: https://github.com/symfony/symfony/tree/2.2/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form diff --git a/book/from_flat_php_to_symfony2.rst b/book/from_flat_php_to_symfony2.rst deleted file mode 100644 index ff684736571..00000000000 --- a/book/from_flat_php_to_symfony2.rst +++ /dev/null @@ -1,763 +0,0 @@ -Symfony2 versus Flat PHP -======================== - -**Why is Symfony2 better than just opening up a file and writing flat PHP?** - -If you've never used a PHP framework, aren't familiar with the MVC philosophy, -or just wonder what all the *hype* is around Symfony2, this chapter is for -you. Instead of *telling* you that Symfony2 allows you to develop faster and -better software than with flat PHP, you'll see for yourself. - -In this chapter, you'll write a simple application in flat PHP, and then -refactor it to be more organized. You'll travel through time, seeing the -decisions behind why web development has evolved over the past several years -to where it is now. - -By the end, you'll see how Symfony2 can rescue you from mundane tasks and -let you take back control of your code. - -A simple Blog in flat PHP -------------------------- - -In this chapter, you'll build the token blog application using only flat PHP. -To begin, create a single page that displays blog entries that have been -persisted to the database. Writing in flat PHP is quick and dirty: - -.. code-block:: html+php - - - - - - - List of Posts - - -

List of Posts

- - - - - - -That's quick to write, fast to execute, and, as your app grows, impossible -to maintain. There are several problems that need to be addressed: - -* **No error-checking**: What if the connection to the database fails? - -* **Poor organization**: If the application grows, this single file will become - increasingly unmaintainable. Where should you put code to handle a form - submission? How can you validate data? Where should code go for sending - emails? - -* **Difficult to reuse code**: Since everything is in one file, there's no - way to reuse any part of the application for other "pages" of the blog. - -.. note:: - - Another problem not mentioned here is the fact that the database is - tied to MySQL. Though not covered here, Symfony2 fully integrates `Doctrine`_, - a library dedicated to database abstraction and mapping. - -Let's get to work on solving these problems and more. - -Isolating the Presentation -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The code can immediately gain from separating the application "logic" from -the code that prepares the HTML "presentation": - -.. code-block:: html+php - - - - - List of Posts - - -

List of Posts

- - - - -By convention, the file that contains all of the application logic - ``index.php`` - -is known as a "controller". The term :term:`controller` is a word you'll hear -a lot, regardless of the language or framework you use. It refers simply -to the area of *your* code that processes user input and prepares the response. - -In this case, the controller prepares data from the database and then includes -a template to present that data. With the controller isolated, you could -easily change *just* the template file if you needed to render the blog -entries in some other format (e.g. ``list.json.php`` for JSON format). - -Isolating the Application (Domain) Logic -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -So far the application contains only one page. But what if a second page -needed to use the same database connection, or even the same array of blog -posts? Refactor the code so that the core behavior and data-access functions -of the application are isolated in a new file called ``model.php``: - -.. code-block:: html+php - - - - - - <?php echo $title ?> - - - - - - -The template (``templates/list.php``) can now be simplified to "extend" -the layout: - -.. code-block:: html+php - - - - -

List of Posts

- - - - - -You've now introduced a methodology that allows for the reuse of the -layout. Unfortunately, to accomplish this, you're forced to use a few ugly -PHP functions (``ob_start()``, ``ob_get_clean()``) in the template. Symfony2 -uses a ``Templating`` component that allows this to be accomplished cleanly -and easily. You'll see it in action shortly. - -Adding a Blog "show" Page -------------------------- - -The blog "list" page has now been refactored so that the code is better-organized -and reusable. To prove it, add a blog "show" page, which displays an individual -blog post identified by an ``id`` query parameter. - -To begin, create a new function in the ``model.php`` file that retrieves -an individual blog result based on a given id:: - - // model.php - function get_post_by_id($id) - { - $link = open_database_connection(); - - $id = intval($id); - $query = 'SELECT date, title, body FROM post WHERE id = '.$id; - $result = mysql_query($query); - $row = mysql_fetch_assoc($result); - - close_database_connection($link); - - return $row; - } - -Next, create a new file called ``show.php`` - the controller for this new -page: - -.. code-block:: html+php - - - - -

- -
-
- -
- - - - -Creating the second page is now very easy and no code is duplicated. Still, -this page introduces even more lingering problems that a framework can solve -for you. For example, a missing or invalid ``id`` query parameter will cause -the page to crash. It would be better if this caused a 404 page to be rendered, -but this can't really be done easily yet. Worse, had you forgotten to clean -the ``id`` parameter via the ``intval()`` function, your -entire database would be at risk for an SQL injection attack. - -Another major problem is that each individual controller file must include -the ``model.php`` file. What if each controller file suddenly needed to include -an additional file or perform some other global task (e.g. enforce security)? -As it stands now, that code would need to be added to every controller file. -If you forget to include something in one file, hopefully it doesn't relate -to security... - -A "Front Controller" to the Rescue ----------------------------------- - -The solution is to use a :term:`front controller`: a single PHP file through -which *all* requests are processed. With a front controller, the URIs for the -application change slightly, but start to become more flexible: - -.. code-block:: text - - Without a front controller - /index.php => Blog post list page (index.php executed) - /show.php => Blog post show page (show.php executed) - - With index.php as the front controller - /index.php => Blog post list page (index.php executed) - /index.php/show => Blog post show page (index.php executed) - -.. tip:: - The ``index.php`` portion of the URI can be removed if using Apache - rewrite rules (or equivalent). In that case, the resulting URI of the - blog show page would be simply ``/show``. - -When using a front controller, a single PHP file (``index.php`` in this case) -renders *every* request. For the blog post show page, ``/index.php/show`` will -actually execute the ``index.php`` file, which is now responsible for routing -requests internally based on the full URI. As you'll see, a front controller -is a very powerful tool. - -Creating the Front Controller -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You're about to take a **big** step with the application. With one file handling -all requests, you can centralize things such as security handling, configuration -loading, and routing. In this application, ``index.php`` must now be smart -enough to render the blog post list page *or* the blog post show page based -on the requested URI: - -.. code-block:: html+php - -

Page Not Found

'; - } - -For organization, both controllers (formerly ``index.php`` and ``show.php``) -are now PHP functions and each has been moved into a separate file, ``controllers.php``: - -.. code-block:: php - - function list_action() - { - $posts = get_all_posts(); - require 'templates/list.php'; - } - - function show_action($id) - { - $post = get_post_by_id($id); - require 'templates/show.php'; - } - -As a front controller, ``index.php`` has taken on an entirely new role, one -that includes loading the core libraries and routing the application so that -one of the two controllers (the ``list_action()`` and ``show_action()`` -functions) is called. In reality, the front controller is beginning to look and -act a lot like Symfony2's mechanism for handling and routing requests. - -.. tip:: - - Another advantage of a front controller is flexible URLs. Notice that - the URL to the blog post show page could be changed from ``/show`` to ``/read`` - by changing code in only one location. Before, an entire file needed to - be renamed. In Symfony2, URLs are even more flexible. - -By now, the application has evolved from a single PHP file into a structure -that is organized and allows for code reuse. You should be happier, but far -from satisfied. For example, the "routing" system is fickle, and wouldn't -recognize that the list page (``/index.php``) should be accessible also via ``/`` -(if Apache rewrite rules were added). Also, instead of developing the blog, -a lot of time is being spent working on the "architecture" of the code (e.g. -routing, calling controllers, templates, etc.). More time will need to be -spent to handle form submissions, input validation, logging and security. -Why should you have to reinvent solutions to all these routine problems? - -Add a Touch of Symfony2 -~~~~~~~~~~~~~~~~~~~~~~~ - -Symfony2 to the rescue. Before actually using Symfony2, you need to download -it. This can be done by using Composer, which takes care of downloading the -correct version and all its dependencies and provides an autoloader. An -autoloader is a tool that makes it possible to start using PHP classes -without explicitly including the file containing the class. - -In your root directory, create a ``composer.json`` file with the following -content: - -.. code-block:: json - - { - "require": { - "symfony/symfony": "2.2.*" - }, - "autoload": { - "files": ["model.php","controllers.php"] - } - } - -Next, `download Composer`_ and then run the following command, which will download Symfony -into a vendor/ directory: - -.. code-block:: bash - - $ php composer.phar install - -Beside downloading your dependencies, Composer generates a ``vendor/autoload.php`` file, -which takes care of autoloading for all the files in the Symfony Framework as well as -the files mentioned in the autoload section of your ``composer.json``. - -Core to Symfony's philosophy is the idea that an application's main job is -to interpret each request and return a response. To this end, Symfony2 provides -both a :class:`Symfony\\Component\\HttpFoundation\\Request` and a -:class:`Symfony\\Component\\HttpFoundation\\Response` class. These classes are -object-oriented representations of the raw HTTP request being processed and -the HTTP response being returned. Use them to improve the blog: - -.. code-block:: html+php - - getPathInfo(); - if ('/' == $uri) { - $response = list_action(); - } elseif ('/show' == $uri && $request->query->has('id')) { - $response = show_action($request->query->get('id')); - } else { - $html = '

Page Not Found

'; - $response = new Response($html, 404); - } - - // echo the headers and send the response - $response->send(); - -The controllers are now responsible for returning a ``Response`` object. -To make this easier, you can add a new ``render_template()`` function, which, -incidentally, acts quite a bit like the Symfony2 templating engine: - -.. code-block:: php - - // controllers.php - use Symfony\Component\HttpFoundation\Response; - - function list_action() - { - $posts = get_all_posts(); - $html = render_template('templates/list.php', array('posts' => $posts)); - - return new Response($html); - } - - function show_action($id) - { - $post = get_post_by_id($id); - $html = render_template('templates/show.php', array('post' => $post)); - - return new Response($html); - } - - // helper function to render templates - function render_template($path, array $args) - { - extract($args); - ob_start(); - require $path; - $html = ob_get_clean(); - - return $html; - } - -By bringing in a small part of Symfony2, the application is more flexible and -reliable. The ``Request`` provides a dependable way to access information -about the HTTP request. Specifically, the ``getPathInfo()`` method returns -a cleaned URI (always returning ``/show`` and never ``/index.php/show``). -So, even if the user goes to ``/index.php/show``, the application is intelligent -enough to route the request through ``show_action()``. - -The ``Response`` object gives flexibility when constructing the HTTP response, -allowing HTTP headers and content to be added via an object-oriented interface. -And while the responses in this application are simple, this flexibility -will pay dividends as your application grows. - -The Sample Application in Symfony2 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The blog has come a *long* way, but it still contains a lot of code for such -a simple application. Along the way, you've made a simple routing -system and a method using ``ob_start()`` and ``ob_get_clean()`` to render -templates. If, for some reason, you needed to continue building this "framework" -from scratch, you could at least use Symfony's standalone `Routing`_ and -`Templating`_ components, which already solve these problems. - -Instead of re-solving common problems, you can let Symfony2 take care of -them for you. Here's the same sample application, now built in Symfony2:: - - // src/Acme/BlogBundle/Controller/BlogController.php - namespace Acme\BlogBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - - class BlogController extends Controller - { - public function listAction() - { - $posts = $this->get('doctrine')->getManager() - ->createQuery('SELECT p FROM AcmeBlogBundle:Post p') - ->execute(); - - return $this->render( - 'AcmeBlogBundle:Blog:list.html.php', - array('posts' => $posts) - ); - } - - public function showAction($id) - { - $post = $this->get('doctrine') - ->getManager() - ->getRepository('AcmeBlogBundle:Post') - ->find($id) - ; - - if (!$post) { - // cause the 404 page not found to be displayed - throw $this->createNotFoundException(); - } - - return $this->render( - 'AcmeBlogBundle:Blog:show.html.php', - array('post' => $post) - ); - } - } - -The two controllers are still lightweight. Each uses the :doc:`Doctrine ORM library` -to retrieve objects from the database and the ``Templating`` component to -render a template and return a ``Response`` object. The list template is -now quite a bit simpler: - -.. code-block:: html+php - - - extend('::layout.html.php') ?> - - set('title', 'List of Posts') ?> - -

List of Posts

- - -The layout is nearly identical: - -.. code-block:: html+php - - - - - - <?php echo $view['slots']->output( - 'title', - 'Default title' - ) ?> - - - output('_content') ?> - - - -.. note:: - - The show template is left as an exercise, as it should be trivial to - create based on the list template. - -When Symfony2's engine (called the ``Kernel``) boots up, it needs a map so -that it knows which controllers to execute based on the request information. -A routing configuration map provides this information in a readable format: - -.. code-block:: yaml - - # app/config/routing.yml - blog_list: - path: /blog - defaults: { _controller: AcmeBlogBundle:Blog:list } - - blog_show: - path: /blog/show/{id} - defaults: { _controller: AcmeBlogBundle:Blog:show } - -Now that Symfony2 is handling all the mundane tasks, the front controller -is dead simple. And since it does so little, you'll never have to touch -it once it's created (and if you use a Symfony2 distribution, you won't -even need to create it!):: - - // web/app.php - require_once __DIR__.'/../app/bootstrap.php'; - require_once __DIR__.'/../app/AppKernel.php'; - - use Symfony\Component\HttpFoundation\Request; - - $kernel = new AppKernel('prod', false); - $kernel->handle(Request::createFromGlobals())->send(); - -The front controller's only job is to initialize Symfony2's engine (``Kernel``) -and pass it a ``Request`` object to handle. Symfony2's core then uses the -routing map to determine which controller to call. Just like before, the -controller method is responsible for returning the final ``Response`` object. -There's really not much else to it. - -For a visual representation of how Symfony2 handles each request, see the -:ref:`request flow diagram`. - -Where Symfony2 Delivers -~~~~~~~~~~~~~~~~~~~~~~~ - -In the upcoming chapters, you'll learn more about how each piece of Symfony -works and the recommended organization of a project. For now, let's see how -migrating the blog from flat PHP to Symfony2 has improved life: - -* Your application now has **clear and consistently organized code** (though - Symfony doesn't force you into this). This promotes **reusability** and - allows for new developers to be productive in your project more quickly; - -* 100% of the code you write is for *your* application. You **don't need - to develop or maintain low-level utilities** such as :ref:`autoloading`, - :doc:`routing`, or rendering :doc:`controllers`; - -* Symfony2 gives you **access to open source tools** such as Doctrine and the - Templating, Security, Form, Validation and Translation components (to name - a few); - -* The application now enjoys **fully-flexible URLs** thanks to the ``Routing`` - component; - -* Symfony2's HTTP-centric architecture gives you access to powerful tools - such as **HTTP caching** powered by **Symfony2's internal HTTP cache** or - more powerful tools such as `Varnish`_. This is covered in a later chapter - all about :doc:`caching`. - -And perhaps best of all, by using Symfony2, you now have access to a whole -set of **high-quality open source tools developed by the Symfony2 community**! -A good selection of Symfony2 community tools can be found on `KnpBundles.com`_. - -Better templates ----------------- - -If you choose to use it, Symfony2 comes standard with a templating engine -called `Twig`_ that makes templates faster to write and easier to read. -It means that the sample application could contain even less code! Take, -for example, the list template written in Twig: - -.. code-block:: html+jinja - - {# src/Acme/BlogBundle/Resources/views/Blog/list.html.twig #} - {% extends "::layout.html.twig" %} - - {% block title %}List of Posts{% endblock %} - - {% block body %} -

List of Posts

- - {% endblock %} - -The corresponding ``layout.html.twig`` template is also easier to write: - -.. code-block:: html+jinja - - {# app/Resources/views/layout.html.twig #} - - - - {% block title %}Default title{% endblock %} - - - {% block body %}{% endblock %} - - - -Twig is well-supported in Symfony2. And while PHP templates will always -be supported in Symfony2, the many advantages of Twig will continue to -be discussed. For more information, see the :doc:`templating chapter`. - -Learn more from the Cookbook ----------------------------- - -* :doc:`/cookbook/templating/PHP` -* :doc:`/cookbook/controller/service` - -.. _`Doctrine`: http://www.doctrine-project.org -.. _`download Composer`: http://getcomposer.org/download/ -.. _`Routing`: https://github.com/symfony/Routing -.. _`Templating`: https://github.com/symfony/Templating -.. _`KnpBundles.com`: http://knpbundles.com/ -.. _`Twig`: http://twig.sensiolabs.org -.. _`Varnish`: https://www.varnish-cache.org/ -.. _`PHPUnit`: http://www.phpunit.de diff --git a/book/http_cache.rst b/book/http_cache.rst deleted file mode 100644 index c14cb9f9f3e..00000000000 --- a/book/http_cache.rst +++ /dev/null @@ -1,1084 +0,0 @@ -.. index:: - single: Cache - -HTTP Cache -========== - -The nature of rich web applications means that they're dynamic. No matter -how efficient your application, each request will always contain more overhead -than serving a static file. - -And for most Web applications, that's fine. Symfony2 is lightning fast, and -unless you're doing some serious heavy-lifting, each request will come back -quickly without putting too much stress on your server. - -But as your site grows, that overhead can become a problem. The processing -that's normally performed on every request should be done only once. This -is exactly what caching aims to accomplish. - -Caching on the Shoulders of Giants ----------------------------------- - -The most effective way to improve performance of an application is to cache -the full output of a page and then bypass the application entirely on each -subsequent request. Of course, this isn't always possible for highly dynamic -websites, or is it? In this chapter, you'll see how the Symfony2 cache -system works and why this is the best possible approach. - -The Symfony2 cache system is different because it relies on the simplicity -and power of the HTTP cache as defined in the :term:`HTTP specification`. -Instead of reinventing a caching methodology, Symfony2 embraces the standard -that defines basic communication on the Web. Once you understand the fundamental -HTTP validation and expiration caching models, you'll be ready to master -the Symfony2 cache system. - -For the purposes of learning how to cache with Symfony2, the -subject is covered in four steps: - -#. A :ref:`gateway cache `, or reverse proxy, is - an independent layer that sits in front of your application. The reverse - proxy caches responses as they're returned from your application and answers - requests with cached responses before they hit your application. Symfony2 - provides its own reverse proxy, but any reverse proxy can be used. - -#. :ref:`HTTP cache ` headers are used - to communicate with the gateway cache and any other caches between your - application and the client. Symfony2 provides sensible defaults and a - powerful interface for interacting with the cache headers. - -#. HTTP :ref:`expiration and validation ` - are the two models used for determining whether cached content is *fresh* - (can be reused from the cache) or *stale* (should be regenerated by the - application). - -#. :ref:`Edge Side Includes ` (ESI) allow HTTP - cache to be used to cache page fragments (even nested fragments) independently. - With ESI, you can even cache an entire page for 60 minutes, but an embedded - sidebar for only 5 minutes. - -Since caching with HTTP isn't unique to Symfony, many articles already exist -on the topic. If you're new to HTTP caching, Ryan -Tomayko's article `Things Caches Do`_ is *highly* recommended . Another in-depth resource is Mark -Nottingham's `Cache Tutorial`_. - -.. index:: - single: Cache; Proxy - single: Cache; Reverse proxy - single: Cache; Gateway - -.. _gateway-caches: - -Caching with a Gateway Cache ----------------------------- - -When caching with HTTP, the *cache* is separated from your application entirely -and sits between your application and the client making the request. - -The job of the cache is to accept requests from the client and pass them -back to your application. The cache will also receive responses back from -your application and forward them on to the client. The cache is the "middle-man" -of the request-response communication between the client and your application. - -Along the way, the cache will store each response that is deemed "cacheable" -(See :ref:`http-cache-introduction`). If the same resource is requested again, -the cache sends the cached response to the client, ignoring your application -entirely. - -This type of cache is known as a HTTP gateway cache and many exist such -as `Varnish`_, `Squid in reverse proxy mode`_, and the Symfony2 reverse proxy. - -.. index:: - single: Cache; Types of - -Types of Caches -~~~~~~~~~~~~~~~ - -But a gateway cache isn't the only type of cache. In fact, the HTTP cache -headers sent by your application are consumed and interpreted by up to three -different types of caches: - -* *Browser caches*: Every browser comes with its own local cache that is - mainly useful for when you hit "back" or for images and other assets. - The browser cache is a *private* cache as cached resources aren't shared - with anyone else; - -* *Proxy caches*: A proxy is a *shared* cache as many people can be behind a - single one. It's usually installed by large corporations and ISPs to reduce - latency and network traffic; - -* *Gateway caches*: Like a proxy, it's also a *shared* cache but on the server - side. Installed by network administrators, it makes websites more scalable, - reliable and performant. - -.. tip:: - - Gateway caches are sometimes referred to as reverse proxy caches, - surrogate caches, or even HTTP accelerators. - -.. note:: - - The significance of *private* versus *shared* caches will become more - obvious when caching responses containing content that is - specific to exactly one user (e.g. account information) is discussed. - -Each response from your application will likely go through one or both of -the first two cache types. These caches are outside of your control but follow -the HTTP cache directions set in the response. - -.. index:: - single: Cache; Symfony2 reverse proxy - -.. _`symfony-gateway-cache`: - -Symfony2 Reverse Proxy -~~~~~~~~~~~~~~~~~~~~~~ - -Symfony2 comes with a reverse proxy (also called a gateway cache) written -in PHP. Enable it and cacheable responses from your application will start -to be cached right away. Installing it is just as easy. Each new Symfony2 -application comes with a pre-configured caching kernel (``AppCache``) that -wraps the default one (``AppKernel``). The caching Kernel *is* the reverse -proxy. - -To enable caching, modify the code of a front controller to use the caching -kernel:: - - // web/app.php - require_once __DIR__.'/../app/bootstrap.php.cache'; - require_once __DIR__.'/../app/AppKernel.php'; - require_once __DIR__.'/../app/AppCache.php'; - - use Symfony\Component\HttpFoundation\Request; - - $kernel = new AppKernel('prod', false); - $kernel->loadClassCache(); - // wrap the default AppKernel with the AppCache one - $kernel = new AppCache($kernel); - $request = Request::createFromGlobals(); - $response = $kernel->handle($request); - $response->send(); - $kernel->terminate($request, $response); - -The caching kernel will immediately act as a reverse proxy - caching responses -from your application and returning them to the client. - -.. tip:: - - The cache kernel has a special ``getLog()`` method that returns a string - representation of what happened in the cache layer. In the development - environment, use it to debug and validate your cache strategy:: - - error_log($kernel->getLog()); - -The ``AppCache`` object has a sensible default configuration, but it can be -finely tuned via a set of options you can set by overriding the -:method:`Symfony\\Bundle\\FrameworkBundle\\HttpCache\\HttpCache::getOptions` -method:: - - // app/AppCache.php - use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache; - - class AppCache extends HttpCache - { - protected function getOptions() - { - return array( - 'debug' => false, - 'default_ttl' => 0, - 'private_headers' => array('Authorization', 'Cookie'), - 'allow_reload' => false, - 'allow_revalidate' => false, - 'stale_while_revalidate' => 2, - 'stale_if_error' => 60, - ); - } - } - -.. tip:: - - Unless overridden in ``getOptions()``, the ``debug`` option will be set - to automatically be the debug value of the wrapped ``AppKernel``. - -Here is a list of the main options: - -* ``default_ttl``: The number of seconds that a cache entry should be - considered fresh when no explicit freshness information is provided in a - response. Explicit ``Cache-Control`` or ``Expires`` headers override this - value (default: ``0``); - -* ``private_headers``: Set of request headers that trigger "private" - ``Cache-Control`` behavior on responses that don't explicitly state whether - the response is ``public`` or ``private`` via a ``Cache-Control`` directive. - (default: ``Authorization`` and ``Cookie``); - -* ``allow_reload``: Specifies whether the client can force a cache reload by - including a ``Cache-Control`` "no-cache" directive in the request. Set it to - ``true`` for compliance with RFC 2616 (default: ``false``); - -* ``allow_revalidate``: Specifies whether the client can force a cache - revalidate by including a ``Cache-Control`` "max-age=0" directive in the - request. Set it to ``true`` for compliance with RFC 2616 (default: false); - -* ``stale_while_revalidate``: Specifies the default number of seconds (the - granularity is the second as the Response TTL precision is a second) during - which the cache can immediately return a stale response while it revalidates - it in the background (default: ``2``); this setting is overridden by the - ``stale-while-revalidate`` HTTP ``Cache-Control`` extension (see RFC 5861); - -* ``stale_if_error``: Specifies the default number of seconds (the granularity - is the second) during which the cache can serve a stale response when an - error is encountered (default: ``60``). This setting is overridden by the - ``stale-if-error`` HTTP ``Cache-Control`` extension (see RFC 5861). - -If ``debug`` is ``true``, Symfony2 automatically adds a ``X-Symfony-Cache`` -header to the response containing useful information about cache hits and -misses. - -.. sidebar:: Changing from one Reverse Proxy to Another - - The Symfony2 reverse proxy is a great tool to use when developing your - website or when you deploy your website to a shared host where you cannot - install anything beyond PHP code. But being written in PHP, it cannot - be as fast as a proxy written in C. That's why it is highly recommended you - use Varnish or Squid on your production servers if possible. The good - news is that the switch from one proxy server to another is easy and - transparent as no code modification is needed in your application. Start - easy with the Symfony2 reverse proxy and upgrade later to Varnish when - your traffic increases. - - For more information on using Varnish with Symfony2, see the - :doc:`How to use Varnish ` cookbook chapter. - -.. note:: - - The performance of the Symfony2 reverse proxy is independent of the - complexity of the application. That's because the application kernel is - only booted when the request needs to be forwarded to it. - -.. index:: - single: Cache; HTTP - -.. _http-cache-introduction: - -Introduction to HTTP Caching ----------------------------- - -To take advantage of the available cache layers, your application must be -able to communicate which responses are cacheable and the rules that govern -when/how that cache should become stale. This is done by setting HTTP cache -headers on the response. - -.. tip:: - - Keep in mind that "HTTP" is nothing more than the language (a simple text - language) that web clients (e.g. browsers) and web servers use to communicate - with each other. HTTP caching is the part of that language that allows clients - and servers to exchange information related to caching. - -HTTP specifies four response cache headers that are looked at here: - -* ``Cache-Control`` -* ``Expires`` -* ``ETag`` -* ``Last-Modified`` - -The most important and versatile header is the ``Cache-Control`` header, -which is actually a collection of various cache information. - -.. note:: - - Each of the headers will be explained in full detail in the - :ref:`http-expiration-validation` section. - -.. index:: - single: Cache; Cache-Control header - single: HTTP headers; Cache-Control - -The Cache-Control Header -~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``Cache-Control`` header is unique in that it contains not one, but various -pieces of information about the cacheability of a response. Each piece of -information is separated by a comma: - - Cache-Control: private, max-age=0, must-revalidate - - Cache-Control: max-age=3600, must-revalidate - -Symfony provides an abstraction around the ``Cache-Control`` header to make -its creation more manageable:: - - // ... - - use Symfony\Component\HttpFoundation\Response; - - $response = new Response(); - - // mark the response as either public or private - $response->setPublic(); - $response->setPrivate(); - - // set the private or shared max age - $response->setMaxAge(600); - $response->setSharedMaxAge(600); - - // set a custom Cache-Control directive - $response->headers->addCacheControlDirective('must-revalidate', true); - -Public vs Private Responses -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Both gateway and proxy caches are considered "shared" caches as the cached -content is shared by more than one user. If a user-specific response were -ever mistakenly stored by a shared cache, it might be returned later to any -number of different users. Imagine if your account information were cached -and then returned to every subsequent user who asked for their account page! - -To handle this situation, every response may be set to be public or private: - -* *public*: Indicates that the response may be cached by both private and - shared caches; - -* *private*: Indicates that all or part of the response message is intended - for a single user and must not be cached by a shared cache. - -Symfony conservatively defaults each response to be private. To take advantage -of shared caches (like the Symfony2 reverse proxy), the response will need -to be explicitly set as public. - -.. index:: - single: Cache; Safe methods - -Safe Methods -~~~~~~~~~~~~ - -HTTP caching only works for "safe" HTTP methods (like GET and HEAD). Being -safe means that you never change the application's state on the server when -serving the request (you can of course log information, cache data, etc). -This has two very reasonable consequences: - -* You should *never* change the state of your application when responding - to a GET or HEAD request. Even if you don't use a gateway cache, the presence - of proxy caches mean that any GET or HEAD request may or may not actually - hit your server; - -* Don't expect PUT, POST or DELETE methods to cache. These methods are meant - to be used when mutating the state of your application (e.g. deleting a - blog post). Caching them would prevent certain requests from hitting and - mutating your application. - -Caching Rules and Defaults -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -HTTP 1.1 allows caching anything by default unless there is an explicit -``Cache-Control`` header. In practice, most caches do nothing when requests -have a cookie, an authorization header, use a non-safe method (i.e. PUT, POST, -DELETE), or when responses have a redirect status code. - -Symfony2 automatically sets a sensible and conservative ``Cache-Control`` -header when none is set by the developer by following these rules: - -* If no cache header is defined (``Cache-Control``, ``Expires``, ``ETag`` - or ``Last-Modified``), ``Cache-Control`` is set to ``no-cache``, meaning - that the response will not be cached; - -* If ``Cache-Control`` is empty (but one of the other cache headers is present), - its value is set to ``private, must-revalidate``; - -* But if at least one ``Cache-Control`` directive is set, and no 'public' or - ``private`` directives have been explicitly added, Symfony2 adds the - ``private`` directive automatically (except when ``s-maxage`` is set). - -.. _http-expiration-validation: - -HTTP Expiration and Validation ------------------------------- - -The HTTP specification defines two caching models: - -* With the `expiration model`_, you simply specify how long a response should - be considered "fresh" by including a ``Cache-Control`` and/or an ``Expires`` - header. Caches that understand expiration will not make the same request - until the cached version reaches its expiration time and becomes "stale"; - -* When pages are really dynamic (i.e. their representation changes often), - the `validation model`_ is often necessary. With this model, the - cache stores the response, but asks the server on each request whether - or not the cached response is still valid. The application uses a unique - response identifier (the ``Etag`` header) and/or a timestamp (the ``Last-Modified`` - header) to check if the page has changed since being cached. - -The goal of both models is to never generate the same response twice by relying -on a cache to store and return "fresh" responses. - -.. sidebar:: Reading the HTTP Specification - - The HTTP specification defines a simple but powerful language in which - clients and servers can communicate. As a web developer, the request-response - model of the specification dominates your work. Unfortunately, the actual - specification document - `RFC 2616`_ - can be difficult to read. - - There is an on-going effort (`HTTP Bis`_) to rewrite the RFC 2616. It does - not describe a new version of HTTP, but mostly clarifies the original HTTP - specification. The organization is also improved as the specification - is split into seven parts; everything related to HTTP caching can be - found in two dedicated parts (`P4 - Conditional Requests`_ and `P6 - - Caching: Browser and intermediary caches`_). - - As a web developer, you are strongly urged to read the specification. Its - clarity and power - even more than ten years after its creation - is - invaluable. Don't be put-off by the appearance of the spec - its contents - are much more beautiful than its cover. - -.. index:: - single: Cache; HTTP expiration - -Expiration -~~~~~~~~~~ - -The expiration model is the more efficient and straightforward of the two -caching models and should be used whenever possible. When a response is cached -with an expiration, the cache will store the response and return it directly -without hitting the application until it expires. - -The expiration model can be accomplished using one of two, nearly identical, -HTTP headers: ``Expires`` or ``Cache-Control``. - -.. index:: - single: Cache; Expires header - single: HTTP headers; Expires - -Expiration with the ``Expires`` Header -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -According to the HTTP specification, "the ``Expires`` header field gives -the date/time after which the response is considered stale." The ``Expires`` -header can be set with the ``setExpires()`` ``Response`` method. It takes a -``DateTime`` instance as an argument:: - - $date = new DateTime(); - $date->modify('+600 seconds'); - - $response->setExpires($date); - -The resulting HTTP header will look like this: - -.. code-block:: text - - Expires: Thu, 01 Mar 2011 16:00:00 GMT - -.. note:: - - The ``setExpires()`` method automatically converts the date to the GMT - timezone as required by the specification. - -Note that in HTTP versions before 1.1 the origin server wasn't required to -send the ``Date`` header. Consequently the cache (e.g. the browser) might -need to rely onto his local clock to evaluate the ``Expires`` header making -the lifetime calculation vulnerable to clock skew. Another limitation -of the ``Expires`` header is that the specification states that "HTTP/1.1 -servers should not send ``Expires`` dates more than one year in the future." - -.. index:: - single: Cache; Cache-Control header - single: HTTP headers; Cache-Control - -Expiration with the ``Cache-Control`` Header -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Because of the ``Expires`` header limitations, most of the time, you should -use the ``Cache-Control`` header instead. Recall that the ``Cache-Control`` -header is used to specify many different cache directives. For expiration, -there are two directives, ``max-age`` and ``s-maxage``. The first one is -used by all caches, whereas the second one is only taken into account by -shared caches:: - - // Sets the number of seconds after which the response - // should no longer be considered fresh - $response->setMaxAge(600); - - // Same as above but only for shared caches - $response->setSharedMaxAge(600); - -The ``Cache-Control`` header would take on the following format (it may have -additional directives): - -.. code-block:: text - - Cache-Control: max-age=600, s-maxage=600 - -.. index:: - single: Cache; Validation - -Validation -~~~~~~~~~~ - -When a resource needs to be updated as soon as a change is made to the underlying -data, the expiration model falls short. With the expiration model, the application -won't be asked to return the updated response until the cache finally becomes -stale. - -The validation model addresses this issue. Under this model, the cache continues -to store responses. The difference is that, for each request, the cache asks -the application whether or not the cached response is still valid. If the -cache *is* still valid, your application should return a 304 status code -and no content. This tells the cache that it's ok to return the cached response. - -Under this model, you mainly save bandwidth as the representation is not -sent twice to the same client (a 304 response is sent instead). But if you -design your application carefully, you might be able to get the bare minimum -data needed to send a 304 response and save CPU also (see below for an implementation -example). - -.. tip:: - - The 304 status code means "Not Modified". It's important because with - this status code do *not* contain the actual content being requested. - Instead, the response is simply a light-weight set of directions that - tell cache that it should use its stored version. - -Like with expiration, there are two different HTTP headers that can be used -to implement the validation model: ``ETag`` and ``Last-Modified``. - -.. index:: - single: Cache; Etag header - single: HTTP headers; Etag - -Validation with the ``ETag`` Header -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``ETag`` header is a string header (called the "entity-tag") that uniquely -identifies one representation of the target resource. It's entirely generated -and set by your application so that you can tell, for example, if the ``/about`` -resource that's stored by the cache is up-to-date with what your application -would return. An ``ETag`` is like a fingerprint and is used to quickly compare -if two different versions of a resource are equivalent. Like fingerprints, -each ``ETag`` must be unique across all representations of the same resource. - -To see a simple implementation, generate the ETag as the md5 of the content:: - - public function indexAction() - { - $response = $this->render('MyBundle:Main:index.html.twig'); - $response->setETag(md5($response->getContent())); - $response->setPublic(); // make sure the response is public/cacheable - $response->isNotModified($this->getRequest()); - - return $response; - } - -The :method:`Symfony\\Component\\HttpFoundation\\Response::isNotModified` -method compares the ``ETag`` sent with the ``Request`` with the one set -on the ``Response``. If the two match, the method automatically sets the -``Response`` status code to 304. - -This algorithm is simple enough and very generic, but you need to create the -whole ``Response`` before being able to compute the ETag, which is sub-optimal. -In other words, it saves on bandwidth, but not CPU cycles. - -In the :ref:`optimizing-cache-validation` section, you'll see how validation -can be used more intelligently to determine the validity of a cache without -doing so much work. - -.. tip:: - - Symfony2 also supports weak ETags by passing ``true`` as the second - argument to the - :method:`Symfony\\Component\\HttpFoundation\\Response::setETag` method. - -.. index:: - single: Cache; Last-Modified header - single: HTTP headers; Last-Modified - -Validation with the ``Last-Modified`` Header -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``Last-Modified`` header is the second form of validation. According -to the HTTP specification, "The ``Last-Modified`` header field indicates -the date and time at which the origin server believes the representation -was last modified." In other words, the application decides whether or not -the cached content has been updated based on whether or not it's been updated -since the response was cached. - -For instance, you can use the latest update date for all the objects needed to -compute the resource representation as the value for the ``Last-Modified`` -header value:: - - public function showAction($articleSlug) - { - // ... - - $articleDate = new \DateTime($article->getUpdatedAt()); - $authorDate = new \DateTime($author->getUpdatedAt()); - - $date = $authorDate > $articleDate ? $authorDate : $articleDate; - - $response->setLastModified($date); - // Set response as public. Otherwise it will be private by default. - $response->setPublic(); - - if ($response->isNotModified($this->getRequest())) { - return $response; - } - - // ... do more work to populate the response with the full content - - return $response; - } - -The :method:`Symfony\\Component\\HttpFoundation\\Response::isNotModified` -method compares the ``If-Modified-Since`` header sent by the request with -the ``Last-Modified`` header set on the response. If they are equivalent, -the ``Response`` will be set to a 304 status code. - -.. note:: - - The ``If-Modified-Since`` request header equals the ``Last-Modified`` - header of the last response sent to the client for the particular resource. - This is how the client and server communicate with each other and decide - whether or not the resource has been updated since it was cached. - -.. index:: - single: Cache; Conditional get - single: HTTP; 304 - -.. _optimizing-cache-validation: - -Optimizing your Code with Validation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The main goal of any caching strategy is to lighten the load on the application. -Put another way, the less you do in your application to return a 304 response, -the better. The ``Response::isNotModified()`` method does exactly that by -exposing a simple and efficient pattern:: - - use Symfony\Component\HttpFoundation\Response; - - public function showAction($articleSlug) - { - // Get the minimum information to compute - // the ETag or the Last-Modified value - // (based on the Request, data is retrieved from - // a database or a key-value store for instance) - $article = ...; - - // create a Response with a ETag and/or a Last-Modified header - $response = new Response(); - $response->setETag($article->computeETag()); - $response->setLastModified($article->getPublishedAt()); - - // Set response as public. Otherwise it will be private by default. - $response->setPublic(); - - // Check that the Response is not modified for the given Request - if ($response->isNotModified($this->getRequest())) { - // return the 304 Response immediately - return $response; - } else { - // do more work here - like retrieving more data - $comments = ...; - - // or render a template with the $response you've already started - return $this->render( - 'MyBundle:MyController:article.html.twig', - array('article' => $article, 'comments' => $comments), - $response - ); - } - } - -When the ``Response`` is not modified, the ``isNotModified()`` automatically sets -the response status code to ``304``, removes the content, and removes some -headers that must not be present for ``304`` responses (see -:method:`Symfony\\Component\\HttpFoundation\\Response::setNotModified`). - -.. index:: - single: Cache; Vary - single: HTTP headers; Vary - -Varying the Response -~~~~~~~~~~~~~~~~~~~~ - -So far, it's been assumed that each URI has exactly one representation of the -target resource. By default, HTTP caching is done by using the URI of the -resource as the cache key. If two people request the same URI of a cacheable -resource, the second person will receive the cached version. - -Sometimes this isn't enough and different versions of the same URI need to -be cached based on one or more request header values. For instance, if you -compress pages when the client supports it, any given URI has two representations: -one when the client supports compression, and one when it does not. This -determination is done by the value of the ``Accept-Encoding`` request header. - -In this case, you need the cache to store both a compressed and uncompressed -version of the response for the particular URI and return them based on the -request's ``Accept-Encoding`` value. This is done by using the ``Vary`` response -header, which is a comma-separated list of different headers whose values -trigger a different representation of the requested resource: - -.. code-block:: text - - Vary: Accept-Encoding, User-Agent - -.. tip:: - - This particular ``Vary`` header would cache different versions of each - resource based on the URI and the value of the ``Accept-Encoding`` and - ``User-Agent`` request header. - -The ``Response`` object offers a clean interface for managing the ``Vary`` -header:: - - // set one vary header - $response->setVary('Accept-Encoding'); - - // set multiple vary headers - $response->setVary(array('Accept-Encoding', 'User-Agent')); - -The ``setVary()`` method takes a header name or an array of header names for -which the response varies. - -Expiration and Validation -~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can of course use both validation and expiration within the same ``Response``. -As expiration wins over validation, you can easily benefit from the best of -both worlds. In other words, by using both expiration and validation, you -can instruct the cache to serve the cached content, while checking back -at some interval (the expiration) to verify that the content is still valid. - -.. index:: - pair: Cache; Configuration - -More Response Methods -~~~~~~~~~~~~~~~~~~~~~ - -The Response class provides many more methods related to the cache. Here are -the most useful ones:: - - // Marks the Response stale - $response->expire(); - - // Force the response to return a proper 304 response with no content - $response->setNotModified(); - -Additionally, most cache-related HTTP headers can be set via the single -:method:`Symfony\\Component\\HttpFoundation\\Response::setCache` method:: - - // Set cache settings in one call - $response->setCache(array( - 'etag' => $etag, - 'last_modified' => $date, - 'max_age' => 10, - 's_maxage' => 10, - 'public' => true, - // 'private' => true, - )); - -.. index:: - single: Cache; ESI - single: ESI - -.. _edge-side-includes: - -Using Edge Side Includes ------------------------- - -Gateway caches are a great way to make your website perform better. But they -have one limitation: they can only cache whole pages. If you can't cache -whole pages or if parts of a page has "more" dynamic parts, you are out of -luck. Fortunately, Symfony2 provides a solution for these cases, based on a -technology called `ESI`_, or Edge Side Includes. Akamaï wrote this specification -almost 10 years ago, and it allows specific parts of a page to have a different -caching strategy than the main page. - -The ESI specification describes tags you can embed in your pages to communicate -with the gateway cache. Only one tag is implemented in Symfony2, ``include``, -as this is the only useful one outside of Akamaï context: - -.. code-block:: html - - - - - - - - - - - - - -.. note:: - - Notice from the example that each ESI tag has a fully-qualified URL. - An ESI tag represents a page fragment that can be fetched via the given - URL. - -When a request is handled, the gateway cache fetches the entire page from -its cache or requests it from the backend application. If the response contains -one or more ESI tags, these are processed in the same way. In other words, -the gateway cache either retrieves the included page fragment from its cache -or requests the page fragment from the backend application again. When all -the ESI tags have been resolved, the gateway cache merges each into the main -page and sends the final content to the client. - -All of this happens transparently at the gateway cache level (i.e. outside -of your application). As you'll see, if you choose to take advantage of ESI -tags, Symfony2 makes the process of including them almost effortless. - -Using ESI in Symfony2 -~~~~~~~~~~~~~~~~~~~~~ - -First, to use ESI, be sure to enable it in your application configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - # ... - esi: { enabled: true } - - .. code-block:: xml - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - // ... - 'esi' => array('enabled' => true), - )); - -Now, suppose you have a page that is relatively static, except for a news -ticker at the bottom of the content. With ESI, you can cache the news ticker -independent of the rest of the page. - -.. code-block:: php - - public function indexAction() - { - $response = $this->render('MyBundle:MyController:index.html.twig'); - // set the shared max age - which also marks the response as public - $response->setSharedMaxAge(600); - - return $response; - } - -In this example, the full-page cache has a lifetime of ten minutes. -Next, include the news ticker in the template by embedding an action. -This is done via the ``render`` helper (See :ref:`templating-embedding-controller` -for more details). - -As the embedded content comes from another page (or controller for that -matter), Symfony2 uses the standard ``render`` helper to configure ESI tags: - -.. configuration-block:: - - .. code-block:: jinja - - {# you can use a controller reference #} - {{ render_esi(controller('...:news', { 'max': 5 })) }} - - {# ... or a URL #} - {{ render_esi(url('latest_news', { 'max': 5 })) }} - - .. code-block:: html+php - - render( - new ControllerReference('...:news', array('max' => 5)), - array('renderer' => 'esi')) - ?> - - render( - $view['router']->generate('latest_news', array('max' => 5), true), - array('renderer' => 'esi'), - ) ?> - -By using the ``esi`` renderer (via the ``render_esi`` Twig function), you -tell Symfony2 that the action should be rendered as an ESI tag. You might be -wondering why you would want to use a helper instead of just writing the ESI -tag yourself. That's because using a helper makes your application work even -if there is no gateway cache installed. - -When using the default ``render`` function (or setting the renderer to -``inline``), Symfony2 merges the included page content into the main one -before sending the response to the client. But if you use the ``esi`` renderer -(i.e. call ``render_esi``), *and* if Symfony2 detects that it's talking to a -gateway cache that supports ESI, it generates an ESI include tag. But if there -is no gateway cache or if it does not support ESI, Symfony2 will just merge -the included page content within the main one as it would have done if you had -used ``render``. - -.. note:: - - Symfony2 detects if a gateway cache supports ESI via another Akamaï - specification that is supported out of the box by the Symfony2 reverse - proxy. - -The embedded action can now specify its own caching rules, entirely independent -of the master page. - -.. code-block:: php - - public function newsAction($max) - { - // ... - - $response->setSharedMaxAge(60); - } - -With ESI, the full page cache will be valid for 600 seconds, but the news -component cache will only last for 60 seconds. - -When using a controller reference, the ESI tag should reference the embedded -action as an accessible URL so the gateway cache can fetch it independently of -the rest of the page. Symfony2 takes care of generating a unique URL for any -controller reference and it is able to route them properly thanks to a -listener that must be enabled in your configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - # ... - fragments: { path: /_fragment } - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - // ... - 'fragments' => array('path' => '/_fragment'), - )); - -One great advantage of the ESI renderer is that you can make your application -as dynamic as needed and at the same time, hit the application as little as -possible. - -.. tip:: - - The listener listener only responds to local IP addresses or trusted - proxies. - -.. note:: - - Once you start using ESI, remember to always use the ``s-maxage`` - directive instead of ``max-age``. As the browser only ever receives the - aggregated resource, it is not aware of the sub-components, and so it will - obey the ``max-age`` directive and cache the entire page. And you don't - want that. - -The ``render_esi`` helper supports two other useful options: - -* ``alt``: used as the ``alt`` attribute on the ESI tag, which allows you - to specify an alternative URL to be used if the ``src`` cannot be found; - -* ``ignore_errors``: if set to true, an ``onerror`` attribute will be added - to the ESI with a value of ``continue`` indicating that, in the event of - a failure, the gateway cache will simply remove the ESI tag silently. - -.. index:: - single: Cache; Invalidation - -.. _http-cache-invalidation: - -Cache Invalidation ------------------- - - "There are only two hard things in Computer Science: cache invalidation - and naming things." --Phil Karlton - -You should never need to invalidate cached data because invalidation is already -taken into account natively in the HTTP cache models. If you use validation, -you never need to invalidate anything by definition; and if you use expiration -and need to invalidate a resource, it means that you set the expires date -too far away in the future. - -.. note:: - - Since invalidation is a topic specific to each type of reverse proxy, - if you don't worry about invalidation, you can switch between reverse - proxies without changing anything in your application code. - -Actually, all reverse proxies provide ways to purge cached data, but you -should avoid them as much as possible. The most standard way is to purge the -cache for a given URL by requesting it with the special ``PURGE`` HTTP method. - -Here is how you can configure the Symfony2 reverse proxy to support the -``PURGE`` HTTP method:: - - // app/AppCache.php - - // ... - use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpFoundation\Response; - - class AppCache extends HttpCache - { - protected function invalidate(Request $request, $catch = false) - { - if ('PURGE' !== $request->getMethod()) { - return parent::invalidate($request, $catch); - } - - $response = new Response(); - if (!$this->getStore()->purge($request->getUri())) { - $response->setStatusCode(404, 'Not purged'); - } else { - $response->setStatusCode(200, 'Purged'); - } - - return $response; - } - } - -.. caution:: - - You must protect the ``PURGE`` HTTP method somehow to avoid random people - purging your cached data. - -Summary -------- - -Symfony2 was designed to follow the proven rules of the road: HTTP. Caching -is no exception. Mastering the Symfony2 cache system means becoming familiar -with the HTTP cache models and using them effectively. This means that, instead -of relying only on Symfony2 documentation and code examples, you have access -to a world of knowledge related to HTTP caching and gateway caches such as -Varnish. - -Learn more from the Cookbook ----------------------------- - -* :doc:`/cookbook/cache/varnish` - -.. _`Things Caches Do`: http://tomayko.com/writings/things-caches-do -.. _`Cache Tutorial`: http://www.mnot.net/cache_docs/ -.. _`Varnish`: https://www.varnish-cache.org/ -.. _`Squid in reverse proxy mode`: http://wiki.squid-cache.org/SquidFaq/ReverseProxy -.. _`expiration model`: http://tools.ietf.org/html/rfc2616#section-13.2 -.. _`validation model`: http://tools.ietf.org/html/rfc2616#section-13.3 -.. _`RFC 2616`: http://tools.ietf.org/html/rfc2616 -.. _`HTTP Bis`: http://tools.ietf.org/wg/httpbis/ -.. _`P4 - Conditional Requests`: http://tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-12 -.. _`P6 - Caching: Browser and intermediary caches`: http://tools.ietf.org/html/draft-ietf-httpbis-p6-cache-12 -.. _`ESI`: http://www.w3.org/TR/esi-lang diff --git a/book/http_fundamentals.rst b/book/http_fundamentals.rst deleted file mode 100644 index 16520777863..00000000000 --- a/book/http_fundamentals.rst +++ /dev/null @@ -1,572 +0,0 @@ -.. index:: - single: Symfony2 Fundamentals - -Symfony2 and HTTP Fundamentals -============================== - -Congratulations! By learning about Symfony2, you're well on your way towards -being a more *productive*, *well-rounded* and *popular* web developer (actually, -you're on your own for the last part). Symfony2 is built to get back to -basics: to develop tools that let you develop faster and build more robust -applications, while staying out of your way. Symfony is built on the best -ideas from many technologies: the tools and concepts you're about to learn -represent the efforts of thousands of people, over many years. In other words, -you're not just learning "Symfony", you're learning the fundamentals of the -web, development best practices, and how to use many amazing new PHP libraries, -inside or independently of Symfony2. So, get ready. - -True to the Symfony2 philosophy, this chapter begins by explaining the fundamental -concept common to web development: HTTP. Regardless of your background or -preferred programming language, this chapter is a **must-read** for everyone. - -HTTP is Simple --------------- - -HTTP (Hypertext Transfer Protocol to the geeks) is a text language that allows -two machines to communicate with each other. That's it! For example, when -checking for the latest `xkcd`_ comic, the following (approximate) conversation -takes place: - -.. image:: /images/http-xkcd.png - :align: center - -And while the actual language used is a bit more formal, it's still dead-simple. -HTTP is the term used to describe this simple text-based language. And no -matter how you develop on the web, the goal of your server is *always* to -understand simple text requests, and return simple text responses. - -Symfony2 is built from the ground-up around that reality. Whether you realize -it or not, HTTP is something you use everyday. With Symfony2, you'll learn -how to master it. - -.. index:: - single: HTTP; Request-response paradigm - -Step1: The Client sends a Request -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Every conversation on the web starts with a *request*. The request is a text -message created by a client (e.g. a browser, an iPhone app, etc) in a -special format known as HTTP. The client sends that request to a server, -and then waits for the response. - -Take a look at the first part of the interaction (the request) between a -browser and the xkcd web server: - -.. image:: /images/http-xkcd-request.png - :align: center - -In HTTP-speak, this HTTP request would actually look something like this: - -.. code-block:: text - - GET / HTTP/1.1 - Host: xkcd.com - Accept: text/html - User-Agent: Mozilla/5.0 (Macintosh) - -This simple message communicates *everything* necessary about exactly which -resource the client is requesting. The first line of an HTTP request is the -most important and contains two things: the URI and the HTTP method. - -The URI (e.g. ``/``, ``/contact``, etc) is the unique address or location -that identifies the resource the client wants. The HTTP method (e.g. ``GET``) -defines what you want to *do* with the resource. The HTTP methods are the -*verbs* of the request and define the few common ways that you can act upon -the resource: - -+----------+---------------------------------------+ -| *GET* | Retrieve the resource from the server | -+----------+---------------------------------------+ -| *POST* | Create a resource on the server | -+----------+---------------------------------------+ -| *PUT* | Update the resource on the server | -+----------+---------------------------------------+ -| *DELETE* | Delete the resource from the server | -+----------+---------------------------------------+ - -With this in mind, you can imagine what an HTTP request might look like to -delete a specific blog entry, for example: - -.. code-block:: text - - DELETE /blog/15 HTTP/1.1 - -.. note:: - - There are actually nine HTTP methods defined by the HTTP specification, - but many of them are not widely used or supported. In reality, many modern - browsers don't support the ``PUT`` and ``DELETE`` methods. - -In addition to the first line, an HTTP request invariably contains other -lines of information called request headers. The headers can supply a wide -range of information such as the requested ``Host``, the response formats -the client accepts (``Accept``) and the application the client is using to -make the request (``User-Agent``). Many other headers exist and can be found -on Wikipedia's `List of HTTP header fields`_ article. - -Step 2: The Server returns a Response -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Once a server has received the request, it knows exactly which resource the -client needs (via the URI) and what the client wants to do with that resource -(via the method). For example, in the case of a GET request, the server -prepares the resource and returns it in an HTTP response. Consider the response -from the xkcd web server: - -.. image:: /images/http-xkcd.png - :align: center - -Translated into HTTP, the response sent back to the browser will look something -like this: - -.. code-block:: text - - HTTP/1.1 200 OK - Date: Sat, 02 Apr 2011 21:05:05 GMT - Server: lighttpd/1.4.19 - Content-Type: text/html - - - - - -The HTTP response contains the requested resource (the HTML content in this -case), as well as other information about the response. The first line is -especially important and contains the HTTP response status code (200 in this -case). The status code communicates the overall outcome of the request back -to the client. Was the request successful? Was there an error? Different -status codes exist that indicate success, an error, or that the client needs -to do something (e.g. redirect to another page). A full list can be found -on Wikipedia's `List of HTTP status codes`_ article. - -Like the request, an HTTP response contains additional pieces of information -known as HTTP headers. For example, one important HTTP response header is -``Content-Type``. The body of the same resource could be returned in multiple -different formats like HTML, XML, or JSON and the ``Content-Type`` header uses -Internet Media Types like ``text/html`` to tell the client which format is -being returned. A list of common media types can be found on Wikipedia's -`List of common media types`_ article. - -Many other headers exist, some of which are very powerful. For example, certain -headers can be used to create a powerful caching system. - -Requests, Responses and Web Development -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This request-response conversation is the fundamental process that drives all -communication on the web. And as important and powerful as this process is, -it's inescapably simple. - -The most important fact is this: regardless of the language you use, the -type of application you build (web, mobile, JSON API), or the development -philosophy you follow, the end goal of an application is **always** to understand -each request and create and return the appropriate response. - -Symfony is architected to match this reality. - -.. tip:: - - To learn more about the HTTP specification, read the original `HTTP 1.1 RFC`_ - or the `HTTP Bis`_, which is an active effort to clarify the original - specification. A great tool to check both the request and response headers - while browsing is the `Live HTTP Headers`_ extension for Firefox. - -.. index:: - single: Symfony2 Fundamentals; Requests and responses - -Requests and Responses in PHP ------------------------------ - -So how do you interact with the "request" and create a "response" when using -PHP? In reality, PHP abstracts you a bit from the whole process:: - - $uri = $_SERVER['REQUEST_URI']; - $foo = $_GET['foo']; - - header('Content-type: text/html'); - echo 'The URI requested is: '.$uri; - echo 'The value of the "foo" parameter is: '.$foo; - -As strange as it sounds, this small application is in fact taking information -from the HTTP request and using it to create an HTTP response. Instead of -parsing the raw HTTP request message, PHP prepares superglobal variables -such as ``$_SERVER`` and ``$_GET`` that contain all the information from -the request. Similarly, instead of returning the HTTP-formatted text response, -you can use the ``header()`` function to create response headers and simply -print out the actual content that will be the content portion of the response -message. PHP will create a true HTTP response and return it to the client: - -.. code-block:: text - - HTTP/1.1 200 OK - Date: Sat, 03 Apr 2011 02:14:33 GMT - Server: Apache/2.2.17 (Unix) - Content-Type: text/html - - The URI requested is: /testing?foo=symfony - The value of the "foo" parameter is: symfony - -Requests and Responses in Symfony ---------------------------------- - -Symfony provides an alternative to the raw PHP approach via two classes that -allow you to interact with the HTTP request and response in an easier way. -The :class:`Symfony\\Component\\HttpFoundation\\Request` class is a simple -object-oriented representation of the HTTP request message. With it, you -have all the request information at your fingertips:: - - use Symfony\Component\HttpFoundation\Request; - - $request = Request::createFromGlobals(); - - // the URI being requested (e.g. /about) minus any query parameters - $request->getPathInfo(); - - // retrieve GET and POST variables respectively - $request->query->get('foo'); - $request->request->get('bar', 'default value if bar does not exist'); - - // retrieve SERVER variables - $request->server->get('HTTP_HOST'); - - // retrieves an instance of UploadedFile identified by foo - $request->files->get('foo'); - - // retrieve a COOKIE value - $request->cookies->get('PHPSESSID'); - - // retrieve an HTTP request header, with normalized, lowercase keys - $request->headers->get('host'); - $request->headers->get('content_type'); - - $request->getMethod(); // GET, POST, PUT, DELETE, HEAD - $request->getLanguages(); // an array of languages the client accepts - -As a bonus, the ``Request`` class does a lot of work in the background that -you'll never need to worry about. For example, the ``isSecure()`` method -checks the *three* different values in PHP that can indicate whether or not -the user is connecting via a secured connection (i.e. ``https``). - -.. sidebar:: ParameterBags and Request attributes - - As seen above, the ``$_GET`` and ``$_POST`` variables are accessible via - the public ``query`` and ``request`` properties respectively. Each of - these objects is a :class:`Symfony\\Component\\HttpFoundation\\ParameterBag` - object, which has methods like - :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::get`, - :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::has`, - :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::all` and more. - In fact, every public property used in the previous example is some instance - of the ParameterBag. - - .. _book-fundamentals-attributes: - - The Request class also has a public ``attributes`` property, which holds - special data related to how the application works internally. For the - Symfony2 framework, the ``attributes`` holds the values returned by the - matched route, like ``_controller``, ``id`` (if you have an ``{id}`` - wildcard), and even the name of the matched route (``_route``). The - ``attributes`` property exists entirely to be a place where you can - prepare and store context-specific information about the request. - - -Symfony also provides a ``Response`` class: a simple PHP representation of -an HTTP response message. This allows your application to use an object-oriented -interface to construct the response that needs to be returned to the client:: - - use Symfony\Component\HttpFoundation\Response; - $response = new Response(); - - $response->setContent('

Hello world!

'); - $response->setStatusCode(200); - $response->headers->set('Content-Type', 'text/html'); - - // prints the HTTP headers followed by the content - $response->send(); - -If Symfony offered nothing else, you would already have a toolkit for easily -accessing request information and an object-oriented interface for creating -the response. Even as you learn the many powerful features in Symfony, keep -in mind that the goal of your application is always *to interpret a request -and create the appropriate response based on your application logic*. - -.. tip:: - - The ``Request`` and ``Response`` classes are part of a standalone component - included with Symfony called ``HttpFoundation``. This component can be - used entirely independently of Symfony and also provides classes for handling - sessions and file uploads. - -The Journey from the Request to the Response --------------------------------------------- - -Like HTTP itself, the ``Request`` and ``Response`` objects are pretty simple. -The hard part of building an application is writing what comes in between. -In other words, the real work comes in writing the code that interprets the -request information and creates the response. - -Your application probably does many things, like sending emails, handling -form submissions, saving things to a database, rendering HTML pages and protecting -content with security. How can you manage all of this and still keep your -code organized and maintainable? - -Symfony was created to solve these problems so that you don't have to. - -The Front Controller -~~~~~~~~~~~~~~~~~~~~ - -Traditionally, applications were built so that each "page" of a site was -its own physical file: - -.. code-block:: text - - index.php - contact.php - blog.php - -There are several problems with this approach, including the inflexibility -of the URLs (what if you wanted to change ``blog.php`` to ``news.php`` without -breaking all of your links?) and the fact that each file *must* manually -include some set of core files so that security, database connections and -the "look" of the site can remain consistent. - -A much better solution is to use a :term:`front controller`: a single PHP -file that handles every request coming into your application. For example: - -+------------------------+------------------------+ -| ``/index.php`` | executes ``index.php`` | -+------------------------+------------------------+ -| ``/index.php/contact`` | executes ``index.php`` | -+------------------------+------------------------+ -| ``/index.php/blog`` | executes ``index.php`` | -+------------------------+------------------------+ - -.. tip:: - - Using Apache's ``mod_rewrite`` (or equivalent with other web servers), - the URLs can easily be cleaned up to be just ``/``, ``/contact`` and - ``/blog``. - -Now, every request is handled exactly the same way. Instead of individual URLs -executing different PHP files, the front controller is *always* executed, -and the routing of different URLs to different parts of your application -is done internally. This solves both problems with the original approach. -Almost all modern web apps do this - including apps like WordPress. - -Stay Organized -~~~~~~~~~~~~~~ - -Inside your front controller, you have to figure out which code should be -executed and what the content to return should be. To figure this out, you'll -need to check the incoming URI and execute different parts of your code depending -on that value. This can get ugly quickly:: - - // index.php - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpFoundation\Response; - $request = Request::createFromGlobals(); - $path = $request->getPathInfo(); // the URI path being requested - - if (in_array($path, array('', '/'))) { - $response = new Response('Welcome to the homepage.'); - } elseif ($path == '/contact') { - $response = new Response('Contact us'); - } else { - $response = new Response('Page not found.', 404); - } - $response->send(); - -Solving this problem can be difficult. Fortunately it's *exactly* what Symfony -is designed to do. - -The Symfony Application Flow -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When you let Symfony handle each request, life is much easier. Symfony follows -the same simple pattern for every request: - -.. _request-flow-figure: - -.. figure:: /images/request-flow.png - :align: center - :alt: Symfony2 request flow - - Incoming requests are interpreted by the routing and passed to controller - functions that return ``Response`` objects. - -Each "page" of your site is defined in a routing configuration file that -maps different URLs to different PHP functions. The job of each PHP function, -called a :term:`controller`, is to use information from the request - along -with many other tools Symfony makes available - to create and return a ``Response`` -object. In other words, the controller is where *your* code goes: it's where -you interpret the request and create a response. - -It's that easy! To review: - -* Each request executes a front controller file; - -* The routing system determines which PHP function should be executed based - on information from the request and routing configuration you've created; - -* The correct PHP function is executed, where your code creates and returns - the appropriate ``Response`` object. - -A Symfony Request in Action -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Without diving into too much detail, here is this process in action. Suppose -you want to add a ``/contact`` page to your Symfony application. First, start -by adding an entry for ``/contact`` to your routing configuration file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - contact: - path: /contact - defaults: { _controller: AcmeDemoBundle:Main:contact } - - .. code-block:: xml - - - AcmeBlogBundle:Main:contact - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('contact', new Route('/contact', array( - '_controller' => 'AcmeBlogBundle:Main:contact', - ))); - - return $collection; - -.. note:: - - This example uses :doc:`YAML` to define the routing - configuration. Routing configuration can also be written in other formats - such as XML or PHP. - -When someone visits the ``/contact`` page, this route is matched, and the -specified controller is executed. As you'll learn in the :doc:`routing chapter`, -the ``AcmeDemoBundle:Main:contact`` string is a short syntax that points to a -specific PHP method ``contactAction`` inside a class called ``MainController``:: - - // src/Acme/DemoBundle/Controller/MainController.php - use Symfony\Component\HttpFoundation\Response; - - class MainController - { - public function contactAction() - { - return new Response('

Contact us!

'); - } - } - -In this very simple example, the controller simply creates a -:class:`Symfony\\Component\\HttpFoundation\\Response` object with the HTML -"``

Contact us!

"``. In the :doc:`controller chapter`, -you'll learn how a controller can render templates, allowing your "presentation" -code (i.e. anything that actually writes out HTML) to live in a separate -template file. This frees up the controller to worry only about the hard -stuff: interacting with the database, handling submitted data, or sending -email messages. - -Symfony2: Build your App, not your Tools. ------------------------------------------ - -You now know that the goal of any app is to interpret each incoming request -and create an appropriate response. As an application grows, it becomes more -difficult to keep your code organized and maintainable. Invariably, the same -complex tasks keep coming up over and over again: persisting things to the -database, rendering and reusing templates, handling form submissions, sending -emails, validating user input and handling security. - -The good news is that none of these problems is unique. Symfony provides -a framework full of tools that allow you to build your application, not your -tools. With Symfony2, nothing is imposed on you: you're free to use the full -Symfony framework, or just one piece of Symfony all by itself. - -.. index:: - single: Symfony2 Components - -Standalone Tools: The Symfony2 *Components* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -So what *is* Symfony2? First, Symfony2 is a collection of over twenty independent -libraries that can be used inside *any* PHP project. These libraries, called -the *Symfony2 Components*, contain something useful for almost any situation, -regardless of how your project is developed. To name a few: - -* :doc:`HttpFoundation` - Contains - the ``Request`` and ``Response`` classes, as well as other classes for handling - sessions and file uploads; - -* :doc:`Routing` - Powerful and fast routing system that - allows you to map a specific URI (e.g. ``/contact``) to some information - about how that request should be handled (e.g. execute the ``contactAction()`` - method); - -* `Form`_ - A full-featured and flexible framework for creating forms and - handling form submissions; - -* `Validator`_ A system for creating rules about data and then validating - whether or not user-submitted data follows those rules; - -* :doc:`ClassLoader` An autoloading library that allows - PHP classes to be used without needing to manually ``require`` the files - containing those classes; - -* :doc:`Templating` A toolkit for rendering templates, - handling template inheritance (i.e. a template is decorated with a layout) - and performing other common template tasks; - -* `Security`_ - A powerful library for handling all types of security inside - an application; - -* `Translation`_ A framework for translating strings in your application. - -Each and every one of these components is decoupled and can be used in *any* -PHP project, regardless of whether or not you use the Symfony2 framework. -Every part is made to be used if needed and replaced when necessary. - -The Full Solution: The Symfony2 *Framework* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -So then, what *is* the Symfony2 *Framework*? The *Symfony2 Framework* is -a PHP library that accomplishes two distinct tasks: - -#. Provides a selection of components (i.e. the Symfony2 Components) and - third-party libraries (e.g. `Swiftmailer`_ for sending emails); - -#. Provides sensible configuration and a "glue" library that ties all of these - pieces together. - -The goal of the framework is to integrate many independent tools in order -to provide a consistent experience for the developer. Even the framework -itself is a Symfony2 bundle (i.e. a plugin) that can be configured or replaced -entirely. - -Symfony2 provides a powerful set of tools for rapidly developing web applications -without imposing on your application. Normal users can quickly start development -by using a Symfony2 distribution, which provides a project skeleton with -sensible defaults. For more advanced users, the sky is the limit. - -.. _`xkcd`: http://xkcd.com/ -.. _`HTTP 1.1 RFC`: http://www.w3.org/Protocols/rfc2616/rfc2616.html -.. _`HTTP Bis`: http://datatracker.ietf.org/wg/httpbis/ -.. _`Live HTTP Headers`: https://addons.mozilla.org/en-US/firefox/addon/live-http-headers/ -.. _`List of HTTP status codes`: http://en.wikipedia.org/wiki/List_of_HTTP_status_codes -.. _`List of HTTP header fields`: http://en.wikipedia.org/wiki/List_of_HTTP_header_fields -.. _`List of common media types`: http://en.wikipedia.org/wiki/Internet_media_type#List_of_common_media_types -.. _`Form`: https://github.com/symfony/Form -.. _`Validator`: https://github.com/symfony/Validator -.. _`Security`: https://github.com/symfony/Security -.. _`Translation`: https://github.com/symfony/Translation -.. _`Swiftmailer`: http://swiftmailer.org/ diff --git a/book/index.rst b/book/index.rst deleted file mode 100755 index 915b0fc7a7f..00000000000 --- a/book/index.rst +++ /dev/null @@ -1,27 +0,0 @@ -The Book -======== - -.. toctree:: - :hidden: - - http_fundamentals - from_flat_php_to_symfony2 - installation - page_creation - controller - routing - templating - doctrine - propel - testing - validation - forms - security - http_cache - translation - service_container - performance - internals - stable_api - -.. include:: /book/map.rst.inc diff --git a/book/installation.rst b/book/installation.rst deleted file mode 100644 index b4167818ece..00000000000 --- a/book/installation.rst +++ /dev/null @@ -1,374 +0,0 @@ -.. index:: - single: Installation - -Installing and Configuring Symfony -================================== - -The goal of this chapter is to get you up and running with a working application -built on top of Symfony. Fortunately, Symfony offers "distributions", which -are functional Symfony "starter" projects that you can download and begin -developing in immediately. - -.. tip:: - - If you're looking for instructions on how best to create a new project - and store it via source control, see `Using Source Control`_. - -Installing a Symfony2 Distribution ----------------------------------- - -.. tip:: - - First, check that you have installed and configured a Web server (such - as Apache) with PHP 5.3.8 or higher. For more information on Symfony2 - requirements, see the :doc:`requirements reference`. - -Symfony2 packages "distributions", which are fully-functional applications -that include the Symfony2 core libraries, a selection of useful bundles, a -sensible directory structure and some default configuration. When you download -a Symfony2 distribution, you're downloading a functional application skeleton -that can be used immediately to begin developing your application. - -Start by visiting the Symfony2 download page at `http://symfony.com/download`_. -On this page, you'll see the *Symfony Standard Edition*, which is the main -Symfony2 distribution. There are 2 ways to get your project started: - -Option 1) Composer -~~~~~~~~~~~~~~~~~~ - -`Composer`_ is a dependency management library for PHP, which you can use -to download the Symfony2 Standard Edition. - -Start by `downloading Composer`_ anywhere onto your local computer. If you -have curl installed, it's as easy as: - -.. code-block:: bash - - curl -s https://getcomposer.org/installer | php - -.. note:: - - If your computer is not ready to use Composer, you'll see some recommendations - when running this command. Follow those recommendations to get Composer - working properly. - -Composer is an executable PHAR file, which you can use to download the Standard -Distribution: - -.. code-block:: bash - - php composer.phar create-project symfony/framework-standard-edition /path/to/webroot/Symfony 2.2.0 - -.. tip:: - - For an exact version, replace `2.2.0` with the latest Symfony version - (e.g. 2.2.1). For details, see the `Symfony Installation Page`_ - -.. tip:: - - To download the vendor files faster, add the ``--prefer-dist`` option at - the end of any Composer command. - -This command may take several minutes to run as Composer downloads the Standard -Distribution along with all of the vendor libraries that it needs. When it finishes, -you should have a directory that looks something like this: - -.. code-block:: text - - path/to/webroot/ <- your web server directory (sometimes named htdocs or public) - Symfony/ <- the new directory - app/ - cache/ - config/ - logs/ - src/ - ... - vendor/ - ... - web/ - app.php - ... - -Option 2) Download an Archive -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can also download an archive of the Standard Edition. Here, you'll -need to make two choices: - -* Download either a ``.tgz`` or ``.zip`` archive - both are equivalent, download - whatever you're more comfortable using; - -* Download the distribution with or without vendors. If you're planning on - using more third-party libraries or bundles and managing them via Composer, - you should probably download "without vendors". - -Download one of the archives somewhere under your local web server's root -directory and unpack it. From a UNIX command line, this can be done with -one of the following commands (replacing ``###`` with your actual filename): - -.. code-block:: bash - - # for .tgz file - $ tar zxvf Symfony_Standard_Vendors_2.2.###.tgz - - # for a .zip file - $ unzip Symfony_Standard_Vendors_2.2.###.zip - -If you've downloaded "without vendors", you'll definitely need to read the -next section. - -.. note:: - - You can easily override the default directory structure. See - :doc:`/cookbook/configuration/override_dir_structure` for more - information. - -All public files and the front controller that handles incoming requests in -a Symfony2 application live in the ``Symfony/web/`` directory. So, assuming -you unpacked the archive into your web server's or virtual host's document root, -your application's URLs will start with ``http://localhost/Symfony/web/``. -To get nice and short URLs you should point the document root of your web -server or virtual host to the ``Symfony/web/`` directory. Though this is not -required for development it is recommended when your application goes into -production as all system and configuration files become inaccessible to clients. -For information on configuring your specific web server document root, see -the following documentation: `Apache`_ | `Nginx`_ . - -.. note:: - - The following examples assume you don't touch the document root settings - so all URLs start with ``http://localhost/Symfony/web/`` - -.. _installation-updating-vendors: - -Updating Vendors -~~~~~~~~~~~~~~~~ - -At this point, you've downloaded a fully-functional Symfony project in which -you'll start to develop your own application. A Symfony project depends on -a number of external libraries. These are downloaded into the `vendor/` directory -of your project via a library called `Composer`_. - -Depending on how you downloaded Symfony, you may or may not need to do update -your vendors right now. But, updating your vendors is always safe, and guarantees -that you have all the vendor libraries you need. - -Step 1: Get `Composer`_ (The great new PHP packaging system) - -.. code-block:: bash - - curl -s http://getcomposer.org/installer | php - -Make sure you download ``composer.phar`` in the same folder where -the ``composer.json`` file is located (this is your Symfony project -root by default). - -Step 2: Install vendors - -.. code-block:: bash - - $ php composer.phar install - -This command downloads all of the necessary vendor libraries - including -Symfony itself - into the ``vendor/`` directory. - -.. note:: - - If you don't have ``curl`` installed, you can also just download the ``installer`` - file manually at http://getcomposer.org/installer. Place this file into your - project and then run: - - .. code-block:: bash - - php installer - php composer.phar install - -.. tip:: - - When running ``php composer.phar install`` or ``php composer.phar update``, - composer will execute post install/update commands to clear the cache - and install assets. By default, the assets will be copied into your ``web`` - directory. - - Instead of copying your Symfony assets, you can create symlinks if - your operating system supports it. To create symlinks, add an entry - in the ``extra`` node of your composer.json file with the key - ``symfony-assets-install`` and the value ``symlink``: - - - .. code-block:: json - - "extra": { - "symfony-app-dir": "app", - "symfony-web-dir": "web", - "symfony-assets-install": "symlink" - } - - When passing ``relative`` instead of ``symlink`` to symfony-assets-install, - the command will generate relative symlinks. - -Configuration and Setup -~~~~~~~~~~~~~~~~~~~~~~~ - -At this point, all of the needed third-party libraries now live in the ``vendor/`` -directory. You also have a default application setup in ``app/`` and some -sample code inside the ``src/`` directory. - -Symfony2 comes with a visual server configuration tester to help make sure -your Web server and PHP are configured to use Symfony. Use the following URL -to check your configuration: - -.. code-block:: text - - http://localhost/config.php - -If there are any issues, correct them now before moving on. - -.. sidebar:: Setting up Permissions - - One common issue is that the ``app/cache`` and ``app/logs`` directories - must be writable both by the web server and the command line user. On - a UNIX system, if your web server user is different from your command - line user, you can run the following commands just once in your project - to ensure that permissions will be setup properly. - - **Note that not all web servers run as the user** ``www-data`` as in the examples - below. Instead, check which user *your* web server is being run as and - use it in place of ``www-data``. - - On a UNIX system, this can be done with one of the following commands: - - .. code-block:: bash - - $ ps aux | grep httpd - - or - - .. code-block:: bash - - $ ps aux | grep apache - - **1. Using ACL on a system that supports chmod +a** - - Many systems allow you to use the ``chmod +a`` command. Try this first, - and if you get an error - try the next method. Be sure to replace ``www-data`` - with your web server user on the first ``chmod`` command: - - .. code-block:: bash - - $ rm -rf app/cache/* - $ rm -rf app/logs/* - - $ sudo chmod +a "www-data allow delete,write,append,file_inherit,directory_inherit" app/cache app/logs - $ sudo chmod +a "`whoami` allow delete,write,append,file_inherit,directory_inherit" app/cache app/logs - - **2. Using Acl on a system that does not support chmod +a** - - Some systems don't support ``chmod +a``, but do support another utility - called ``setfacl``. You may need to `enable ACL support`_ on your partition - and install setfacl before using it (as is the case with Ubuntu), like - so: - - .. code-block:: bash - - $ sudo setfacl -R -m u:www-data:rwX -m u:`whoami`:rwX app/cache app/logs - $ sudo setfacl -dR -m u:www-data:rwx -m u:`whoami`:rwx app/cache app/logs - - **3. Without using ACL** - - If you don't have access to changing the ACL of the directories, you will - need to change the umask so that the cache and log directories will - be group-writable or world-writable (depending if the web server user - and the command line user are in the same group or not). To achieve - this, put the following line at the beginning of the ``app/console``, - ``web/app.php`` and ``web/app_dev.php`` files:: - - umask(0002); // This will let the permissions be 0775 - - // or - - umask(0000); // This will let the permissions be 0777 - - Note that using the ACL is recommended when you have access to them - on your server because changing the umask is not thread-safe. - -When everything is fine, click on "Go to the Welcome page" to request your -first "real" Symfony2 webpage: - -.. code-block:: text - - http://localhost/app_dev.php/ - -Symfony2 should welcome and congratulate you for your hard work so far! - -.. image:: /images/quick_tour/welcome.png - -.. tip:: - - To get nice and short urls you should point the document root of your - webserver or virtual host to the ``Symfony/web/`` directory. Though - this is not required for development it is recommended at the time your - application goes into production as all system and configuration files - become inaccessible to clients then. For information on configuring - your specific web server document root, read - :doc:`/cookbook/configuration/web_server_configuration` - or consult the official documentation of your webserver: - `Apache`_ | `Nginx`_ . - -Beginning Development ---------------------- - -Now that you have a fully-functional Symfony2 application, you can begin -development! Your distribution may contain some sample code - check the -``README.md`` file included with the distribution (open it as a text file) -to learn about what sample code was included with your distribution. - -If you're new to Symfony, check out ":doc:`page_creation`", where you'll -learn how to create pages, change configuration, and do everything else you'll -need in your new application. - -Be sure to also check out the :doc:`Cookbook`, which contains -a wide variety of articles about solving specific problems with Symfony. - -.. note:: - - If you want to remove the sample code from your distribution, take a look - at this cookbook article: ":doc:`/cookbook/bundles/remove`" - -Using Source Control --------------------- - -If you're using a version control system like ``Git`` or ``Subversion``, you -can setup your version control system and begin committing your project to -it as normal. The Symfony Standard edition *is* the starting point for your -new project. - -For specific instructions on how best to setup your project to be stored -in git, see :doc:`/cookbook/workflow/new_project_git`. - -Ignoring the ``vendor/`` Directory -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you've downloaded the archive *without vendors*, you can safely ignore -the entire ``vendor/`` directory and not commit it to source control. With -``Git``, this is done by creating and adding the following to a ``.gitignore`` -file: - -.. code-block:: text - - /vendor/ - -Now, the vendor directory won't be committed to source control. This is fine -(actually, it's great!) because when someone else clones or checks out the -project, he/she can simply run the ``php composer.phar install`` script to -install all the necessary project dependencies. - -.. _`enable ACL support`: https://help.ubuntu.com/community/FilePermissionsACLs -.. _`http://symfony.com/download`: http://symfony.com/download -.. _`Git`: http://git-scm.com/ -.. _`GitHub Bootcamp`: http://help.github.com/set-up-git-redirect -.. _`Composer`: http://getcomposer.org/ -.. _`downloading Composer`: http://getcomposer.org/download/ -.. _`Apache`: http://httpd.apache.org/docs/current/mod/core.html#documentroot -.. _`Nginx`: http://wiki.nginx.org/Symfony -.. _`Symfony Installation Page`: http://symfony.com/download diff --git a/book/internals.rst b/book/internals.rst deleted file mode 100644 index d61951b86fb..00000000000 --- a/book/internals.rst +++ /dev/null @@ -1,717 +0,0 @@ -.. index:: - single: Internals - -Internals -========= - -Looks like you want to understand how Symfony2 works and how to extend it. -That makes me very happy! This section is an in-depth explanation of the -Symfony2 internals. - -.. note:: - - You need to read this section only if you want to understand how Symfony2 - works behind the scene, or if you want to extend Symfony2. - -Overview --------- - -The Symfony2 code is made of several independent layers. Each layer is built -on top of the previous one. - -.. tip:: - - Autoloading is not managed by the framework directly; it's done by using - Composer's autoloader (``vendor/autoload.php``), which is included in - the ``app/autoload.php`` file. - -``HttpFoundation`` Component -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The deepest level is the :namespace:`Symfony\\Component\\HttpFoundation` -component. HttpFoundation provides the main objects needed to deal with HTTP. -It is an Object-Oriented abstraction of some native PHP functions and -variables: - -* The :class:`Symfony\\Component\\HttpFoundation\\Request` class abstracts - the main PHP global variables like ``$_GET``, ``$_POST``, ``$_COOKIE``, - ``$_FILES``, and ``$_SERVER``; - -* The :class:`Symfony\\Component\\HttpFoundation\\Response` class abstracts - some PHP functions like ``header()``, ``setcookie()``, and ``echo``; - -* The :class:`Symfony\\Component\\HttpFoundation\\Session` class and - :class:`Symfony\\Component\\HttpFoundation\\SessionStorage\\SessionStorageInterface` - interface abstract session management ``session_*()`` functions. - -.. note:: - - Read more about the :doc:`HttpFoundation Component `. - -``HttpKernel`` Component -~~~~~~~~~~~~~~~~~~~~~~~~ - -On top of HttpFoundation is the :namespace:`Symfony\\Component\\HttpKernel` -component. HttpKernel handles the dynamic part of HTTP; it is a thin wrapper -on top of the Request and Response classes to standardize the way requests are -handled. It also provides extension points and tools that makes it the ideal -starting point to create a Web framework without too much overhead. - -It also optionally adds configurability and extensibility, thanks to the -Dependency Injection component and a powerful plugin system (bundles). - -.. seealso:: - - Read more about the :doc:`HttpKernel Component `, - :doc:`Dependency Injection ` and - :doc:`Bundles `. - -``FrameworkBundle`` Bundle -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The :namespace:`Symfony\\Bundle\\FrameworkBundle` bundle is the bundle that -ties the main components and libraries together to make a lightweight and fast -MVC framework. It comes with a sensible default configuration and conventions -to ease the learning curve. - -.. index:: - single: Internals; Kernel - -Kernel ------- - -The :class:`Symfony\\Component\\HttpKernel\\HttpKernel` class is the central -class of Symfony2 and is responsible for handling client requests. Its main -goal is to "convert" a :class:`Symfony\\Component\\HttpFoundation\\Request` -object to a :class:`Symfony\\Component\\HttpFoundation\\Response` object. - -Every Symfony2 Kernel implements -:class:`Symfony\\Component\\HttpKernel\\HttpKernelInterface`:: - - function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true) - -.. index:: - single: Internals; Controller resolver - -Controllers -~~~~~~~~~~~ - -To convert a Request to a Response, the Kernel relies on a "Controller". A -Controller can be any valid PHP callable. - -The Kernel delegates the selection of what Controller should be executed -to an implementation of -:class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface`:: - - public function getController(Request $request); - - public function getArguments(Request $request, $controller); - -The -:method:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface::getController` -method returns the Controller (a PHP callable) associated with the given -Request. The default implementation -(:class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver`) -looks for a ``_controller`` request attribute that represents the controller -name (a "class::method" string, like ``Bundle\BlogBundle\PostController:indexAction``). - -.. tip:: - - The default implementation uses the - :class:`Symfony\\Bundle\\FrameworkBundle\\EventListener\\RouterListener` - to define the ``_controller`` Request attribute (see :ref:`kernel-core-request`). - -The -:method:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface::getArguments` -method returns an array of arguments to pass to the Controller callable. The -default implementation automatically resolves the method arguments, based on -the Request attributes. - -.. sidebar:: Matching Controller method arguments from Request attributes - - For each method argument, Symfony2 tries to get the value of a Request - attribute with the same name. If it is not defined, the argument default - value is used if defined:: - - // Symfony2 will look for an 'id' attribute (mandatory) - // and an 'admin' one (optional) - public function showAction($id, $admin = true) - { - // ... - } - -.. index:: - single: Internals; Request handling - -Handling Requests -~~~~~~~~~~~~~~~~~ - -The :method:`Symfony\\Component\\HttpKernel\\HttpKernel::handle` method -takes a ``Request`` and *always* returns a ``Response``. To convert the -``Request``, ``handle()`` relies on the Resolver and an ordered chain of -Event notifications (see the next section for more information about each -Event): - -#. Before doing anything else, the ``kernel.request`` event is notified -- if - one of the listeners returns a ``Response``, it jumps to step 8 directly; - -#. The Resolver is called to determine the Controller to execute; - -#. Listeners of the ``kernel.controller`` event can now manipulate the - Controller callable the way they want (change it, wrap it, ...); - -#. The Kernel checks that the Controller is actually a valid PHP callable; - -#. The Resolver is called to determine the arguments to pass to the Controller; - -#. The Kernel calls the Controller; - -#. If the Controller does not return a ``Response``, listeners of the - ``kernel.view`` event can convert the Controller return value to a ``Response``; - -#. Listeners of the ``kernel.response`` event can manipulate the ``Response`` - (content and headers); - -#. The Response is returned. - -If an Exception is thrown during processing, the ``kernel.exception`` is -notified and listeners are given a chance to convert the Exception to a -Response. If that works, the ``kernel.response`` event is notified; if not, the -Exception is re-thrown. - -If you don't want Exceptions to be caught (for embedded requests for -instance), disable the ``kernel.exception`` event by passing ``false`` as the -third argument to the ``handle()`` method. - -.. index:: - single: Internals; Internal requests - -Internal Requests -~~~~~~~~~~~~~~~~~ - -At any time during the handling of a request (the 'master' one), a sub-request -can be handled. You can pass the request type to the ``handle()`` method (its -second argument): - -* ``HttpKernelInterface::MASTER_REQUEST``; -* ``HttpKernelInterface::SUB_REQUEST``. - -The type is passed to all events and listeners can act accordingly (some -processing must only occur on the master request). - -.. index:: - pair: Kernel; Event - -Events -~~~~~~ - -Each event thrown by the Kernel is a subclass of -:class:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent`. This means that -each event has access to the same basic information: - -* :method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::getRequestType` - - returns the *type* of the request (``HttpKernelInterface::MASTER_REQUEST`` - or ``HttpKernelInterface::SUB_REQUEST``); - -* :method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::getKernel` - - returns the Kernel handling the request; - -* :method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::getRequest` - - returns the current ``Request`` being handled. - -``getRequestType()`` -.................... - -The ``getRequestType()`` method allows listeners to know the type of the -request. For instance, if a listener must only be active for master requests, -add the following code at the beginning of your listener method:: - - use Symfony\Component\HttpKernel\HttpKernelInterface; - - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { - // return immediately - return; - } - -.. tip:: - - If you are not yet familiar with the Symfony2 Event Dispatcher, read the - :doc:`Event Dispatcher Component Documentation` - section first. - -.. index:: - single: Event; kernel.request - -.. _kernel-core-request: - -``kernel.request`` Event -........................ - -*Event Class*: :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent` - -The goal of this event is to either return a ``Response`` object immediately -or setup variables so that a Controller can be called after the event. Any -listener can return a ``Response`` object via the ``setResponse()`` method on -the event. In this case, all other listeners won't be called. - -This event is used by ``FrameworkBundle`` to populate the ``_controller`` -``Request`` attribute, via the -:class:`Symfony\\Bundle\\FrameworkBundle\\EventListener\\RouterListener`. RequestListener -uses a :class:`Symfony\\Component\\Routing\\RouterInterface` object to match -the ``Request`` and determine the Controller name (stored in the -``_controller`` ``Request`` attribute). - -.. seealso:: - - Read more on the :ref:`kernel.request event `. - -.. index:: - single: Event; kernel.controller - -``kernel.controller`` Event -........................... - -*Event Class*: :class:`Symfony\\Component\\HttpKernel\\Event\\FilterControllerEvent` - -This event is not used by ``FrameworkBundle``, but can be an entry point used -to modify the controller that should be executed:: - - use Symfony\Component\HttpKernel\Event\FilterControllerEvent; - - public function onKernelController(FilterControllerEvent $event) - { - $controller = $event->getController(); - // ... - - // the controller can be changed to any PHP callable - $event->setController($controller); - } - -.. seealso:: - - Read more on the :ref:`kernel.controller event `. - -.. index:: - single: Event; kernel.view - -``kernel.view`` Event -..................... - -*Event Class*: :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForControllerResultEvent` - -This event is not used by ``FrameworkBundle``, but it can be used to implement -a view sub-system. This event is called *only* if the Controller does *not* -return a ``Response`` object. The purpose of the event is to allow some other -return value to be converted into a ``Response``. - -The value returned by the Controller is accessible via the -``getControllerResult`` method:: - - use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; - use Symfony\Component\HttpFoundation\Response; - - public function onKernelView(GetResponseForControllerResultEvent $event) - { - $val = $event->getControllerResult(); - $response = new Response(); - - // ... some how customize the Response from the return value - - $event->setResponse($response); - } - -.. seealso:: - - Read more on the :ref:`kernel.view event `. - -.. index:: - single: Event; kernel.response - -``kernel.response`` Event -......................... - -*Event Class*: :class:`Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent` - -The purpose of this event is to allow other systems to modify or replace the -``Response`` object after its creation:: - - public function onKernelResponse(FilterResponseEvent $event) - { - $response = $event->getResponse(); - - // ... modify the response object - } - -The ``FrameworkBundle`` registers several listeners: - -* :class:`Symfony\\Component\\HttpKernel\\EventListener\\ProfilerListener`: - collects data for the current request; - -* :class:`Symfony\\Bundle\\WebProfilerBundle\\EventListener\\WebDebugToolbarListener`: - injects the Web Debug Toolbar; - -* :class:`Symfony\\Component\\HttpKernel\\EventListener\\ResponseListener`: fixes the - Response ``Content-Type`` based on the request format; - -* :class:`Symfony\\Component\\HttpKernel\\EventListener\\EsiListener`: adds a - ``Surrogate-Control`` HTTP header when the Response needs to be parsed for - ESI tags. - -.. seealso:: - - Read more on the :ref:`kernel.response event `. - -.. index:: - single: Event; kernel.terminate - -``kernel.terminate`` Event -.......................... - -.. versionadded:: 2.1 - The ``kernel.terminate`` event is new since Symfony 2.1. - -The purpose of this event is to perform "heavier" tasks after the response -was already served to the client. - -.. seealso:: - - Read more on the :ref:`kernel.terminate event `. - -.. index:: - single: Event; kernel.exception - -.. _kernel-kernel.exception: - -``kernel.exception`` Event -.......................... - -*Event Class*: :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent` - -``FrameworkBundle`` registers an -:class:`Symfony\\Component\\HttpKernel\\EventListener\\ExceptionListener` that -forwards the ``Request`` to a given Controller (the value of the -``exception_listener.controller`` parameter -- must be in the -``class::method`` notation). - -A listener on this event can create and set a ``Response`` object, create -and set a new ``Exception`` object, or do nothing:: - - use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; - use Symfony\Component\HttpFoundation\Response; - - public function onKernelException(GetResponseForExceptionEvent $event) - { - $exception = $event->getException(); - $response = new Response(); - // setup the Response object based on the caught exception - $event->setResponse($response); - - // you can alternatively set a new Exception - // $exception = new \Exception('Some special exception'); - // $event->setException($exception); - } - -.. note:: - - As Symfony ensures that the Response status code is set to the most - appropriate one depending on the exception, setting the status on the - response won't work. If you want to overwrite the status code (which you - should not without a good reason), set the ``X-Status-Code`` header:: - - return new Response('Error', 404 /* ignored */, array('X-Status-Code' => 200)); - -.. index:: - single: Event Dispatcher - -The Event Dispatcher --------------------- - -The event dispatcher is a standalone component that is responsible for much -of the underlying logic and flow behind a Symfony request. For more information, -see the :doc:`Event Dispatcher Component Documentation`. - -.. seealso:: - - Read more on the :ref:`kernel.exception event `. - -.. index:: - single: Profiler - -.. _internals-profiler: - -Profiler --------- - -When enabled, the Symfony2 profiler collects useful information about each -request made to your application and store them for later analysis. Use the -profiler in the development environment to help you to debug your code and -enhance performance; use it in the production environment to explore problems -after the fact. - -You rarely have to deal with the profiler directly as Symfony2 provides -visualizer tools like the Web Debug Toolbar and the Web Profiler. If you use -the Symfony2 Standard Edition, the profiler, the web debug toolbar, and the -web profiler are all already configured with sensible settings. - -.. note:: - - The profiler collects information for all requests (simple requests, - redirects, exceptions, Ajax requests, ESI requests; and for all HTTP - methods and all formats). It means that for a single URL, you can have - several associated profiling data (one per external request/response - pair). - -.. index:: - single: Profiler; Visualizing - -Visualizing Profiling Data -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Using the Web Debug Toolbar -........................... - -In the development environment, the web debug toolbar is available at the -bottom of all pages. It displays a good summary of the profiling data that -gives you instant access to a lot of useful information when something does -not work as expected. - -If the summary provided by the Web Debug Toolbar is not enough, click on the -token link (a string made of 13 random characters) to access the Web Profiler. - -.. note:: - - If the token is not clickable, it means that the profiler routes are not - registered (see below for configuration information). - -Analyzing Profiling data with the Web Profiler -.............................................. - -The Web Profiler is a visualization tool for profiling data that you can use -in development to debug your code and enhance performance; but it can also be -used to explore problems that occur in production. It exposes all information -collected by the profiler in a web interface. - -.. index:: - single: Profiler; Using the profiler service - -Accessing the Profiling information -................................... - -You don't need to use the default visualizer to access the profiling -information. But how can you retrieve profiling information for a specific -request after the fact? When the profiler stores data about a Request, it also -associates a token with it; this token is available in the ``X-Debug-Token`` -HTTP header of the Response:: - - $profile = $container->get('profiler')->loadProfileFromResponse($response); - - $profile = $container->get('profiler')->loadProfile($token); - -.. tip:: - - When the profiler is enabled but not the web debug toolbar, or when you - want to get the token for an Ajax request, use a tool like Firebug to get - the value of the ``X-Debug-Token`` HTTP header. - -Use the :method:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler::find` -method to access tokens based on some criteria:: - - // get the latest 10 tokens - $tokens = $container->get('profiler')->find('', '', 10); - - // get the latest 10 tokens for all URL containing /admin/ - $tokens = $container->get('profiler')->find('', '/admin/', 10); - - // get the latest 10 tokens for local requests - $tokens = $container->get('profiler')->find('127.0.0.1', '', 10); - -If you want to manipulate profiling data on a different machine than the one -where the information were generated, use the -:method:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler::export` and -:method:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler::import` methods:: - - // on the production machine - $profile = $container->get('profiler')->loadProfile($token); - $data = $profiler->export($profile); - - // on the development machine - $profiler->import($data); - -.. index:: - single: Profiler; Visualizing - -Configuration -............. - -The default Symfony2 configuration comes with sensible settings for the -profiler, the web debug toolbar, and the web profiler. Here is for instance -the configuration for the development environment: - -.. configuration-block:: - - .. code-block:: yaml - - # load the profiler - framework: - profiler: { only_exceptions: false } - - # enable the web profiler - web_profiler: - toolbar: true - intercept_redirects: true - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // load the profiler - $container->loadFromExtension('framework', array( - 'profiler' => array('only-exceptions' => false), - )); - - // enable the web profiler - $container->loadFromExtension('web_profiler', array( - 'toolbar' => true, - 'intercept-redirects' => true, - 'verbose' => true, - )); - -When ``only-exceptions`` is set to ``true``, the profiler only collects data -when an exception is thrown by the application. - -When ``intercept-redirects`` is set to ``true``, the web profiler intercepts -the redirects and gives you the opportunity to look at the collected data -before following the redirect. - -If you enable the web profiler, you also need to mount the profiler routes: - -.. configuration-block:: - - .. code-block:: yaml - - _profiler: - resource: @WebProfilerBundle/Resources/config/routing/profiler.xml - prefix: /_profiler - - .. code-block:: xml - - - - .. code-block:: php - - $collection->addCollection($loader->import("@WebProfilerBundle/Resources/config/routing/profiler.xml"), '/_profiler'); - -As the profiler adds some overhead, you might want to enable it only under -certain circumstances in the production environment. The ``only-exceptions`` -settings limits profiling to 500 pages, but what if you want to get -information when the client IP comes from a specific address, or for a limited -portion of the website? You can use a request matcher: - -.. configuration-block:: - - .. code-block:: yaml - - # enables the profiler only for request coming for the 192.168.0.0 network - framework: - profiler: - matcher: { ip: 192.168.0.0/24 } - - # enables the profiler only for the /admin URLs - framework: - profiler: - matcher: { path: "^/admin/" } - - # combine rules - framework: - profiler: - matcher: { ip: 192.168.0.0/24, path: "^/admin/" } - - # use a custom matcher instance defined in the "custom_matcher" service - framework: - profiler: - matcher: { service: custom_matcher } - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - .. code-block:: php - - // enables the profiler only for request coming for the 192.168.0.0 network - $container->loadFromExtension('framework', array( - 'profiler' => array( - 'matcher' => array('ip' => '192.168.0.0/24'), - ), - )); - - // enables the profiler only for the /admin URLs - $container->loadFromExtension('framework', array( - 'profiler' => array( - 'matcher' => array('path' => '^/admin/'), - ), - )); - - // combine rules - $container->loadFromExtension('framework', array( - 'profiler' => array( - 'matcher' => array('ip' => '192.168.0.0/24', 'path' => '^/admin/'), - ), - )); - - # use a custom matcher instance defined in the "custom_matcher" service - $container->loadFromExtension('framework', array( - 'profiler' => array( - 'matcher' => array('service' => 'custom_matcher'), - ), - )); - -Learn more from the Cookbook ----------------------------- - -* :doc:`/cookbook/testing/profiling` -* :doc:`/cookbook/profiler/data_collector` -* :doc:`/cookbook/event_dispatcher/class_extension` -* :doc:`/cookbook/event_dispatcher/method_behavior` - -.. _`Symfony2 Dependency Injection component`: https://github.com/symfony/DependencyInjection diff --git a/book/map.rst.inc b/book/map.rst.inc deleted file mode 100644 index 573c8027524..00000000000 --- a/book/map.rst.inc +++ /dev/null @@ -1,19 +0,0 @@ -* :doc:`/book/http_fundamentals` -* :doc:`/book/from_flat_php_to_symfony2` -* :doc:`/book/installation` -* :doc:`/book/page_creation` -* :doc:`/book/controller` -* :doc:`/book/routing` -* :doc:`/book/templating` -* :doc:`/book/doctrine` -* :doc:`/book/propel` -* :doc:`/book/testing` -* :doc:`/book/validation` -* :doc:`/book/forms` -* :doc:`/book/security` -* :doc:`/book/http_cache` -* :doc:`/book/translation` -* :doc:`/book/service_container` -* :doc:`/book/performance` -* :doc:`/book/internals` -* :doc:`/book/stable_api` diff --git a/book/page_creation.rst b/book/page_creation.rst deleted file mode 100644 index f90ae6d4f48..00000000000 --- a/book/page_creation.rst +++ /dev/null @@ -1,1007 +0,0 @@ -.. index:: - single: Page creation - -Creating Pages in Symfony2 -========================== - -Creating a new page in Symfony2 is a simple two-step process: - -* *Create a route*: A route defines the URL (e.g. ``/about``) to your page - and specifies a controller (which is a PHP function) that Symfony2 should - execute when the URL of an incoming request matches the route path; - -* *Create a controller*: A controller is a PHP function that takes the incoming - request and transforms it into the Symfony2 ``Response`` object that's - returned to the user. - -This simple approach is beautiful because it matches the way that the Web works. -Every interaction on the Web is initiated by an HTTP request. The job of -your application is simply to interpret the request and return the appropriate -HTTP response. - -Symfony2 follows this philosophy and provides you with tools and conventions -to keep your application organized as it grows in users and complexity. - -.. index:: - single: Page creation; Example - -The "Hello Symfony!" Page -------------------------- - -Start by building a spin-off of the classic "Hello World!" application. When -you're finished, the user will be able to get a personal greeting (e.g. "Hello Symfony") -by going to the following URL: - -.. code-block:: text - - http://localhost/app_dev.php/hello/Symfony - -Actually, you'll be able to replace ``Symfony`` with any other name to be -greeted. To create the page, follow the simple two-step process. - -.. note:: - - The tutorial assumes that you've already downloaded Symfony2 and configured - your webserver. The above URL assumes that ``localhost`` points to the - ``web`` directory of your new Symfony2 project. For detailed information - on this process, see the documentation on the web server you are using. - Here's the relevant documentation page for some web server you might be using: - - * For Apache HTTP Server, refer to `Apache's DirectoryIndex documentation`_ - * For Nginx, refer to `Nginx HttpCoreModule location documentation`_ - -Before you begin: Create the Bundle -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Before you begin, you'll need to create a *bundle*. In Symfony2, a :term:`bundle` -is like a plugin, except that all of the code in your application will live -inside a bundle. - -A bundle is nothing more than a directory that houses everything related -to a specific feature, including PHP classes, configuration, and even stylesheets -and Javascript files (see :ref:`page-creation-bundles`). - -To create a bundle called ``AcmeHelloBundle`` (a play bundle that you'll -build in this chapter), run the following command and follow the on-screen -instructions (use all of the default options): - -.. code-block:: bash - - $ php app/console generate:bundle --namespace=Acme/HelloBundle --format=yml - -Behind the scenes, a directory is created for the bundle at ``src/Acme/HelloBundle``. -A line is also automatically added to the ``app/AppKernel.php`` file so that -the bundle is registered with the kernel:: - - // app/AppKernel.php - public function registerBundles() - { - $bundles = array( - ..., - new Acme\HelloBundle\AcmeHelloBundle(), - ); - // ... - - return $bundles; - } - -Now that you have a bundle setup, you can begin building your application -inside the bundle. - -Step 1: Create the Route -~~~~~~~~~~~~~~~~~~~~~~~~ - -By default, the routing configuration file in a Symfony2 application is -located at ``app/config/routing.yml``. Like all configuration in Symfony2, -you can also choose to use XML or PHP out of the box to configure routes. - -If you look at the main routing file, you'll see that Symfony already added -an entry when you generated the ``AcmeHelloBundle``: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - acme_hello: - resource: "@AcmeHelloBundle/Resources/config/routing.yml" - prefix: / - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->addCollection( - $loader->import('@AcmeHelloBundle/Resources/config/routing.php'), - '/', - ); - - return $collection; - -This entry is pretty basic: it tells Symfony to load routing configuration -from the ``Resources/config/routing.yml`` file that lives inside the ``AcmeHelloBundle``. -This means that you place routing configuration directly in ``app/config/routing.yml`` -or organize your routes throughout your application, and import them from here. - -Now that the ``routing.yml`` file from the bundle is being imported, add -the new route that defines the URL of the page that you're about to create: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/HelloBundle/Resources/config/routing.yml - hello: - path: /hello/{name} - defaults: { _controller: AcmeHelloBundle:Hello:index } - - .. code-block:: xml - - - - - - - - AcmeHelloBundle:Hello:index - - - - .. code-block:: php - - // src/Acme/HelloBundle/Resources/config/routing.php - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('hello', new Route('/hello/{name}', array( - '_controller' => 'AcmeHelloBundle:Hello:index', - ))); - - return $collection; - -The routing consists of two basic pieces: the ``path``, which is the URL -that this route will match, and a ``defaults`` array, which specifies the -controller that should be executed. The placeholder syntax in the path -(``{name}``) is a wildcard. It means that ``/hello/Ryan``, ``/hello/Fabien`` -or any other similar URL will match this route. The ``{name}`` placeholder -parameter will also be passed to the controller so that you can use its value -to personally greet the user. - -.. note:: - - The routing system has many more great features for creating flexible - and powerful URL structures in your application. For more details, see - the chapter all about :doc:`Routing `. - -Step 2: Create the Controller -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When a URL such as ``/hello/Ryan`` is handled by the application, the ``hello`` -route is matched and the ``AcmeHelloBundle:Hello:index`` controller is executed -by the framework. The second step of the page-creation process is to create -that controller. - -The controller - ``AcmeHelloBundle:Hello:index`` is the *logical* name of -the controller, and it maps to the ``indexAction`` method of a PHP class -called ``Acme\HelloBundle\Controller\HelloController``. Start by creating this file -inside your ``AcmeHelloBundle``:: - - // src/Acme/HelloBundle/Controller/HelloController.php - namespace Acme\HelloBundle\Controller; - - class HelloController - { - } - -In reality, the controller is nothing more than a PHP method that you create -and Symfony executes. This is where your code uses information from the request -to build and prepare the resource being requested. Except in some advanced -cases, the end product of a controller is always the same: a Symfony2 ``Response`` -object. - -Create the ``indexAction`` method that Symfony will execute when the ``hello`` -route is matched:: - - // src/Acme/HelloBundle/Controller/HelloController.php - namespace Acme\HelloBundle\Controller; - - use Symfony\Component\HttpFoundation\Response; - - class HelloController - { - public function indexAction($name) - { - return new Response('Hello '.$name.'!'); - } - } - -The controller is simple: it creates a new ``Response`` object, whose first -argument is the content that should be used in the response (a small HTML -page in this example). - -Congratulations! After creating only a route and a controller, you already -have a fully-functional page! If you've setup everything correctly, your -application should greet you: - -.. code-block:: text - - http://localhost/app_dev.php/hello/Ryan - -.. tip:: - - You can also view your app in the "prod" :ref:`environment` - by visiting: - - .. code-block:: text - - http://localhost/app.php/hello/Ryan - - If you get an error, it's likely because you need to clear your cache - by running: - - .. code-block:: bash - - $ php app/console cache:clear --env=prod --no-debug - -An optional, but common, third step in the process is to create a template. - -.. note:: - - Controllers are the main entry point for your code and a key ingredient - when creating pages. Much more information can be found in the - :doc:`Controller Chapter `. - -Optional Step 3: Create the Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Templates allow you to move all of the presentation (e.g. HTML code) into -a separate file and reuse different portions of the page layout. Instead -of writing the HTML inside the controller, render a template instead: - -.. code-block:: php - :linenos: - - // src/Acme/HelloBundle/Controller/HelloController.php - namespace Acme\HelloBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - - class HelloController extends Controller - { - public function indexAction($name) - { - return $this->render( - 'AcmeHelloBundle:Hello:index.html.twig', - array('name' => $name) - ); - - // render a PHP template instead - // return $this->render( - // 'AcmeHelloBundle:Hello:index.html.php', - // array('name' => $name) - // ); - } - } - -.. note:: - - In order to use the :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::render` - method, your controller must extend the - :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` class, - which adds shortcuts for tasks that are common inside controllers. This - is done in the above example by adding the ``use`` statement on line 4 - and then extending ``Controller`` on line 6. - -The ``render()`` method creates a ``Response`` object filled with the content -of the given, rendered template. Like any other controller, you will ultimately -return that ``Response`` object. - -Notice that there are two different examples for rendering the template. -By default, Symfony2 supports two different templating languages: classic -PHP templates and the succinct but powerful `Twig`_ templates. Don't be -alarmed - you're free to choose either or even both in the same project. - -The controller renders the ``AcmeHelloBundle:Hello:index.html.twig`` template, -which uses the following naming convention: - - **BundleName**:**ControllerName**:**TemplateName** - -This is the *logical* name of the template, which is mapped to a physical -location using the following convention. - - **/path/to/BundleName**/Resources/views/**ControllerName**/**TemplateName** - -In this case, ``AcmeHelloBundle`` is the bundle name, ``Hello`` is the -controller, and ``index.html.twig`` the template: - -.. configuration-block:: - - .. code-block:: jinja - :linenos: - - {# src/Acme/HelloBundle/Resources/views/Hello/index.html.twig #} - {% extends '::base.html.twig' %} - - {% block body %} - Hello {{ name }}! - {% endblock %} - - .. code-block:: html+php - - - extend('::base.html.php') ?> - - Hello escape($name) ?>! - -Step through the Twig template line-by-line: - -* *line 2*: The ``extends`` token defines a parent template. The template - explicitly defines a layout file inside of which it will be placed. - -* *line 4*: The ``block`` token says that everything inside should be placed - inside a block called ``body``. As you'll see, it's the responsibility - of the parent template (``base.html.twig``) to ultimately render the - block called ``body``. - -The parent template, ``::base.html.twig``, is missing both the **BundleName** -and **ControllerName** portions of its name (hence the double colon (``::``) -at the beginning). This means that the template lives outside of the bundles -and in the ``app`` directory: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# app/Resources/views/base.html.twig #} - - - - - {% block title %}Welcome!{% endblock %} - {% block stylesheets %}{% endblock %} - - - - {% block body %}{% endblock %} - {% block javascripts %}{% endblock %} - - - - .. code-block:: html+php - - - - - - - <?php $view['slots']->output('title', 'Welcome!') ?> - output('stylesheets') ?> - - - - output('_content') ?> - output('javascripts') ?> - - - -The base template file defines the HTML layout and renders the ``body`` block -that you defined in the ``index.html.twig`` template. It also renders a ``title`` -block, which you could choose to define in the ``index.html.twig`` template. -Since you did not define the ``title`` block in the child template, it defaults -to "Welcome!". - -Templates are a powerful way to render and organize the content for your -page. A template can render anything, from HTML markup, to CSS code, or anything -else that the controller may need to return. - -In the lifecycle of handling a request, the templating engine is simply -an optional tool. Recall that the goal of each controller is to return a -``Response`` object. Templates are a powerful, but optional, tool for creating -the content for that ``Response`` object. - -.. index:: - single: Directory Structure - -The Directory Structure ------------------------ - -After just a few short sections, you already understand the philosophy behind -creating and rendering pages in Symfony2. You've also already begun to see -how Symfony2 projects are structured and organized. By the end of this section, -you'll know where to find and put different types of files and why. - -Though entirely flexible, by default, each Symfony :term:`application` has -the same basic and recommended directory structure: - -* ``app/``: This directory contains the application configuration; - -* ``src/``: All the project PHP code is stored under this directory; - -* ``vendor/``: Any vendor libraries are placed here by convention; - -* ``web/``: This is the web root directory and contains any publicly accessible files; - -.. _the-web-directory: - -The Web Directory -~~~~~~~~~~~~~~~~~ - -The web root directory is the home of all public and static files including -images, stylesheets, and JavaScript files. It is also where each -:term:`front controller` lives:: - - // web/app.php - require_once __DIR__.'/../app/bootstrap.php.cache'; - require_once __DIR__.'/../app/AppKernel.php'; - - use Symfony\Component\HttpFoundation\Request; - - $kernel = new AppKernel('prod', false); - $kernel->loadClassCache(); - $kernel->handle(Request::createFromGlobals())->send(); - -The front controller file (``app.php`` in this example) is the actual PHP -file that's executed when using a Symfony2 application and its job is to -use a Kernel class, ``AppKernel``, to bootstrap the application. - -.. tip:: - - Having a front controller means different and more flexible URLs than - are used in a typical flat PHP application. When using a front controller, - URLs are formatted in the following way: - - .. code-block:: text - - http://localhost/app.php/hello/Ryan - - The front controller, ``app.php``, is executed and the "internal:" URL - ``/hello/Ryan`` is routed internally using the routing configuration. - By using Apache ``mod_rewrite`` rules, you can force the ``app.php`` file - to be executed without needing to specify it in the URL: - - .. code-block:: text - - http://localhost/hello/Ryan - -Though front controllers are essential in handling every request, you'll -rarely need to modify or even think about them. They'll be mentioned again -briefly in the `Environments`_ section. - -The Application (``app``) Directory -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -As you saw in the front controller, the ``AppKernel`` class is the main entry -point of the application and is responsible for all configuration. As such, -it is stored in the ``app/`` directory. - -This class must implement two methods that define everything that Symfony -needs to know about your application. You don't even need to worry about -these methods when starting - Symfony fills them in for you with sensible -defaults. - -* ``registerBundles()``: Returns an array of all bundles needed to run the - application (see :ref:`page-creation-bundles`); - -* ``registerContainerConfiguration()``: Loads the main application configuration - resource file (see the `Application Configuration`_ section). - -In day-to-day development, you'll mostly use the ``app/`` directory to modify -configuration and routing files in the ``app/config/`` directory (see -`Application Configuration`_). It also contains the application cache -directory (``app/cache``), a log directory (``app/logs``) and a directory -for application-level resource files, such as templates (``app/Resources``). -You'll learn more about each of these directories in later chapters. - -.. _autoloading-introduction-sidebar: - -.. sidebar:: Autoloading - - When Symfony is loading, a special file - ``vendor/autoload.php`` - is - included. This file is created by Composer and will autoload all - application files living in the `src/` folder as well as all - third-party libraries mentioned in the ``composer.json`` file. - - Because of the autoloader, you never need to worry about using ``include`` - or ``require`` statements. Instead, Composer uses the namespace of a class - to determine its location and automatically includes the file on your - behalf the instant you need a class. - - The autoloader is already configured to look in the ``src/`` directory - for any of your PHP classes. For autoloading to work, the class name and - path to the file have to follow the same pattern: - - .. code-block:: text - - Class Name: - Acme\HelloBundle\Controller\HelloController - Path: - src/Acme/HelloBundle/Controller/HelloController.php - -The Source (``src``) Directory -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Put simply, the ``src/`` directory contains all of the actual code (PHP code, -templates, configuration files, stylesheets, etc) that drives *your* application. -When developing, the vast majority of your work will be done inside one or -more bundles that you create in this directory. - -But what exactly is a :term:`bundle`? - -.. _page-creation-bundles: - -The Bundle System ------------------ - -A bundle is similar to a plugin in other software, but even better. The key -difference is that *everything* is a bundle in Symfony2, including both the -core framework functionality and the code written for your application. -Bundles are first-class citizens in Symfony2. This gives you the flexibility -to use pre-built features packaged in `third-party bundles`_ or to distribute -your own bundles. It makes it easy to pick and choose which features to enable -in your application and to optimize them the way you want. - -.. note:: - - While you'll learn the basics here, an entire cookbook entry is devoted - to the organization and best practices of :doc:`bundles`. - -A bundle is simply a structured set of files within a directory that implement -a single feature. You might create a ``BlogBundle``, a ``ForumBundle`` or -a bundle for user management (many of these exist already as open source -bundles). Each directory contains everything related to that feature, including -PHP files, templates, stylesheets, JavaScripts, tests and anything else. -Every aspect of a feature exists in a bundle and every feature lives in a -bundle. - -An application is made up of bundles as defined in the ``registerBundles()`` -method of the ``AppKernel`` class:: - - // app/AppKernel.php - public function registerBundles() - { - $bundles = array( - new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), - new Symfony\Bundle\SecurityBundle\SecurityBundle(), - new Symfony\Bundle\TwigBundle\TwigBundle(), - new Symfony\Bundle\MonologBundle\MonologBundle(), - new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(), - new Symfony\Bundle\DoctrineBundle\DoctrineBundle(), - new Symfony\Bundle\AsseticBundle\AsseticBundle(), - new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), - new JMS\SecurityExtraBundle\JMSSecurityExtraBundle(), - ); - - if (in_array($this->getEnvironment(), array('dev', 'test'))) { - $bundles[] = new Acme\DemoBundle\AcmeDemoBundle(); - $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); - $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle(); - $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle(); - } - - return $bundles; - } - -With the ``registerBundles()`` method, you have total control over which bundles -are used by your application (including the core Symfony bundles). - -.. tip:: - - A bundle can live *anywhere* as long as it can be autoloaded (via the - autoloader configured at ``app/autoload.php``). - -Creating a Bundle -~~~~~~~~~~~~~~~~~ - -The Symfony Standard Edition comes with a handy task that creates a fully-functional -bundle for you. Of course, creating a bundle by hand is pretty easy as well. - -To show you how simple the bundle system is, create a new bundle called -``AcmeTestBundle`` and enable it. - -.. tip:: - - The ``Acme`` portion is just a dummy name that should be replaced by - some "vendor" name that represents you or your organization (e.g. ``ABCTestBundle`` - for some company named ``ABC``). - -Start by creating a ``src/Acme/TestBundle/`` directory and adding a new file -called ``AcmeTestBundle.php``:: - - // src/Acme/TestBundle/AcmeTestBundle.php - namespace Acme\TestBundle; - - use Symfony\Component\HttpKernel\Bundle\Bundle; - - class AcmeTestBundle extends Bundle - { - } - -.. tip:: - - The name ``AcmeTestBundle`` follows the standard :ref:`Bundle naming conventions`. - You could also choose to shorten the name of the bundle to simply ``TestBundle`` - by naming this class ``TestBundle`` (and naming the file ``TestBundle.php``). - -This empty class is the only piece you need to create the new bundle. Though -commonly empty, this class is powerful and can be used to customize the behavior -of the bundle. - -Now that you've created the bundle, enable it via the ``AppKernel`` class:: - - // app/AppKernel.php - public function registerBundles() - { - $bundles = array( - ..., - // register your bundles - new Acme\TestBundle\AcmeTestBundle(), - ); - // ... - - return $bundles; - } - -And while it doesn't do anything yet, ``AcmeTestBundle`` is now ready to -be used. - -And as easy as this is, Symfony also provides a command-line interface for -generating a basic bundle skeleton: - -.. code-block:: bash - - $ php app/console generate:bundle --namespace=Acme/TestBundle - -The bundle skeleton generates with a basic controller, template and routing -resource that can be customized. You'll learn more about Symfony2's command-line -tools later. - -.. tip:: - - Whenever creating a new bundle or using a third-party bundle, always make - sure the bundle has been enabled in ``registerBundles()``. When using - the ``generate:bundle`` command, this is done for you. - -Bundle Directory Structure -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The directory structure of a bundle is simple and flexible. By default, the -bundle system follows a set of conventions that help to keep code consistent -between all Symfony2 bundles. Take a look at ``AcmeHelloBundle``, as it contains -some of the most common elements of a bundle: - -* ``Controller/`` contains the controllers of the bundle (e.g. ``HelloController.php``); - -* ``DependencyInjection/`` holds certain dependency injection extension classes, - which may import service configuration, register compiler passes or more - (this directory is not necessary); - -* ``Resources/config/`` houses configuration, including routing configuration - (e.g. ``routing.yml``); - -* ``Resources/views/`` holds templates organized by controller name (e.g. - ``Hello/index.html.twig``); - -* ``Resources/public/`` contains web assets (images, stylesheets, etc) and is - copied or symbolically linked into the project ``web/`` directory via - the ``assets:install`` console command; - -* ``Tests/`` holds all tests for the bundle. - -A bundle can be as small or large as the feature it implements. It contains -only the files you need and nothing else. - -As you move through the book, you'll learn how to persist objects to a database, -create and validate forms, create translations for your application, write -tests and much more. Each of these has their own place and role within the -bundle. - -Application Configuration -------------------------- - -An application consists of a collection of bundles representing all of the -features and capabilities of your application. Each bundle can be customized -via configuration files written in YAML, XML or PHP. By default, the main -configuration file lives in the ``app/config/`` directory and is called -either ``config.yml``, ``config.xml`` or ``config.php`` depending on which -format you prefer: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - imports: - - { resource: parameters.yml } - - { resource: security.yml } - - framework: - secret: "%secret%" - router: { resource: "%kernel.root_dir%/config/routing.yml" } - # ... - - # Twig Configuration - twig: - debug: "%kernel.debug%" - strict_variables: "%kernel.debug%" - - # ... - - .. code-block:: xml - - - - - - - - - - - - - - - - - - .. code-block:: php - - $this->import('parameters.yml'); - $this->import('security.yml'); - - $container->loadFromExtension('framework', array( - 'secret' => '%secret%', - 'router' => array('resource' => '%kernel.root_dir%/config/routing.php'), - // ... - ), - )); - - // Twig Configuration - $container->loadFromExtension('twig', array( - 'debug' => '%kernel.debug%', - 'strict_variables' => '%kernel.debug%', - )); - - // ... - -.. note:: - - You'll learn exactly how to load each file/format in the next section - `Environments`_. - -Each top-level entry like ``framework`` or ``twig`` defines the configuration -for a particular bundle. For example, the ``framework`` key defines the configuration -for the core Symfony ``FrameworkBundle`` and includes configuration for the -routing, templating, and other core systems. - -For now, don't worry about the specific configuration options in each section. -The configuration file ships with sensible defaults. As you read more and -explore each part of Symfony2, you'll learn about the specific configuration -options of each feature. - -.. sidebar:: Configuration Formats - - Throughout the chapters, all configuration examples will be shown in all - three formats (YAML, XML and PHP). Each has its own advantages and - disadvantages. The choice of which to use is up to you: - - * *YAML*: Simple, clean and readable (learn more about yaml in - ":doc:`/components/yaml/yaml_format`"); - - * *XML*: More powerful than YAML at times and supports IDE autocompletion; - - * *PHP*: Very powerful but less readable than standard configuration formats. - -Default Configuration Dump -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.1 - The ``config:dump-reference`` command was added in Symfony 2.1 - -You can dump the default configuration for a bundle in yaml to the console using -the ``config:dump-reference`` command. Here is an example of dumping the default -FrameworkBundle configuration: - -.. code-block:: text - - app/console config:dump-reference FrameworkBundle - -The extension alias (configuration key) can also be used: - -.. code-block:: text - - app/console config:dump-reference framework - -.. note:: - - See the cookbook article: :doc:`How to expose a Semantic Configuration for - a Bundle` for information on adding - configuration for your own bundle. - -.. index:: - single: Environments; Introduction - -.. _environments-summary: - -Environments ------------- - -An application can run in various environments. The different environments -share the same PHP code (apart from the front controller), but use different -configuration. For instance, a ``dev`` environment will log warnings and -errors, while a ``prod`` environment will only log errors. Some files are -rebuilt on each request in the ``dev`` environment (for the developer's convenience), -but cached in the ``prod`` environment. All environments live together on -the same machine and execute the same application. - -A Symfony2 project generally begins with three environments (``dev``, ``test`` -and ``prod``), though creating new environments is easy. You can view your -application in different environments simply by changing the front controller -in your browser. To see the application in the ``dev`` environment, access -the application via the development front controller: - -.. code-block:: text - - http://localhost/app_dev.php/hello/Ryan - -If you'd like to see how your application will behave in the production environment, -call the ``prod`` front controller instead: - -.. code-block:: text - - http://localhost/app.php/hello/Ryan - -Since the ``prod`` environment is optimized for speed; the configuration, -routing and Twig templates are compiled into flat PHP classes and cached. -When viewing changes in the ``prod`` environment, you'll need to clear these -cached files and allow them to rebuild: - -.. code-block:: bash - - $ php app/console cache:clear --env=prod --no-debug - -.. note:: - - If you open the ``web/app.php`` file, you'll find that it's configured explicitly - to use the ``prod`` environment:: - - $kernel = new AppKernel('prod', false); - - You can create a new front controller for a new environment by copying - this file and changing ``prod`` to some other value. - -.. note:: - - The ``test`` environment is used when running automated tests and cannot - be accessed directly through the browser. See the :doc:`testing chapter` - for more details. - -.. index:: - single: Environments; Configuration - -Environment Configuration -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``AppKernel`` class is responsible for actually loading the configuration -file of your choice:: - - // app/AppKernel.php - public function registerContainerConfiguration(LoaderInterface $loader) - { - $loader->load( - __DIR__.'/config/config_'.$this->getEnvironment().'.yml' - ); - } - -You already know that the ``.yml`` extension can be changed to ``.xml`` or -``.php`` if you prefer to use either XML or PHP to write your configuration. -Notice also that each environment loads its own configuration file. Consider -the configuration file for the ``dev`` environment. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_dev.yml - imports: - - { resource: config.yml } - - framework: - router: { resource: "%kernel.root_dir%/config/routing_dev.yml" } - profiler: { only_exceptions: false } - - # ... - - .. code-block:: xml - - - - - - - - - - - - - - .. code-block:: php - - // app/config/config_dev.php - $loader->import('config.php'); - - $container->loadFromExtension('framework', array( - 'router' => array('resource' => '%kernel.root_dir%/config/routing_dev.php'), - 'profiler' => array('only-exceptions' => false), - )); - - // ... - -The ``imports`` key is similar to a PHP ``include`` statement and guarantees -that the main configuration file (``config.yml``) is loaded first. The rest -of the file tweaks the default configuration for increased logging and other -settings conducive to a development environment. - -Both the ``prod`` and ``test`` environments follow the same model: each environment -imports the base configuration file and then modifies its configuration values -to fit the needs of the specific environment. This is just a convention, -but one that allows you to reuse most of your configuration and customize -just pieces of it between environments. - -Summary -------- - -Congratulations! You've now seen every fundamental aspect of Symfony2 and have -hopefully discovered how easy and flexible it can be. And while there are -*a lot* of features still to come, be sure to keep the following basic points -in mind: - -* Creating a page is a three-step process involving a **route**, a **controller** - and (optionally) a **template**; - -* Each project contains just a few main directories: ``web/`` (web assets and - the front controllers), ``app/`` (configuration), ``src/`` (your bundles), - and ``vendor/`` (third-party code) (there's also a ``bin/`` directory that's - used to help updated vendor libraries); - -* Each feature in Symfony2 (including the Symfony2 framework core) is organized - into a *bundle*, which is a structured set of files for that feature; - -* The **configuration** for each bundle lives in the ``Resources/config`` - directory of the bundle and can be specified in YAML, XML or PHP; - -* The global **application configuration** lives in the ``app/config`` - directory; - -* Each **environment** is accessible via a different front controller (e.g. - ``app.php`` and ``app_dev.php``) and loads a different configuration file. - -From here, each chapter will introduce you to more and more powerful tools -and advanced concepts. The more you know about Symfony2, the more you'll -appreciate the flexibility of its architecture and the power it gives you -to rapidly develop applications. - -.. _`Twig`: http://twig.sensiolabs.org -.. _`third-party bundles`: http://knpbundles.com -.. _`Symfony Standard Edition`: http://symfony.com/download -.. _`Apache's DirectoryIndex documentation`: http://httpd.apache.org/docs/2.0/mod/mod_dir.html -.. _`Nginx HttpCoreModule location documentation`: http://wiki.nginx.org/HttpCoreModule#location diff --git a/book/performance.rst b/book/performance.rst deleted file mode 100644 index 32720e3a2e5..00000000000 --- a/book/performance.rst +++ /dev/null @@ -1,142 +0,0 @@ -.. index:: - single: Tests - -Performance -=========== - -Symfony2 is fast, right out of the box. Of course, if you really need speed, -there are many ways that you can make Symfony even faster. In this chapter, -you'll explore many of the most common and powerful ways to make your Symfony -application even faster. - -.. index:: - single: Performance; Byte code cache - -Use a Byte Code Cache (e.g. APC) --------------------------------- - -One of the best (and easiest) things that you should do to improve your performance -is to use a "byte code cache". The idea of a byte code cache is to remove -the need to constantly recompile the PHP source code. There are a number of -`byte code caches`_ available, some of which are open source. The most widely -used byte code cache is probably `APC`_ - -Using a byte code cache really has no downside, and Symfony2 has been architected -to perform really well in this type of environment. - -Further Optimizations -~~~~~~~~~~~~~~~~~~~~~ - -Byte code caches usually monitor the source files for changes. This ensures -that if the source of a file changes, the byte code is recompiled automatically. -This is really convenient, but obviously adds overhead. - -For this reason, some byte code caches offer an option to disable these checks. -Obviously, when disabling these checks, it will be up to the server admin -to ensure that the cache is cleared whenever any source files change. Otherwise, -the updates you've made won't be seen. - -For example, to disable these checks in APC, simply add ``apc.stat=0`` to -your php.ini configuration. - -.. index:: - single: Performance; Autoloader - -Use Composer's Class Map Functionality --------------------------------------- - -By default, the Symfony2 standard edition uses Composer's autoloader -in the `autoload.php`_ file. This autoloader is easy to use, as it will -automatically find any new classes that you've placed in the registered -directories. - -Unfortunately, this comes at a cost, as the loader iterates over all configured -namespaces to find a particular file, making ``file_exists`` calls until it -finally finds the file it's looking for. - -The simplest solution is to tell Composer to build a "class map" (i.e. a -big array of the locations of all the classes). This can be done from the -command line, and might become part of your deploy process: - -.. code-block:: bash - - php composer.phar dump-autoload --optimize - -Internally, this builds the big class map array in ``vendor/composer/autoload_classmap.php``. - -Caching the Autoloader with APC -------------------------------- - -Another solution is to to cache the location of each class after it's located -the first time. Symfony comes with a class - :class:`Symfony\\Component\\ClassLoader\\ApcClassLoader` - -that does exactly this. To use it, just adapt your front controller file. -If you're using the Standard Distribution, this code should already be available -as comments in this file:: - - // app.php - // ... - - $loader = require_once __DIR__.'/../app/bootstrap.php.cache'; - - // Use APC for autoloading to improve performance - // Change 'sf2' by the prefix you want in order to prevent key conflict with another application - /* - $loader = new ApcClassLoader('sf2', $loader); - $loader->register(true); - */ - - // ... - -.. note:: - - When using the APC autoloader, if you add new classes, they will be found - automatically and everything will work the same as before (i.e. no - reason to "clear" the cache). However, if you change the location of a - particular namespace or prefix, you'll need to flush your APC cache. Otherwise, - the autoloader will still be looking at the old location for all classes - inside that namespace. - -.. index:: - single: Performance; Bootstrap files - -Use Bootstrap Files -------------------- - -To ensure optimal flexibility and code reuse, Symfony2 applications leverage -a variety of classes and 3rd party components. But loading all of these classes -from separate files on each request can result in some overhead. To reduce -this overhead, the Symfony2 Standard Edition provides a script to generate -a so-called `bootstrap file`_, consisting of multiple classes definitions -in a single file. By including this file (which contains a copy of many of -the core classes), Symfony no longer needs to include any of the source files -containing those classes. This will reduce disc IO quite a bit. - -If you're using the Symfony2 Standard Edition, then you're probably already -using the bootstrap file. To be sure, open your front controller (usually -``app.php``) and check to make sure that the following line exists:: - - require_once __DIR__.'/../app/bootstrap.php.cache'; - -Note that there are two disadvantages when using a bootstrap file: - -* the file needs to be regenerated whenever any of the original sources change - (i.e. when you update the Symfony2 source or vendor libraries); - -* when debugging, one will need to place break points inside the bootstrap file. - -If you're using Symfony2 Standard Edition, the bootstrap file is automatically -rebuilt after updating the vendor libraries via the ``php composer.phar install`` -command. - -Bootstrap Files and Byte Code Caches -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Even when using a byte code cache, performance will improve when using a bootstrap -file since there will be fewer files to monitor for changes. Of course if this -feature is disabled in the byte code cache (e.g. ``apc.stat=0`` in APC), there -is no longer a reason to use a bootstrap file. - -.. _`byte code caches`: http://en.wikipedia.org/wiki/List_of_PHP_accelerators -.. _`APC`: http://php.net/manual/en/book.apc.php -.. _`autoload.php`: https://github.com/symfony/symfony-standard/blob/master/app/autoload.php -.. _`bootstrap file`: https://github.com/sensio/SensioDistributionBundle/blob/master/Composer/ScriptHandler.php diff --git a/book/propel.rst b/book/propel.rst deleted file mode 100644 index bfc51a772a1..00000000000 --- a/book/propel.rst +++ /dev/null @@ -1,459 +0,0 @@ -.. index:: - single: Propel - -Databases and Propel -==================== - -One of the most common and challenging tasks for any application -involves persisting and reading information to and from a database. Symfony2 -does not come integrated with any ORMs but the Propel integration is easy. -To install Propel, read `Working With Symfony2`_ on the Propel documentation. - -A Simple Example: A Product ---------------------------- - -In this section, you'll configure your database, create a ``Product`` object, -persist it to the database and fetch it back out. - -.. sidebar:: Code along with the example - - If you want to follow along with the example in this chapter, create an - ``AcmeStoreBundle`` via: - - .. code-block:: bash - - $ php app/console generate:bundle --namespace=Acme/StoreBundle - -Configuring the Database -~~~~~~~~~~~~~~~~~~~~~~~~ - -Before you can start, you'll need to configure your database connection -information. By convention, this information is usually configured in an -``app/config/parameters.yml`` file: - -.. code-block:: yaml - - # app/config/parameters.yml - parameters: - database_driver: mysql - database_host: localhost - database_name: test_project - database_user: root - database_password: password - database_charset: UTF8 - -.. note:: - - Defining the configuration via ``parameters.yml`` is just a convention. The - parameters defined in that file are referenced by the main configuration - file when setting up Propel: - -These parameters defined in ``parameters.yml`` can now be included in the -configuration file (``config.yml``): - -.. code-block:: yaml - - propel: - dbal: - driver: "%database_driver%" - user: "%database_user%" - password: "%database_password%" - dsn: "%database_driver%:host=%database_host%;dbname=%database_name%;charset=%database_charset%" - -Now that Propel knows about your database, Symfony2 can create the database for -you: - -.. code-block:: bash - - $ php app/console propel:database:create - -.. note:: - - In this example, you have one configured connection, named ``default``. If - you want to configure more than one connection, read the `PropelBundle - configuration section`_. - -Creating a Model Class -~~~~~~~~~~~~~~~~~~~~~~ - -In the Propel world, ActiveRecord classes are known as **models** because classes -generated by Propel contain some business logic. - -.. note:: - - For people who use Symfony2 with Doctrine2, **models** are equivalent to - **entities**. - -Suppose you're building an application where products need to be displayed. -First, create a ``schema.xml`` file inside the ``Resources/config`` directory -of your ``AcmeStoreBundle``: - -.. code-block:: xml - - - - - - - - -
-
- -Building the Model -~~~~~~~~~~~~~~~~~~ - -After creating your ``schema.xml``, generate your model from it by running: - -.. code-block:: bash - - $ php app/console propel:model:build - -This generates each model class to quickly develop your application in the -``Model/`` directory the ``AcmeStoreBundle`` bundle. - -Creating the Database Tables/Schema -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Now you have a usable ``Product`` class and all you need to persist it. Of -course, you don't yet have the corresponding ``product`` table in your -database. Fortunately, Propel can automatically create all the database tables -needed for every known model in your application. To do this, run: - -.. code-block:: bash - - $ php app/console propel:sql:build - $ php app/console propel:sql:insert --force - -Your database now has a fully-functional ``product`` table with columns that -match the schema you've specified. - -.. tip:: - - You can run the last three commands combined by using the following - command: ``php app/console propel:build --insert-sql``. - -Persisting Objects to the Database -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Now that you have a ``Product`` object and corresponding ``product`` table, -you're ready to persist data to the database. From inside a controller, this -is pretty easy. Add the following method to the ``DefaultController`` of the -bundle:: - - // src/Acme/StoreBundle/Controller/DefaultController.php - - // ... - use Acme\StoreBundle\Model\Product; - use Symfony\Component\HttpFoundation\Response; - - public function createAction() - { - $product = new Product(); - $product->setName('A Foo Bar'); - $product->setPrice(19.99); - $product->setDescription('Lorem ipsum dolor'); - - $product->save(); - - return new Response('Created product id '.$product->getId()); - } - -In this piece of code, you instantiate and work with the ``$product`` object. -When you call the ``save()`` method on it, you persist it to the database. No -need to use other services, the object knows how to persist itself. - -.. note:: - - If you're following along with this example, you'll need to create a - :doc:`route ` that points to this action to see it in action. - -Fetching Objects from the Database -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Fetching an object back from the database is even easier. For example, suppose -you've configured a route to display a specific ``Product`` based on its ``id`` -value:: - - // ... - use Acme\StoreBundle\Model\ProductQuery; - - public function showAction($id) - { - $product = ProductQuery::create() - ->findPk($id); - - if (!$product) { - throw $this->createNotFoundException( - 'No product found for id '.$id - ); - } - - // ... do something, like pass the $product object into a template - } - -Updating an Object -~~~~~~~~~~~~~~~~~~ - -Once you've fetched an object from Propel, updating it is easy. Suppose you -have a route that maps a product id to an update action in a controller:: - - // ... - use Acme\StoreBundle\Model\ProductQuery; - - public function updateAction($id) - { - $product = ProductQuery::create() - ->findPk($id); - - if (!$product) { - throw $this->createNotFoundException( - 'No product found for id '.$id - ); - } - - $product->setName('New product name!'); - $product->save(); - - return $this->redirect($this->generateUrl('homepage')); - } - -Updating an object involves just three steps: - -#. fetching the object from Propel (line 6 - 13); -#. modifying the object (line 15); -#. saving it (line 16). - -Deleting an Object -~~~~~~~~~~~~~~~~~~ - -Deleting an object is very similar to updating, but requires a call to the -``delete()`` method on the object:: - - $product->delete(); - -Querying for Objects --------------------- - -Propel provides generated ``Query`` classes to run both basic and complex queries -without any work:: - - \Acme\StoreBundle\Model\ProductQuery::create()->findPk($id); - - \Acme\StoreBundle\Model\ProductQuery::create() - ->filterByName('Foo') - ->findOne(); - -Imagine that you want to query for products which cost more than 19.99, ordered -from cheapest to most expensive. From inside a controller, do the following:: - - $products = \Acme\StoreBundle\Model\ProductQuery::create() - ->filterByPrice(array('min' => 19.99)) - ->orderByPrice() - ->find(); - -In one line, you get your products in a powerful oriented object way. No need -to waste your time with SQL or whatever, Symfony2 offers fully object oriented -programming and Propel respects the same philosophy by providing an awesome -abstraction layer. - -If you want to reuse some queries, you can add your own methods to the -``ProductQuery`` class:: - - // src/Acme/StoreBundle/Model/ProductQuery.php - class ProductQuery extends BaseProductQuery - { - public function filterByExpensivePrice() - { - return $this - ->filterByPrice(array('min' => 1000)); - } - } - -But note that Propel generates a lot of methods for you and a simple -``findAllOrderedByName()`` can be written without any effort:: - - \Acme\StoreBundle\Model\ProductQuery::create() - ->orderByName() - ->find(); - -Relationships/Associations --------------------------- - -Suppose that the products in your application all belong to exactly one -"category". In this case, you'll need a ``Category`` object and a way to relate -a ``Product`` object to a ``Category`` object. - -Start by adding the ``category`` definition in your ``schema.xml``: - -.. code-block:: xml - - - - - - - - - - - - -
- - - - -
-
- -Create the classes: - -.. code-block:: bash - - $ php app/console propel:model:build - -Assuming you have products in your database, you don't want lose them. Thanks to -migrations, Propel will be able to update your database without losing existing -data. - -.. code-block:: bash - - $ php app/console propel:migration:generate-diff - $ php app/console propel:migration:migrate - -Your database has been updated, you can continue to write your application. - -Saving Related Objects -~~~~~~~~~~~~~~~~~~~~~~ - -Now, try the code in action. Imagine you're inside a controller:: - - // ... - use Acme\StoreBundle\Model\Category; - use Acme\StoreBundle\Model\Product; - use Symfony\Component\HttpFoundation\Response; - - class DefaultController extends Controller - { - public function createProductAction() - { - $category = new Category(); - $category->setName('Main Products'); - - $product = new Product(); - $product->setName('Foo'); - $product->setPrice(19.99); - // relate this product to the category - $product->setCategory($category); - - // save the whole - $product->save(); - - return new Response( - 'Created product id: '.$product->getId().' and category id: '.$category->getId() - ); - } - } - -Now, a single row is added to both the ``category`` and product tables. The -``product.category_id`` column for the new product is set to whatever the id is -of the new category. Propel manages the persistence of this relationship for -you. - -Fetching Related Objects -~~~~~~~~~~~~~~~~~~~~~~~~ - -When you need to fetch associated objects, your workflow looks just like it did -before. First, fetch a ``$product`` object and then access its related -``Category``:: - - // ... - use Acme\StoreBundle\Model\ProductQuery; - - public function showAction($id) - { - $product = ProductQuery::create() - ->joinWithCategory() - ->findPk($id); - - $categoryName = $product->getCategory()->getName(); - - // ... - } - -Note, in the above example, only one query was made. - -More information on Associations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You will find more information on relations by reading the dedicated chapter on -`Relationships`_. - -Lifecycle Callbacks -------------------- - -Sometimes, you need to perform an action right before or after an object is -inserted, updated, or deleted. These types of actions are known as "lifecycle" -callbacks or "hooks", as they're callback methods that you need to execute -during different stages of the lifecycle of an object (e.g. the object is -inserted, updated, deleted, etc). - -To add a hook, just add a new method to the object class:: - - // src/Acme/StoreBundle/Model/Product.php - - // ... - class Product extends BaseProduct - { - public function preInsert(\PropelPDO $con = null) - { - // do something before the object is inserted - } - } - -Propel provides the following hooks: - -* ``preInsert()`` code executed before insertion of a new object -* ``postInsert()`` code executed after insertion of a new object -* ``preUpdate()`` code executed before update of an existing object -* ``postUpdate()`` code executed after update of an existing object -* ``preSave()`` code executed before saving an object (new or existing) -* ``postSave()`` code executed after saving an object (new or existing) -* ``preDelete()`` code executed before deleting an object -* ``postDelete()`` code executed after deleting an object - - -Behaviors ---------- - -All bundled behaviors in Propel are working with Symfony2. To get more -information about how to use Propel behaviors, look at the `Behaviors reference -section`_. - -Commands --------- - -You should read the dedicated section for `Propel commands in Symfony2`_. - -.. _`Working With Symfony2`: http://propelorm.org/cookbook/symfony2/working-with-symfony2.html#installation -.. _`PropelBundle configuration section`: http://propelorm.org/cookbook/symfony2/working-with-symfony2.html#configuration -.. _`Relationships`: http://propelorm.org/documentation/04-relationships.html -.. _`Behaviors reference section`: http://propelorm.org/documentation/#behaviors_reference -.. _`Propel commands in Symfony2`: http://propelorm.org/cookbook/symfony2/working-with-symfony2#the_commands diff --git a/book/routing.rst b/book/routing.rst deleted file mode 100644 index 09ebf2d94d8..00000000000 --- a/book/routing.rst +++ /dev/null @@ -1,1249 +0,0 @@ -.. index:: - single: Routing - -Routing -======= - -Beautiful URLs are an absolute must for any serious web application. This -means leaving behind ugly URLs like ``index.php?article_id=57`` in favor -of something like ``/read/intro-to-symfony``. - -Having flexibility is even more important. What if you need to change the -URL of a page from ``/blog`` to ``/news``? How many links should you need to -hunt down and update to make the change? If you're using Symfony's router, -the change is simple. - -The Symfony2 router lets you define creative URLs that you map to different -areas of your application. By the end of this chapter, you'll be able to: - -* Create complex routes that map to controllers -* Generate URLs inside templates and controllers -* Load routing resources from bundles (or anywhere else) -* Debug your routes - -.. index:: - single: Routing; Basics - -Routing in Action ------------------ - -A *route* is a map from a URL path to a controller. For example, suppose -you want to match any URL like ``/blog/my-post`` or ``/blog/all-about-symfony`` -and send it to a controller that can look up and render that blog entry. -The route is simple: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - blog_show: - path: /blog/{slug} - defaults: { _controller: AcmeBlogBundle:Blog:show } - - .. code-block:: xml - - - - - - - AcmeBlogBundle:Blog:show - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('blog_show', new Route('/blog/{slug}', array( - '_controller' => 'AcmeBlogBundle:Blog:show', - ))); - - return $collection; - -.. versionadded:: 2.2 - The ``path`` option is new in Symfony2.2, ``pattern`` is used in older - versions. - -The path defined by the ``blog_show`` route acts like ``/blog/*`` where -the wildcard is given the name ``slug``. For the URL ``/blog/my-blog-post``, -the ``slug`` variable gets a value of ``my-blog-post``, which is available -for you to use in your controller (keep reading). - -The ``_controller`` parameter is a special key that tells Symfony which controller -should be executed when a URL matches this route. The ``_controller`` string -is called the :ref:`logical name`. It follows a -pattern that points to a specific PHP class and method:: - - // src/Acme/BlogBundle/Controller/BlogController.php - namespace Acme\BlogBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - - class BlogController extends Controller - { - public function showAction($slug) - { - // use the $slug variable to query the database - $blog = ...; - - return $this->render('AcmeBlogBundle:Blog:show.html.twig', array( - 'blog' => $blog, - )); - } - } - -Congratulations! You've just created your first route and connected it to -a controller. Now, when you visit ``/blog/my-post``, the ``showAction`` controller -will be executed and the ``$slug`` variable will be equal to ``my-post``. - -This is the goal of the Symfony2 router: to map the URL of a request to a -controller. Along the way, you'll learn all sorts of tricks that make mapping -even the most complex URLs easy. - -.. index:: - single: Routing; Under the hood - -Routing: Under the Hood ------------------------ - -When a request is made to your application, it contains an address to the -exact "resource" that the client is requesting. This address is called the -URL, (or URI), and could be ``/contact``, ``/blog/read-me``, or anything -else. Take the following HTTP request for example: - -.. code-block:: text - - GET /blog/my-blog-post - -The goal of the Symfony2 routing system is to parse this URL and determine -which controller should be executed. The whole process looks like this: - -#. The request is handled by the Symfony2 front controller (e.g. ``app.php``); - -#. The Symfony2 core (i.e. Kernel) asks the router to inspect the request; - -#. The router matches the incoming URL to a specific route and returns information - about the route, including the controller that should be executed; - -#. The Symfony2 Kernel executes the controller, which ultimately returns - a ``Response`` object. - -.. figure:: /images/request-flow.png - :align: center - :alt: Symfony2 request flow - - The routing layer is a tool that translates the incoming URL into a specific - controller to execute. - -.. index:: - single: Routing; Creating routes - -Creating Routes ---------------- - -Symfony loads all the routes for your application from a single routing configuration -file. The file is usually ``app/config/routing.yml``, but can be configured -to be anything (including an XML or PHP file) via the application configuration -file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - # ... - router: { resource: "%kernel.root_dir%/config/routing.yml" } - - .. code-block:: xml - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - // ... - 'router' => array('resource' => '%kernel.root_dir%/config/routing.php'), - )); - -.. tip:: - - Even though all routes are loaded from a single file, it's common practice - to include additional routing resources. To do so, just point out in the - main routing configuration file which external files should be included. - See the :ref:`routing-include-external-resources` section for more - information. - -Basic Route Configuration -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Defining a route is easy, and a typical application will have lots of routes. -A basic route consists of just two parts: the ``path`` to match and a -``defaults`` array: - -.. configuration-block:: - - .. code-block:: yaml - - _welcome: - path: / - defaults: { _controller: AcmeDemoBundle:Main:homepage } - - .. code-block:: xml - - - - - - - AcmeDemoBundle:Main:homepage - - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('_welcome', new Route('/', array( - '_controller' => 'AcmeDemoBundle:Main:homepage', - ))); - - return $collection; - -This route matches the homepage (``/``) and maps it to the ``AcmeDemoBundle:Main:homepage`` -controller. The ``_controller`` string is translated by Symfony2 into an -actual PHP function and executed. That process will be explained shortly -in the :ref:`controller-string-syntax` section. - -.. index:: - single: Routing; Placeholders - -Routing with Placeholders -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Of course the routing system supports much more interesting routes. Many -routes will contain one or more named "wildcard" placeholders: - -.. configuration-block:: - - .. code-block:: yaml - - blog_show: - path: /blog/{slug} - defaults: { _controller: AcmeBlogBundle:Blog:show } - - .. code-block:: xml - - - - - - - AcmeBlogBundle:Blog:show - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('blog_show', new Route('/blog/{slug}', array( - '_controller' => 'AcmeBlogBundle:Blog:show', - ))); - - return $collection; - -The path will match anything that looks like ``/blog/*``. Even better, -the value matching the ``{slug}`` placeholder will be available inside your -controller. In other words, if the URL is ``/blog/hello-world``, a ``$slug`` -variable, with a value of ``hello-world``, will be available in the controller. -This can be used, for example, to load the blog post matching that string. - -The path will *not*, however, match simply ``/blog``. That's because, -by default, all placeholders are required. This can be changed by adding -a placeholder value to the ``defaults`` array. - -Required and 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:: yaml - - blog: - path: /blog - defaults: { _controller: AcmeBlogBundle:Blog:index } - - .. code-block:: xml - - - - - - - AcmeBlogBundle:Blog:index - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('blog', new Route('/blog', array( - '_controller' => 'AcmeBlogBundle:Blog:index', - ))); - - return $collection; - -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:: yaml - - blog: - path: /blog/{page} - defaults: { _controller: AcmeBlogBundle:Blog:index } - - .. code-block:: xml - - - - - - - AcmeBlogBundle:Blog:index - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('blog', new Route('/blog/{page}', array( - '_controller' => 'AcmeBlogBundle:Blog:index', - ))); - - return $collection; - -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 simply ``/blog``. 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:: yaml - - blog: - path: /blog/{page} - defaults: { _controller: AcmeBlogBundle:Blog:index, page: 1 } - - .. code-block:: xml - - - - - - - AcmeBlogBundle:Blog:index - 1 - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('blog', new Route('/blog/{page}', array( - '_controller' => 'AcmeBlogBundle:Blog:index', - 'page' => 1, - ))); - - return $collection; - -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. - -+---------+------------+ -| /blog | {page} = 1 | -+---------+------------+ -| /blog/1 | {page} = 1 | -+---------+------------+ -| /blog/2 | {page} = 2 | -+---------+------------+ - -.. 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). - -.. index:: - single: Routing; Requirements - -Adding Requirements -~~~~~~~~~~~~~~~~~~~ - -Take a quick look at the routes that have been created so far: - -.. configuration-block:: - - .. code-block:: yaml - - blog: - path: /blog/{page} - defaults: { _controller: AcmeBlogBundle:Blog:index, page: 1 } - - blog_show: - path: /blog/{slug} - defaults: { _controller: AcmeBlogBundle:Blog:show } - - .. code-block:: xml - - - - - - - AcmeBlogBundle:Blog:index - 1 - - - - AcmeBlogBundle:Blog:show - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('blog', new Route('/blog/{page}', array( - '_controller' => 'AcmeBlogBundle:Blog:index', - 'page' => 1, - ))); - - $collection->add('blog_show', new Route('/blog/{show}', array( - '_controller' => 'AcmeBlogBundle:Blog:show', - ))); - - return $collection; - -Can you spot the problem? Notice that both routes have patterns that match -URL's that look like ``/blog/*``. The Symfony router will always choose the -**first** matching route it finds. In other words, the ``blog_show`` route -will *never* be matched. Instead, a URL like ``/blog/my-blog-post`` will match -the first route (``blog``) and return a nonsense value of ``my-blog-post`` -to the ``{page}`` parameter. - -+--------------------+-------+-----------------------+ -| URL | route | parameters | -+====================+=======+=======================+ -| /blog/2 | blog | {page} = 2 | -+--------------------+-------+-----------------------+ -| /blog/my-blog-post | blog | {page} = my-blog-post | -+--------------------+-------+-----------------------+ - -The answer to the problem is to add route *requirements*. The routes in this -example would work perfectly if the ``/blog/{page}`` path *only* matched -URLs where the ``{page}`` portion is an integer. Fortunately, regular expression -requirements can easily be added for each parameter. For example: - -.. configuration-block:: - - .. code-block:: yaml - - blog: - path: /blog/{page} - defaults: { _controller: AcmeBlogBundle:Blog:index, page: 1 } - requirements: - page: \d+ - - .. code-block:: xml - - - - - - - AcmeBlogBundle:Blog:index - 1 - \d+ - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('blog', new Route('/blog/{page}', array( - '_controller' => 'AcmeBlogBundle:Blog:index', - 'page' => 1, - ), array( - 'page' => '\d+', - ))); - - return $collection; - -The ``\d+`` requirement is a regular expression that says that the value of -the ``{page}`` parameter must be a digit (i.e. a number). The ``blog`` route -will still match on a URL like ``/blog/2`` (because 2 is a number), but it -will no longer match a URL like ``/blog/my-blog-post`` (because ``my-blog-post`` -is *not* a number). - -As a result, a URL like ``/blog/my-blog-post`` will now properly match the -``blog_show`` route. - -+--------------------+-----------+-----------------------+ -| URL | route | parameters | -+====================+===========+=======================+ -| /blog/2 | blog | {page} = 2 | -+--------------------+-----------+-----------------------+ -| /blog/my-blog-post | blog_show | {slug} = my-blog-post | -+--------------------+-----------+-----------------------+ - -.. sidebar:: Earlier Routes always Win - - What this all means is that the order of the routes is very important. - If the ``blog_show`` route were placed above the ``blog`` route, the - URL ``/blog/2`` would match ``blog_show`` instead of ``blog`` since the - ``{slug}`` parameter of ``blog_show`` has no requirements. By using proper - ordering and clever requirements, you can accomplish just about anything. - -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:: yaml - - homepage: - path: /{culture} - defaults: { _controller: AcmeDemoBundle:Main:homepage, culture: en } - requirements: - culture: en|fr - - .. code-block:: xml - - - - - - - AcmeDemoBundle:Main:homepage - en - en|fr - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('homepage', new Route('/{culture}', array( - '_controller' => 'AcmeDemoBundle:Main:homepage', - 'culture' => 'en', - ), array( - 'culture' => 'en|fr', - ))); - - return $collection; - -For incoming requests, the ``{culture}`` portion of the URL is matched against -the regular expression ``(en|fr)``. - -+-----+--------------------------+ -| / | {culture} = en | -+-----+--------------------------+ -| /en | {culture} = en | -+-----+--------------------------+ -| /fr | {culture} = fr | -+-----+--------------------------+ -| /es | *won't match this route* | -+-----+--------------------------+ - -.. index:: - single: 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 have a contact form -with two controllers - one for displaying the form (on a GET request) and one -for processing the form when it's submitted (on a POST request). This can -be accomplished with the following route configuration: - -.. configuration-block:: - - .. code-block:: yaml - - contact: - path: /contact - defaults: { _controller: AcmeDemoBundle:Main:contact } - methods: [GET] - - contact_process: - path: /contact - defaults: { _controller: AcmeDemoBundle:Main:contactProcess } - methods: [POST] - - .. code-block:: xml - - - - - - - AcmeDemoBundle:Main:contact - - - - AcmeDemoBundle:Main:contactProcess - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('contact', new Route('/contact', array( - '_controller' => 'AcmeDemoBundle:Main:contact', - ), array(), array(), '', array(), array('GET'))); - - $collection->add('contact_process', new Route('/contact', array( - '_controller' => 'AcmeDemoBundle:Main:contactProcess', - ), array(), array(), '', array(), array('POST'))); - - return $collection; - -.. versionadded:: 2.2 - The ``methods`` option is added in Symfony2.2. Use the ``_method`` - requirement in older versions. - -Despite the fact that these two routes have identical paths (``/contact``), -the first route will match only GET requests and the second route will match -only POST requests. This means that you can display the form and submit the -form via the same URL, while using distinct controllers for the two actions. - -.. note:: - - If no ``methods`` are specified, the route will match on *all* methods. - -Adding a Host -~~~~~~~~~~~~~ - -.. versionadded:: 2.2 - Host matching support was added in Symfony 2.2 - -You can also match on the HTTP *host* of the incoming request. For more -information, see :doc:`/components/routing/hostname_pattern` in the Routing -component documentation. - -.. index:: - single: Routing; Advanced example - single: Routing; _format parameter - -.. _advanced-routing-example: - -Advanced Routing Example -~~~~~~~~~~~~~~~~~~~~~~~~ - -At this point, you have everything you need to create a powerful routing -structure in Symfony. The following is an example of just how flexible the -routing system can be: - -.. configuration-block:: - - .. code-block:: yaml - - article_show: - path: /articles/{culture}/{year}/{title}.{_format} - defaults: { _controller: AcmeDemoBundle:Article:show, _format: html } - requirements: - culture: en|fr - _format: html|rss - year: \d+ - - .. code-block:: xml - - - - - - - AcmeDemoBundle:Article:show - html - en|fr - html|rss - \d+ - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('homepage', new Route('/articles/{culture}/{year}/{title}.{_format}', array( - '_controller' => 'AcmeDemoBundle:Article:show', - '_format' => 'html', - ), array( - 'culture' => 'en|fr', - '_format' => 'html|rss', - 'year' => '\d+', - ))); - - return $collection; - -As you've seen, this route will only match if the ``{culture}`` 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: - -* ``/articles/en/2010/my-post`` -* ``/articles/fr/2010/my-post.rss`` -* ``/articles/en/2013/my-latest-post.html`` - -.. _book-routing-format-param: - -.. sidebar:: The Special ``_format`` Routing Parameter - - This example also highlights the special ``_format`` routing parameter. - When using this parameter, the matched value becomes the "request format" - of the ``Request`` object. Ultimately, the request format is used for such - things such as setting the ``Content-Type`` of the response (e.g. a ``json`` - request format translates into a ``Content-Type`` of ``application/json``). - It can also be used in the controller to render a different template for - each value of ``_format``. The ``_format`` parameter is a very powerful way - to render the same content in different formats. - -.. note:: - - Sometimes you want to make certain parts of your routes globally configurable. - Symfony2.1 provides you with a way to do this by leveraging service container - parameters. Read more about this in ":doc:`/cookbook/routing/service_container_parameters`. - -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 three parameters -that are special: each adds a unique piece of functionality inside your application: - -* ``_controller``: As you've seen, this parameter is used to determine which - controller is executed when the route is matched; - -* ``_format``: Used to set the request format (:ref:`read more`); - -* ``_locale``: Used to set the locale on the request (:ref:`read more`); - -.. tip:: - - If you use the ``_locale`` parameter in a route, that value will also - be stored on the session so that subsequent requests keep this same locale. - -.. index:: - single: Routing; Controllers - single: Controller; String naming format - -.. _controller-string-syntax: - -Controller Naming Pattern -------------------------- - -Every route must have a ``_controller`` parameter, which dictates which -controller should be executed when that route is matched. This parameter -uses a simple string pattern called the *logical controller name*, which -Symfony maps to a specific PHP method and class. The pattern has three parts, -each separated by a colon: - - **bundle**:**controller**:**action** - -For example, a ``_controller`` value of ``AcmeBlogBundle:Blog:show`` means: - -+----------------+------------------+-------------+ -| Bundle | Controller Class | Method Name | -+================+==================+=============+ -| AcmeBlogBundle | BlogController | showAction | -+----------------+------------------+-------------+ - -The controller might look like this:: - - // src/Acme/BlogBundle/Controller/BlogController.php - namespace Acme\BlogBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - - class BlogController extends Controller - { - public function showAction($slug) - { - // ... - } - } - -Notice that Symfony adds the string ``Controller`` to the class name (``Blog`` -=> ``BlogController``) and ``Action`` to the method name (``show`` => ``showAction``). - -You could also refer to this controller using its fully-qualified class name -and method: ``Acme\BlogBundle\Controller\BlogController::showAction``. -But if you follow some simple conventions, the logical name is more concise -and allows more flexibility. - -.. note:: - - In addition to using the logical name or the fully-qualified class name, - Symfony supports a third way of referring to a controller. This method - uses just one colon separator (e.g. ``service_name:indexAction``) and - refers to the controller as a service (see :doc:`/cookbook/controller/service`). - -Route Parameters and Controller Arguments ------------------------------------------ - -The route parameters (e.g. ``{slug}``) are especially important because -each is made available as an argument to the controller method:: - - public function showAction($slug) - { - // ... - } - -In reality, the entire ``defaults`` collection is merged with the parameter -values to form a single array. Each key of that array is available as an -argument on the controller. - -In other words, for each argument of your controller method, Symfony looks -for a route parameter of that name and assigns its value to that argument. -In the advanced example above, any combination (in any order) of the following -variables could be used as arguments to the ``showAction()`` method: - -* ``$culture`` -* ``$year`` -* ``$title`` -* ``$_format`` -* ``$_controller`` - -Since the placeholders and ``defaults`` collection are merged together, even -the ``$_controller`` variable is available. For a more detailed discussion, -see :ref:`route-parameters-controller-arguments`. - -.. tip:: - - You can also use a special ``$_route`` variable, which is set to the - name of the route that was matched. - -.. index:: - single: Routing; Importing routing resources - -.. _routing-include-external-resources: - -Including External Routing Resources ------------------------------------- - -All routes are loaded via a single configuration file - usually ``app/config/routing.yml`` -(see `Creating Routes`_ above). Commonly, however, you'll want to load routes -from other places, like a routing file that lives inside a bundle. This can -be done by "importing" that file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - acme_hello: - resource: "@AcmeHelloBundle/Resources/config/routing.yml" - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\RouteCollection; - - $collection = new RouteCollection(); - $collection->addCollection($loader->import("@AcmeHelloBundle/Resources/config/routing.php")); - - return $collection; - -.. note:: - - When importing resources from YAML, the key (e.g. ``acme_hello``) is meaningless. - Just be sure that it's unique so no other lines override it. - -The ``resource`` key loads the given routing resource. In this example the -resource is the full path to a file, where the ``@AcmeHelloBundle`` shortcut -syntax resolves to the path of that bundle. The imported file might look -like this: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/HelloBundle/Resources/config/routing.yml - acme_hello: - path: /hello/{name} - defaults: { _controller: AcmeHelloBundle:Hello:index } - - .. code-block:: xml - - - - - - - - AcmeHelloBundle:Hello:index - - - - .. code-block:: php - - // src/Acme/HelloBundle/Resources/config/routing.php - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('acme_hello', new Route('/hello/{name}', array( - '_controller' => 'AcmeHelloBundle:Hello:index', - ))); - - return $collection; - -The routes from this file are parsed and loaded in the same way as the main -routing file. - -Prefixing Imported Routes -~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can also choose to provide a "prefix" for the imported routes. For example, -suppose you want the ``acme_hello`` route to have a final path of ``/admin/hello/{name}`` -instead of simply ``/hello/{name}``: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - acme_hello: - resource: "@AcmeHelloBundle/Resources/config/routing.yml" - prefix: /admin - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\RouteCollection; - - $collection = new RouteCollection(); - $collection->addCollection($loader->import("@AcmeHelloBundle/Resources/config/routing.php"), '/admin'); - - return $collection; - -The string ``/admin`` will now be prepended to the path of each route loaded -from the new routing resource. - -.. tip:: - - You can also define routes using annotations. See the - :doc:`FrameworkExtraBundle documentation` - to see how. - -Adding a Host regex to Imported Routes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.2 - Host matching support was added in Symfony 2.2 - -You can set the host regex on imported routes. For more information, see -:ref:`component-routing-host-imported`. - -.. index:: - single: Routing; Debugging - -Visualizing & Debugging 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 ``router:debug`` console command. Execute -the command by running the following from the root of your project. - -.. code-block:: bash - - $ php app/console router:debug - -This command will print a helpful list of *all* the configured routes in -your application: - -.. code-block:: text - - homepage ANY / - contact GET /contact - contact_process POST /contact - article_show ANY /articles/{culture}/{year}/{title}.{_format} - blog ANY /blog/{page} - blog_show ANY /blog/{slug} - -You can also get very specific information on a single route by including -the route name after the command: - -.. code-block:: bash - - $ php app/console router:debug article_show - -Likewise, if you want to test whether a URL matches a given route, you can -use the ``router:match`` console command: - -.. code-block:: bash - - $ php app/console router:match /blog/my-latest-post - -This command will print which route the URL matches. - -.. code-block:: text - - Route "blog_show" matches - -.. index:: - single: Routing; Generating URLs - -Generating URLs ---------------- - -The routing system should also be used to generate URLs. In reality, routing -is a bi-directional system: mapping the URL to a controller+parameters and -a route+parameters back to a URL. The -:method:`Symfony\\Component\\Routing\\Router::match` and -:method:`Symfony\\Component\\Routing\\Router::generate` methods form this bi-directional -system. Take the ``blog_show`` example route from earlier:: - - $params = $this->get('router')->match('/blog/my-blog-post'); - // array( - // 'slug' => 'my-blog-post', - // '_controller' => 'AcmeBlogBundle:Blog:show', - // ) - - $uri = $this->get('router')->generate('blog_show', array('slug' => 'my-blog-post')); - // /blog/my-blog-post - -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, any URL can easily be generated:: - - class MainController extends Controller - { - public function showAction($slug) - { - // ... - - $url = $this->generateUrl( - 'blog_show', - array('slug' => 'my-blog-post') - ); - } - } - -.. note:: - - In controllers that extend Symfony's base - :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller`, - you can use the - :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::generateUrl` - method, which call's the router service's - :method:`Symfony\\Component\\Routing\\Router::generate` method. - -In an upcoming section, you'll learn how to generate URLs from inside templates. - -.. tip:: - - If the frontend of your application uses AJAX requests, you might want - to be able to generate URLs in JavaScript based on your routing configuration. - By using the `FOSJsRoutingBundle`_, you can do exactly that: - - .. code-block:: javascript - - var url = Routing.generate( - 'blog_show', - {"slug": 'my-blog-post'} - ); - - For more information, see the documentation for that bundle. - -.. index:: - single: Routing; Absolute URLs - -Generating Absolute URLs -~~~~~~~~~~~~~~~~~~~~~~~~ - -By default, the router will generate relative URLs (e.g. ``/blog``). To generate -an absolute URL, simply pass ``true`` to the third argument of the ``generate()`` -method:: - - $router->generate('blog_show', array('slug' => 'my-blog-post'), true); - // http://www.example.com/blog/my-blog-post - -.. note:: - - The host that's used when generating an absolute URL is the host of - the current ``Request`` object. This is detected automatically based - on server information supplied by PHP. When generating absolute URLs for - scripts run from the command line, you'll need to manually set the desired - host on the ``RequestContext`` object:: - - $router->getContext()->setHost('www.example.com'); - -.. index:: - single: Routing; Generating URLs in a template - -Generating URLs with Query Strings -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -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:: - - $router->generate('blog', array('page' => 2, 'category' => 'Symfony')); - // /blog/2?category=Symfony - -Generating URLs from a template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The most common place to generate a URL is from within a template when linking -between pages in your application. This is done just as before, but using -a template helper function: - -.. configuration-block:: - - .. code-block:: html+jinja - - - Read this blog post. - - - .. code-block:: html+php - - - Read this blog post. - - -Absolute URLs can also be generated. - -.. configuration-block:: - - .. code-block:: html+jinja - - - Read this blog post. - - - .. code-block:: html+php - - - Read this blog post. - - -Summary -------- - -Routing is a system for mapping the URL of incoming requests to the controller -function that should be called to process the request. It both allows you -to specify beautiful URLs and keeps the functionality of your application -decoupled from those URLs. Routing is a two-way mechanism, meaning that it -should also be used to generate URLs. - -Learn more from the Cookbook ----------------------------- - -* :doc:`/cookbook/routing/scheme` - -.. _`FOSJsRoutingBundle`: https://github.com/FriendsOfSymfony/FOSJsRoutingBundle diff --git a/book/security.rst b/book/security.rst deleted file mode 100644 index 7d9ecf0ef63..00000000000 --- a/book/security.rst +++ /dev/null @@ -1,2054 +0,0 @@ -.. index:: - single: Security - -Security -======== - -Security is a two-step process whose goal is to prevent a user from accessing -a resource that he/she should not have access to. - -In the first step of the process, the security system identifies who the user -is by requiring the user to submit some sort of identification. This is called -**authentication**, and it means that the system is trying to find out who -you are. - -Once the system knows who you are, the next step is to determine if you should -have access to a given resource. This part of the process is called **authorization**, -and it means that the system is checking to see if you have privileges to -perform a certain action. - -.. image:: /images/book/security_authentication_authorization.png - :align: center - -Since the best way to learn is to see an example, start by securing your -application with HTTP Basic authentication. - -.. note:: - - `Symfony's security component`_ is available as a standalone PHP library - for use inside any PHP project. - -Basic Example: HTTP Authentication ----------------------------------- - -The security component can be configured via your application configuration. -In fact, most standard security setups are just a matter of using the right -configuration. The following configuration tells Symfony to secure any URL -matching ``/admin/*`` and to ask the user for credentials using basic HTTP -authentication (i.e. the old-school username/password box): - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - secured_area: - pattern: ^/ - anonymous: ~ - http_basic: - realm: "Secured Demo Area" - - access_control: - - { path: ^/admin, roles: ROLE_ADMIN } - - providers: - in_memory: - memory: - users: - ryan: { password: ryanpass, roles: 'ROLE_USER' } - admin: { password: kitten, roles: 'ROLE_ADMIN' } - - encoders: - Symfony\Component\Security\Core\User\User: plaintext - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'secured_area' => array( - 'pattern' => '^/', - 'anonymous' => array(), - 'http_basic' => array( - 'realm' => 'Secured Demo Area', - ), - ), - ), - 'access_control' => array( - array('path' => '^/admin', 'role' => 'ROLE_ADMIN'), - ), - 'providers' => array( - 'in_memory' => array( - 'memory' => array( - 'users' => array( - 'ryan' => array('password' => 'ryanpass', 'roles' => 'ROLE_USER'), - 'admin' => array('password' => 'kitten', 'roles' => 'ROLE_ADMIN'), - ), - ), - ), - ), - 'encoders' => array( - 'Symfony\Component\Security\Core\User\User' => 'plaintext', - ), - )); - -.. tip:: - - A standard Symfony distribution separates the security configuration - into a separate file (e.g. ``app/config/security.yml``). If you don't - have a separate security file, you can put the configuration directly - into your main config file (e.g. ``app/config/config.yml``). - -The end result of this configuration is a fully-functional security system -that looks like the following: - -* There are two users in the system (``ryan`` and ``admin``); -* Users authenticate themselves via the basic HTTP authentication prompt; -* Any URL matching ``/admin/*`` is secured, and only the ``admin`` user - can access it; -* All URLs *not* matching ``/admin/*`` are accessible by all users (and the - user is never prompted to login). - -Let's look briefly at how security works and how each part of the configuration -comes into play. - -How Security Works: Authentication and Authorization ----------------------------------------------------- - -Symfony's security system works by determining who a user is (i.e. authentication) -and then checking to see if that user should have access to a specific resource -or URL. - -.. _book-security-firewalls: - -Firewalls (Authentication) -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When a user makes a request to a URL that's protected by a firewall, the -security system is activated. The job of the firewall is to determine whether -or not the user needs to be authenticated, and if he does, to send a response -back to the user initiating the authentication process. - -A firewall is activated when the URL of an incoming request matches the configured -firewall's regular expression ``pattern`` config value. In this example, the -``pattern`` (``^/``) will match *every* incoming request. The fact that the -firewall is activated does *not* mean, however, that the HTTP authentication -username and password box is displayed for every URL. For example, any user -can access ``/foo`` without being prompted to authenticate. - -.. image:: /images/book/security_anonymous_user_access.png - :align: center - -This works first because the firewall allows *anonymous users* via the ``anonymous`` -configuration parameter. In other words, the firewall doesn't require the -user to fully authenticate immediately. And because no special ``role`` is -needed to access ``/foo`` (under the ``access_control`` section), the request -can be fulfilled without ever asking the user to authenticate. - -If you remove the ``anonymous`` key, the firewall will *always* make a user -fully authenticate immediately. - -Access Controls (Authorization) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If a user requests ``/admin/foo``, however, the process behaves differently. -This is because of the ``access_control`` configuration section that says -that any URL matching the regular expression pattern ``^/admin`` (i.e. ``/admin`` -or anything matching ``/admin/*``) requires the ``ROLE_ADMIN`` role. Roles -are the basis for most authorization: a user can access ``/admin/foo`` only -if it has the ``ROLE_ADMIN`` role. - -.. image:: /images/book/security_anonymous_user_denied_authorization.png - :align: center - -Like before, when the user originally makes the request, the firewall doesn't -ask for any identification. However, as soon as the access control layer -denies the user access (because the anonymous user doesn't have the ``ROLE_ADMIN`` -role), the firewall jumps into action and initiates the authentication process. -The authentication process depends on the authentication mechanism you're -using. For example, if you're using the form login authentication method, -the user will be redirected to the login page. If you're using HTTP authentication, -the user will be sent an HTTP 401 response so that the user sees the username -and password box. - -The user now has the opportunity to submit its credentials back to the application. -If the credentials are valid, the original request can be re-tried. - -.. image:: /images/book/security_ryan_no_role_admin_access.png - :align: center - -In this example, the user ``ryan`` successfully authenticates with the firewall. -But since ``ryan`` doesn't have the ``ROLE_ADMIN`` role, he's still denied -access to ``/admin/foo``. Ultimately, this means that the user will see some -sort of message indicating that access has been denied. - -.. tip:: - - When Symfony denies the user access, the user sees an error screen and - receives a 403 HTTP status code (``Forbidden``). You can customize the - access denied error screen by following the directions in the - :ref:`Error Pages` cookbook entry - to customize the 403 error page. - -Finally, if the ``admin`` user requests ``/admin/foo``, a similar process -takes place, except now, after being authenticated, the access control layer -will let the request pass through: - -.. image:: /images/book/security_admin_role_access.png - :align: center - -The request flow when a user requests a protected resource is straightforward, -but incredibly flexible. As you'll see later, authentication can be handled -in any number of ways, including via a form login, X.509 certificate, or by -authenticating the user via Twitter. Regardless of the authentication method, -the request flow is always the same: - -#. A user accesses a protected resource; -#. The application redirects the user to the login form; -#. The user submits its credentials (e.g. username/password); -#. The firewall authenticates the user; -#. The authenticated user re-tries the original request. - -.. note:: - - The *exact* process actually depends a little bit on which authentication - mechanism you're using. For example, when using form login, the user - submits its credentials to one URL that processes the form (e.g. ``/login_check``) - and then is redirected back to the originally requested URL (e.g. ``/admin/foo``). - But with HTTP authentication, the user submits its credentials directly - to the original URL (e.g. ``/admin/foo``) and then the page is returned - to the user in that same request (i.e. no redirect). - - These types of idiosyncrasies shouldn't cause you any problems, but they're - good to keep in mind. - -.. tip:: - - You'll also learn later how *anything* can be secured in Symfony2, including - specific controllers, objects, or even PHP methods. - -.. _book-security-form-login: - -Using a Traditional Login Form ------------------------------- - -.. tip:: - - In this section, you'll learn how to create a basic login form that continues - to use the hard-coded users that are defined in the ``security.yml`` file. - - To load users from the database, please read :doc:`/cookbook/security/entity_provider`. - By reading that article and this section, you can create a full login form - system that loads users from the database. - -So far, you've seen how to blanket your application beneath a firewall and -then protect access to certain areas with roles. By using HTTP Authentication, -you can effortlessly tap into the native username/password box offered by -all browsers. However, Symfony supports many authentication mechanisms out -of the box. For details on all of them, see the -:doc:`Security Configuration Reference`. - -In this section, you'll enhance this process by allowing the user to authenticate -via a traditional HTML login form. - -First, enable form login under your firewall: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - secured_area: - pattern: ^/ - anonymous: ~ - form_login: - login_path: login - check_path: login_check - - .. code-block:: xml - - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'secured_area' => array( - 'pattern' => '^/', - 'anonymous' => array(), - 'form_login' => array( - 'login_path' => 'login', - 'check_path' => 'login_check', - ), - ), - ), - )); - -.. tip:: - - If you don't need to customize your ``login_path`` or ``check_path`` - values (the values used here are the default values), you can shorten - your configuration: - - .. configuration-block:: - - .. code-block:: yaml - - form_login: ~ - - .. code-block:: xml - - - - .. code-block:: php - - 'form_login' => array(), - -Now, when the security system initiates the authentication process, it will -redirect the user to the login form (``/login`` by default). Implementing this -login form visually is your job. First, the create two routes we used in the -security configuration: the ``login`` route will display the login form (i.e. -``/login``) and the ``login_check`` route will handle the login form -submission (i.e. ``/login_check``): - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - login: - pattern: /login - defaults: { _controller: AcmeSecurityBundle:Security:login } - login_check: - pattern: /login_check - - .. code-block:: xml - - - - - - - - AcmeSecurityBundle:Security:login - - - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('login', new Route('/login', array( - '_controller' => 'AcmeDemoBundle:Security:login', - ))); - $collection->add('login_check', new Route('/login_check', array())); - - return $collection; - -.. note:: - - You will *not* need to implement a controller for the ``/login_check`` - URL as the firewall will automatically catch and process any form submitted - to this URL. - -.. versionadded:: 2.1 - As of Symfony 2.1, you *must* have routes configured for your ``login_path``, - ``check_path`` ``logout`` keys. These keys can be route names (as shown - in this example) or URLs that have routes configured for them. - -Notice that the name of the ``login`` route matches the``login_path`` config -value, as that's where the security system will redirect users that need -to login. - -Next, create the controller that will display the login form:: - - // src/Acme/SecurityBundle/Controller/SecurityController.php; - namespace Acme\SecurityBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Security\Core\SecurityContext; - - class SecurityController extends Controller - { - public function loginAction() - { - $request = $this->getRequest(); - $session = $request->getSession(); - - // get the login error if there is one - if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) { - $error = $request->attributes->get( - SecurityContext::AUTHENTICATION_ERROR - ); - } else { - $error = $session->get(SecurityContext::AUTHENTICATION_ERROR); - $session->remove(SecurityContext::AUTHENTICATION_ERROR); - } - - return $this->render( - 'AcmeSecurityBundle:Security:login.html.twig', - array( - // last username entered by the user - 'last_username' => $session->get(SecurityContext::LAST_USERNAME), - 'error' => $error, - ) - ); - } - } - -Don't let this controller confuse you. As you'll see in a moment, when the -user submits the form, the security system automatically handles the form -submission for you. If the user had submitted an invalid username or password, -this controller reads the form submission error from the security system so -that it can be displayed back to the user. - -In other words, your job is to display the login form and any login errors -that may have occurred, but the security system itself takes care of checking -the submitted username and password and authenticating the user. - -Finally, create the corresponding template: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #} - {% if error %} -
{{ error.message }}
- {% endif %} - -
- - - - - - - {# - If you want to control the URL the user is redirected to on success (more details below) - - #} - - -
- - .. code-block:: html+php - - - -
getMessage() ?>
- - -
- - - - - - - - - -
- -.. tip:: - - The ``error`` variable passed into the template is an instance of - :class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException`. - It may contain more information - or even sensitive information - about - the authentication failure, so use it wisely! - -The form has very few requirements. First, by submitting the form to ``/login_check`` -(via the ``login_check`` route), the security system will intercept the form -submission and process the form for you automatically. Second, the security -system expects the submitted fields to be called ``_username`` and ``_password`` -(these field names can be :ref:`configured`). - -And that's it! When you submit the form, the security system will automatically -check the user's credentials and either authenticate the user or send the -user back to the login form where the error can be displayed. - -Let's review the whole process: - -#. The user tries to access a resource that is protected; -#. The firewall initiates the authentication process by redirecting the - user to the login form (``/login``); -#. The ``/login`` page renders login form via the route and controller created - in this example; -#. The user submits the login form to ``/login_check``; -#. The security system intercepts the request, checks the user's submitted - credentials, authenticates the user if they are correct, and sends the - user back to the login form if they are not. - -By default, if the submitted credentials are correct, the user will be redirected -to the original page that was requested (e.g. ``/admin/foo``). If the user -originally went straight to the login page, he'll be redirected to the homepage. -This can be highly customized, allowing you to, for example, redirect the -user to a specific URL. - -For more details on this and how to customize the form login process in general, -see :doc:`/cookbook/security/form_login`. - -.. _book-security-common-pitfalls: - -.. sidebar:: Avoid Common Pitfalls - - When setting up your login form, watch out for a few common pitfalls. - - **1. Create the correct routes** - - First, be sure that you've defined the ``login`` and ``login_check`` - routes correctly and that they correspond to the ``login_path`` and - ``check_path`` config values. A misconfiguration here can mean that you're - redirected to a 404 page instead of the login page, or that submitting - the login form does nothing (you just see the login form over and over - again). - - **2. Be sure the login page isn't secure** - - Also, be sure that the login page does *not* require any roles to be - viewed. For example, the following configuration - which requires the - ``ROLE_ADMIN`` role for all URLs (including the ``/login`` URL), will - cause a redirect loop: - - .. configuration-block:: - - .. code-block:: yaml - - access_control: - - { path: ^/, roles: ROLE_ADMIN } - - .. code-block:: xml - - - - - - .. code-block:: php - - 'access_control' => array( - array('path' => '^/', 'role' => 'ROLE_ADMIN'), - ), - - Removing the access control on the ``/login`` URL fixes the problem: - - .. configuration-block:: - - .. code-block:: yaml - - access_control: - - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/, roles: ROLE_ADMIN } - - .. code-block:: xml - - - - - - - .. code-block:: php - - 'access_control' => array( - array('path' => '^/login', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'), - array('path' => '^/', 'role' => 'ROLE_ADMIN'), - ), - - Also, if your firewall does *not* allow for anonymous users, you'll need - to create a special firewall that allows anonymous users for the login - page: - - .. configuration-block:: - - .. code-block:: yaml - - firewalls: - login_firewall: - pattern: ^/login$ - anonymous: ~ - secured_area: - pattern: ^/ - form_login: ~ - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - 'firewalls' => array( - 'login_firewall' => array( - 'pattern' => '^/login$', - 'anonymous' => array(), - ), - 'secured_area' => array( - 'pattern' => '^/', - 'form_login' => array(), - ), - ), - - **3. Be sure ``/login_check`` is behind a firewall** - - Next, make sure that your ``check_path`` URL (e.g. ``/login_check``) - is behind the firewall you're using for your form login (in this example, - the single firewall matches *all* URLs, including ``/login_check``). If - ``/login_check`` doesn't match any firewall, you'll receive a ``Unable - to find the controller for path "/login_check"`` exception. - - **4. Multiple firewalls don't share 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. - -Authorization -------------- - -The first step in security is always authentication: the process of verifying -who the user is. With Symfony, authentication can be done in any way - via -a form login, basic HTTP Authentication, or even via Facebook. - -Once the user has been authenticated, authorization begins. Authorization -provides a standard and powerful way to decide if a user can access any resource -(a URL, a model object, a method call, ...). This works by assigning specific -roles to each user, and then requiring different roles for different resources. - -The process of authorization has two different sides: - -#. The user has a specific set of roles; -#. A resource requires a specific role in order to be accessed. - -In this section, you'll focus on how to secure different resources (e.g. URLs, -method calls, etc) with different roles. Later, you'll learn more about how -roles are created and assigned to users. - -Securing Specific URL Patterns -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The most basic way to secure part of your application is to secure an entire -URL pattern. You've seen this already in the first example of this chapter, -where anything matching the regular expression pattern ``^/admin`` requires -the ``ROLE_ADMIN`` role. - -.. caution:: - - Understanding exactly how ``access_control`` works is **very** important - to make sure your application is properly secured. See :ref:`security-book-access-control-explanation` - below for detailed information. - -You can define as many URL patterns as you need - each is a regular expression. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - access_control: - - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN } - - { path: ^/admin, roles: ROLE_ADMIN } - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - // ... - 'access_control' => array( - array('path' => '^/admin/users', 'role' => 'ROLE_SUPER_ADMIN'), - array('path' => '^/admin', 'role' => 'ROLE_ADMIN'), - ), - )); - -.. tip:: - - Prepending the path with ``^`` ensures that only URLs *beginning* with - the pattern are matched. For example, a path of simply ``/admin`` (without - the ``^``) would correctly match ``/admin/foo`` but would also match URLs - like ``/foo/admin``. - -.. _security-book-access-control-explanation: - -Understanding how ``access_control`` works -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For each incoming request, Symfony2 checks each ``access_control`` entry -to find *one* that matches the current request. As soon as it finds a matching -``access_control`` entry, it stops - only the **first** matching ``access_control`` -is used to enforce access. - -Each ``access_control`` has several options that configure two different -things: (a) :ref:`should the incoming request match this access control entry` -and (b) :ref:`once it matches, should some sort of access restriction be enforced`: - -.. _security-book-access-control-matching-options: - -**(a) Matching Options** - -Symfony2 creates an instance of :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher` -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`` -* ``host`` -* ``methods`` - -Take the following ``access_control`` entries as an example: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - access_control: - - { path: ^/admin, roles: ROLE_USER_IP, ip: 127.0.0.1 } - - { path: ^/admin, roles: ROLE_USER_HOST, host: symfony.com } - - { path: ^/admin, roles: ROLE_USER_METHOD, methods: [POST, PUT] } - - { path: ^/admin, roles: ROLE_USER } - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - 'access_control' => array( - array('path' => '^/admin', 'role' => 'ROLE_USER_IP', 'ip' => '127.0.0.1'), - array('path' => '^/admin', 'role' => 'ROLE_USER_HOST', 'host' => 'symfony.com'), - array('path' => '^/admin', 'role' => 'ROLE_USER_METHOD', 'method' => 'POST, PUT'), - array('path' => '^/admin', 'role' => 'ROLE_USER'), - ), - -For each incoming request, Symfony will decided which ``access_control`` -to use based on the URI, the client's IP address, the incoming host name, -and the request method. Remember, the first rule that matches is used, and -if ``ip``, ``host`` or ``method`` are not specified for an entry, that ``access_control`` -will match any ``ip``, ``host`` or ``method``: - -+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| **URI** | **IP** | **HOST** | **METHOD** | ``access_control`` | Why? | -+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 127.0.0.1 | example.com | GET | rule #1 (``ROLE_USER_IP``) | The URI matches ``path`` and the IP matches ``ip``. | -+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 127.0.0.1 | symfony.com | GET | rule #1 (``ROLE_USER_IP``) | The ``path`` and ``ip`` still match. This would also match | -| | | | | | the ``ROLE_USER_HOST`` entry, but *only* the **first** | -| | | | | | ``access_control`` match is used. | -+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 168.0.0.1 | symfony.com | GET | rule #2 (``ROLE_USER_HOST``) | The ``ip`` doesn't match the first rule, so the second | -| | | | | | rule (which matches) is used. | -+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 168.0.0.1 | symfony.com | POST | rule #2 (``ROLE_USER_HOST``) | The second rule still matches. This would also match the | -| | | | | | third rule (``ROLE_USER_METHOD``), but only the **first** | -| | | | | | matched ``access_control`` is used. | -+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 168.0.0.1 | example.com | POST | rule #3 (``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 | example.com | GET | rule #4 (``ROLE_USER``) | 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. | -+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/foo`` | 127.0.0.1 | 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. | -+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ - -.. _security-book-access-control-enforcement-options: - -**(b) Access Enforcement** - -Once Symfony2 has decided which ``access_control`` entry matches (if any), -it then *enforces* access restrictions based on the ``roles`` and ``requires_channel`` -options: - -* ``role`` If the user does not have the given role(s), then access is denied - (internally, an :class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException` - is thrown); - -* ``requires_channel`` If the incoming request's channel (e.g. ``http``) - does not match this value (e.g. ``https``), the user will be redirected - (e.g. redirected from ``http`` to ``https``, or vice versa). - -.. tip:: - - If access is denied, the system will try to authenticate the user if not - already (e.g. redirect the user to the login page). If the user is already - logged in, the 403 "access denied" error page will be shown. See - :doc:`/cookbook/controller/error_pages` for more information. - -.. _book-security-securing-ip: - -Securing by IP -~~~~~~~~~~~~~~ - -Certain situations may arise when you may need to restrict access to a given -path based on IP. This is particularly relevant in the case of -:ref:`Edge Side Includes` (ESI), for example. When ESI is -enabled, it's recommended to secure access to ESI URLs. Indeed, some ESI may -contain some private content like the current logged in user's information. To -prevent any direct access to these resources from a web browser (by guessing the -ESI URL pattern), the ESI route **must** be secured to be only visible from -the trusted reverse proxy cache. - -.. versionadded:: 2.3 - Version 2.3 allows multiple IP addresses in a single rule with the ``ips: [a, b]`` - construct. Prior to 2.3, users should create one rule per IP address to match and - use the ``ip`` key instead of ``ips``. - -Here is an example of how you might secure all ESI routes that start with a -given prefix, ``/esi``, from outside access: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - access_control: - - { path: ^/esi, roles: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1] } - - { path: ^/esi, roles: ROLE_NO_ACCESS } - - .. code-block:: xml - - - - - - - .. code-block:: php - - 'access_control' => array( - array('path' => '^/esi', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'ips' => '127.0.0.1, ::1'), - array('path' => '^/esi', 'role' => 'ROLE_NO_ACCESS'), - ), - -Here is how it works when the path is ``/esi/something`` coming from the -``10.0.0.1`` IP: - -* The first access control rule is ignored as the ``path`` matches but the - ``ip`` does not match either of the IPs listed; - -* The second access control rule is enabled (the only restriction being the - ``path`` and it matches): as the user cannot have the ``ROLE_NO_ACCESS`` - role as it's not defined, access is denied (the ``ROLE_NO_ACCESS`` role can - be anything that does not match an existing role, it just serves as a trick - to always deny access). - -Now, if the same request comes from ``127.0.0.1`` or ``::1`` (the IPv6 loopback -address): - -* Now, the first access control rule is enabled as both the ``path`` and the - ``ip`` match: access is allowed as the user always has the - ``IS_AUTHENTICATED_ANONYMOUSLY`` role. - -* The second access rule is not examined as the first rule matched. - -.. _book-security-securing-channel: - -Securing by Channel -~~~~~~~~~~~~~~~~~~~ - -You can also require a user to access a URL via SSL; just use the -``requires_channel`` argument in any ``access_control`` entries: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - access_control: - - { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } - - .. code-block:: xml - - - - - - .. code-block:: php - - 'access_control' => array( - array('path' => '^/cart/checkout', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'requires_channel' => 'https'), - ), - -.. _book-security-securing-controller: - -Securing a Controller -~~~~~~~~~~~~~~~~~~~~~ - -Protecting your application based on URL patterns is easy, but may not be -fine-grained enough in certain cases. When necessary, you can easily force -authorization from inside a controller:: - - // ... - use Symfony\Component\Security\Core\Exception\AccessDeniedException; - - public function helloAction($name) - { - if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) { - throw new AccessDeniedException(); - } - - // ... - } - -.. _book-security-securing-controller-annotations: - -You can also choose to install and use the optional ``JMSSecurityExtraBundle``, -which can secure your controller using annotations:: - - // ... - use JMS\SecurityExtraBundle\Annotation\Secure; - - /** - * @Secure(roles="ROLE_ADMIN") - */ - public function helloAction($name) - { - // ... - } - -For more information, see the `JMSSecurityExtraBundle`_ documentation. If you're -using Symfony's Standard Distribution, this bundle is available by default. -If not, you can easily download and install it. - -Securing other Services -~~~~~~~~~~~~~~~~~~~~~~~ - -In fact, anything in Symfony can be protected using a strategy similar to -the one seen in the previous section. For example, suppose you have a service -(i.e. a PHP class) whose job is to send emails from one user to another. -You can restrict use of this class - no matter where it's being used from - -to users that have a specific role. - -For more information on how you can use the security component to secure -different services and methods in your application, see :doc:`/cookbook/security/securing_services`. - -Access Control Lists (ACLs): Securing Individual Database Objects -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Imagine you are designing a blog system where your users can comment on your -posts. Now, you want a user to be able to edit his own comments, but not -those of other users. Also, as the admin user, you yourself want to be able -to edit *all* comments. - -The security component comes with an optional access control list (ACL) system -that you can use when you need to control access to individual instances -of an object in your system. *Without* ACL, you can secure your system so that -only certain users can edit blog comments in general. But *with* ACL, you -can restrict or allow access on a comment-by-comment basis. - -For more information, see the cookbook article: :doc:`/cookbook/security/acl`. - -Users ------ - -In the previous sections, you learned how you can protect different resources -by requiring a set of *roles* for a resource. This section explores -the other side of authorization: users. - -Where do Users come from? (*User Providers*) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -During authentication, the user submits a set of credentials (usually a username -and password). The job of the authentication system is to match those credentials -against some pool of users. So where does this list of users come from? - -In Symfony2, users can come from anywhere - a configuration file, a database -table, a web service, or anything else you can dream up. Anything that provides -one or more users to the authentication system is known as a "user provider". -Symfony2 comes standard with the two most common user providers: one that -loads users from a configuration file and one that loads users from a database -table. - -Specifying Users in a Configuration File -........................................ - -The easiest way to specify your users is directly in a configuration file. -In fact, you've seen this already in the example in this chapter. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - providers: - default_provider: - memory: - users: - ryan: { password: ryanpass, roles: 'ROLE_USER' } - admin: { password: kitten, roles: 'ROLE_ADMIN' } - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - // ... - 'providers' => array( - 'default_provider' => array( - 'memory' => array( - 'users' => array( - 'ryan' => array('password' => 'ryanpass', 'roles' => 'ROLE_USER'), - 'admin' => array('password' => 'kitten', 'roles' => 'ROLE_ADMIN'), - ), - ), - ), - ), - )); - -This user provider is called the "in-memory" user provider, since the users -aren't stored anywhere in a database. The actual user object is provided -by Symfony (:class:`Symfony\\Component\\Security\\Core\\User\\User`). - -.. tip:: - Any user provider can load users directly from configuration by specifying - the ``users`` configuration parameter and listing the users beneath it. - -.. caution:: - - If your username is completely numeric (e.g. ``77``) or contains a dash - (e.g. ``user-name``), you should use that alternative syntax when specifying - users in YAML: - - .. code-block:: yaml - - users: - - { name: 77, password: pass, roles: 'ROLE_USER' } - - { name: user-name, password: pass, roles: 'ROLE_USER' } - -For smaller sites, this method is quick and easy to setup. For more complex -systems, you'll want to load your users from the database. - -.. _book-security-user-entity: - -Loading Users from the Database -............................... - -If you'd like to load your users via the Doctrine ORM, you can easily do -this by creating a ``User`` class and configuring the ``entity`` provider. - -.. tip:: - - A high-quality open source bundle is available that allows your users - to be stored via the Doctrine ORM or ODM. Read more about the `FOSUserBundle`_ - on GitHub. - -With this approach, you'll first create your own ``User`` class, which will -be stored in the database. - -.. code-block:: php - - // src/Acme/UserBundle/Entity/User.php - namespace Acme\UserBundle\Entity; - - use Symfony\Component\Security\Core\User\UserInterface; - use Doctrine\ORM\Mapping as ORM; - - /** - * @ORM\Entity - */ - class User implements UserInterface - { - /** - * @ORM\Column(type="string", length=255) - */ - protected $username; - - // ... - } - -As far as the security system is concerned, the only requirement for your -custom user class is that it implements the :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface` -interface. This means that your concept of a "user" can be anything, as long -as it implements this interface. - -.. versionadded:: 2.1 - In Symfony 2.1, the ``equals`` method was removed from ``UserInterface``. - If you need to override the default implementation of comparison logic, - implement the new :class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface` - interface. - -.. note:: - - The user object will be serialized and saved in the session during requests, - therefore it is recommended that you `implement the \Serializable interface`_ - in your user object. This is especially important if your ``User`` class - has a parent class with private properties. - -Next, configure an ``entity`` user provider, and point it to your ``User`` -class: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - providers: - main: - entity: { class: Acme\UserBundle\Entity\User, property: username } - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'providers' => array( - 'main' => array( - 'entity' => array('class' => 'Acme\UserBundle\Entity\User', 'property' => 'username'), - ), - ), - )); - -With the introduction of this new provider, the authentication system will -attempt to load a ``User`` object from the database by using the ``username`` -field of that class. - -.. note:: - This example is just meant to show you the basic idea behind the ``entity`` - provider. For a full working example, see :doc:`/cookbook/security/entity_provider`. - -For more information on creating your own custom provider (e.g. if you needed -to load users via a web service), see :doc:`/cookbook/security/custom_provider`. - -.. _book-security-encoding-user-password: - -Encoding the User's Password -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -So far, for simplicity, all the examples have stored the users' passwords -in plain text (whether those users are stored in a configuration file or in -a database somewhere). Of course, in a real application, you'll want to encode -your users' passwords for security reasons. This is easily accomplished by -mapping your User class to one of several built-in "encoders". For example, -to store your users in memory, but obscure their passwords via ``sha1``, -do the following: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - providers: - in_memory: - memory: - users: - ryan: { password: bb87a29949f3a1ee0559f8a57357487151281386, roles: 'ROLE_USER' } - admin: { password: 74913f5cd5f61ec0bcfdb775414c2fb3d161b620, roles: 'ROLE_ADMIN' } - - encoders: - Symfony\Component\Security\Core\User\User: - algorithm: sha1 - iterations: 1 - encode_as_base64: false - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - // ... - 'providers' => array( - 'in_memory' => array( - 'memory' => array( - 'users' => array( - 'ryan' => array('password' => 'bb87a29949f3a1ee0559f8a57357487151281386', 'roles' => 'ROLE_USER'), - 'admin' => array('password' => '74913f5cd5f61ec0bcfdb775414c2fb3d161b620', 'roles' => 'ROLE_ADMIN'), - ), - ), - ), - ), - 'encoders' => array( - 'Symfony\Component\Security\Core\User\User' => array( - 'algorithm' => 'sha1', - 'iterations' => 1, - 'encode_as_base64' => false, - ), - ), - )); - -By setting the ``iterations`` to ``1`` and the ``encode_as_base64`` to false, -the password is simply run through the ``sha1`` algorithm one time and without -any extra encoding. You can now calculate the hashed password either programmatically -(e.g. ``hash('sha1', 'ryanpass')``) or via some online tool like `functions-online.com`_ - -If you're creating your users dynamically (and storing them in a database), -you can use even tougher hashing algorithms and then rely on an actual password -encoder object to help you encode passwords. For example, suppose your User -object is ``Acme\UserBundle\Entity\User`` (like in the above example). First, -configure the encoder for that user: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - encoders: - Acme\UserBundle\Entity\User: sha512 - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - // ... - 'encoders' => array( - 'Acme\UserBundle\Entity\User' => 'sha512', - ), - )); - -In this case, you're using the stronger ``sha512`` algorithm. Also, since -you've simply specified the algorithm (``sha512``) as a string, the system -will default to hashing your password 5000 times in a row and then encoding -it as base64. In other words, the password has been greatly obfuscated so -that the hashed password can't be decoded (i.e. you can't determine the password -from the hashed password). - -.. versionadded:: 2.2 - As of Symfony 2.2 you can also use the :ref:`PBKDF2` - and :ref:`BCrypt` password encoders. - -Determining the Hashed Password -............................... - -If you have some sort of registration form for users, you'll need to be able -to determine the hashed password so that you can set it on your user. No -matter what algorithm you configure for your user object, the hashed password -can always be determined in the following way from a controller:: - - $factory = $this->get('security.encoder_factory'); - $user = new Acme\UserBundle\Entity\User(); - - $encoder = $factory->getEncoder($user); - $password = $encoder->encodePassword('ryanpass', $user->getSalt()); - $user->setPassword($password); - -Retrieving the User Object -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -After authentication, the ``User`` object of the current user can be accessed -via the ``security.context`` service. From inside a controller, this will -look like:: - - public function indexAction() - { - $user = $this->get('security.context')->getToken()->getUser(); - } - -In a controller this can be shortcut to: - -.. code-block:: php - - public function indexAction() - { - $user = $this->getUser(); - } - - -.. note:: - - Anonymous users are technically authenticated, meaning that the ``isAuthenticated()`` - method of an anonymous user object will return true. To check if your - user is actually authenticated, check for the ``IS_AUTHENTICATED_FULLY`` - role. - -In a Twig Template this object can be accessed via the ``app.user`` key, -which calls the :method:`GlobalVariables::getUser()` -method: - -.. configuration-block:: - - .. code-block:: html+jinja - -

Username: {{ app.user.username }}

- - .. code-block:: html+php - -

Username: getUser()->getUsername() ?>

- - -Using Multiple User Providers -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Each authentication mechanism (e.g. HTTP Authentication, form login, etc) -uses exactly one user provider, and will use the first declared user provider -by default. But what if you want to specify a few users via configuration -and the rest of your users in the database? This is possible by creating -a new provider that chains the two together: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - providers: - chain_provider: - chain: - providers: [in_memory, user_db] - in_memory: - memory: - users: - foo: { password: test } - user_db: - entity: { class: Acme\UserBundle\Entity\User, property: username } - - .. code-block:: xml - - - - - - in_memory - user_db - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'providers' => array( - 'chain_provider' => array( - 'chain' => array( - 'providers' => array('in_memory', 'user_db'), - ), - ), - 'in_memory' => array( - 'memory' => array( - 'users' => array( - 'foo' => array('password' => 'test'), - ), - ), - ), - 'user_db' => array( - 'entity' => array('class' => 'Acme\UserBundle\Entity\User', 'property' => 'username'), - ), - ), - )); - -Now, all authentication mechanisms will use the ``chain_provider``, since -it's the first specified. The ``chain_provider`` will, in turn, try to load -the user from both the ``in_memory`` and ``user_db`` providers. - -.. tip:: - - If you have no reasons to separate your ``in_memory`` users from your - ``user_db`` users, you can accomplish this even more easily by combining - the two sources into a single provider: - - .. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - providers: - main_provider: - memory: - users: - foo: { password: test } - entity: - class: Acme\UserBundle\Entity\User, - property: username - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'providers' => array( - 'main_provider' => array( - 'memory' => array( - 'users' => array( - 'foo' => array('password' => 'test'), - ), - ), - 'entity' => array('class' => 'Acme\UserBundle\Entity\User', 'property' => 'username'), - ), - ), - )); - -You can also configure the firewall or individual authentication mechanisms -to use a specific provider. Again, unless a provider is specified explicitly, -the first provider is always used: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - secured_area: - # ... - provider: user_db - http_basic: - realm: "Secured Demo Area" - provider: in_memory - form_login: ~ - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'secured_area' => array( - // ... - 'provider' => 'user_db', - 'http_basic' => array( - // ... - 'provider' => 'in_memory', - ), - 'form_login' => array(), - ), - ), - )); - -In this example, if a user tries to login via HTTP authentication, the authentication -system will use the ``in_memory`` user provider. But if the user tries to -login via the form login, the ``user_db`` provider will be used (since it's -the default for the firewall as a whole). - -For more information about user provider and firewall configuration, see -the :doc:`/reference/configuration/security`. - -Roles ------ - -The idea of a "role" is key to the authorization process. Each user is assigned -a set of roles and then each resource requires one or more roles. If the user -has the required roles, access is granted. Otherwise access is denied. - -Roles are pretty simple, and are basically strings that you can invent and -use as needed (though roles are objects internally). For example, if you -need to start limiting access to the blog admin section of your website, -you could protect that section using a ``ROLE_BLOG_ADMIN`` role. This role -doesn't need to be defined anywhere - you can just start using it. - -.. note:: - - All roles **must** begin with the ``ROLE_`` prefix to be managed by - Symfony2. If you define your own roles with a dedicated ``Role`` class - (more advanced), don't use the ``ROLE_`` prefix. - -Hierarchical Roles -~~~~~~~~~~~~~~~~~~ - -Instead of associating many roles to users, you can define role inheritance -rules by creating a role hierarchy: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - role_hierarchy: - ROLE_ADMIN: ROLE_USER - ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] - - .. code-block:: xml - - - - ROLE_USER - ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'role_hierarchy' => array( - 'ROLE_ADMIN' => 'ROLE_USER', - 'ROLE_SUPER_ADMIN' => array('ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH'), - ), - )); - -In the above configuration, users with ``ROLE_ADMIN`` role will also have the -``ROLE_USER`` role. The ``ROLE_SUPER_ADMIN`` role has ``ROLE_ADMIN``, ``ROLE_ALLOWED_TO_SWITCH`` -and ``ROLE_USER`` (inherited from ``ROLE_ADMIN``). - -Logging Out ------------ - -Usually, you'll also want your users to be able to log out. Fortunately, -the firewall can handle this automatically for you when you activate the -``logout`` config parameter: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - secured_area: - # ... - logout: - path: /logout - target: / - # ... - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'secured_area' => array( - // ... - 'logout' => array('path' => 'logout', 'target' => '/'), - ), - ), - // ... - )); - -Once this is configured under your firewall, sending a user to ``/logout`` -(or whatever you configure the ``path`` to be), will un-authenticate the -current user. The user will then be sent to the homepage (the value defined -by the ``target`` parameter). Both the ``path`` and ``target`` config parameters -default to what's specified here. In other words, unless you need to customize -them, you can omit them entirely and shorten your configuration: - -.. configuration-block:: - - .. code-block:: yaml - - logout: ~ - - .. code-block:: xml - - - - .. code-block:: php - - 'logout' => array(), - -Note that you will *not* need to implement a controller for the ``/logout`` -URL as the firewall takes care of everything. You *do*, however, need to create -a route so that you can use it to generate the URL: - -.. caution:: - - As of Symfony 2.1, you *must* have a route that corresponds to your logout - path. Without this route, logging out will not work. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - logout: - path: /logout - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('logout', new Route('/logout', array())); - - return $collection; - -Once the user has been logged out, he will be redirected to whatever path -is defined by the ``target`` parameter above (e.g. the ``homepage``). For -more information on configuring the logout, see the -:doc:`Security Configuration Reference`. - -.. _book-security-template: - -Access Control in Templates ---------------------------- - -If you want to check if the current user has a role inside a template, use -the built-in helper function: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% if is_granted('ROLE_ADMIN') %} - Delete - {% endif %} - - .. code-block:: html+php - - isGranted('ROLE_ADMIN')): ?> - Delete - - -.. note:: - - If you use this function and are *not* at a URL where there is a firewall - active, an exception will be thrown. Again, it's almost always a good - idea to have a main firewall that covers all URLs (as has been shown - in this chapter). - -Access Control in Controllers ------------------------------ - -If you want to check if the current user has a role in your controller, use -the :method:`Symfony\\Component\\Security\\Core\\SecurityContext::isGranted` -method of the security context:: - - public function indexAction() - { - // show different content to admin users - if ($this->get('security.context')->isGranted('ROLE_ADMIN')) { - // ... load admin content here - } - - // ... load other regular content here - } - -.. note:: - - A firewall must be active or an exception will be thrown when the ``isGranted`` - method is called. See the note above about templates for more details. - -Impersonating a User --------------------- - -Sometimes, it's useful to be able to switch from one user to another without -having to logout and login again (for instance when you are debugging or trying -to understand a bug a user sees that you can't reproduce). This can be easily -done by activating the ``switch_user`` firewall listener: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - main: - # ... - switch_user: true - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'main'=> array( - // ... - 'switch_user' => true - ), - ), - )); - -To switch to another user, just add a query string with the ``_switch_user`` -parameter and the username as the value to the current URL: - -.. code-block:: text - - http://example.com/somewhere?_switch_user=thomas - -To switch back to the original user, use the special ``_exit`` username: - -.. code-block:: text - - http://example.com/somewhere?_switch_user=_exit - -During impersonation, the user is provided with a special role called -``ROLE_PREVIOUS_ADMIN``. In a template, for instance, this role can be used -to show a link to exit impersonation: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% if is_granted('ROLE_PREVIOUS_ADMIN') %} - Exit impersonation - {% endif %} - - .. code-block:: html+php - - isGranted('ROLE_PREVIOUS_ADMIN')): ?> - - Exit impersonation - - - -Of course, this feature needs to be made available to a small group of users. -By default, access is restricted to users having the ``ROLE_ALLOWED_TO_SWITCH`` -role. The name of this role can be modified via the ``role`` setting. For -extra security, you can also change the query parameter name via the ``parameter`` -setting: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - main: - # ... - switch_user: { role: ROLE_ADMIN, parameter: _want_to_be_this_user } - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'main'=> array( - // ... - 'switch_user' => array('role' => 'ROLE_ADMIN', 'parameter' => '_want_to_be_this_user'), - ), - ), - )); - -Stateless Authentication ------------------------- - -By default, Symfony2 relies on a cookie (the Session) to persist the security -context of the user. But if you use certificates or HTTP authentication for -instance, persistence is not needed as credentials are available for each -request. In that case, and if you don't need to store anything else between -requests, you can activate the stateless authentication (which means that no -cookie will be ever created by Symfony2): - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - main: - http_basic: ~ - stateless: true - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'main' => array('http_basic' => array(), 'stateless' => true), - ), - )); - -.. note:: - - If you use a form login, Symfony2 will create a cookie even if you set - ``stateless`` to ``true``. - -Utilities ---------- - -.. versionadded:: 2.2 - The ``StringUtils`` and ``SecureRandom`` classes were added in Symfony 2.2 - -The Symfony Security Component comes with a collection of nice utilities related -to security. These utilities are used by Symfony, but you should also use -them if you want to solve the problem they address. - -Comparing Strings -~~~~~~~~~~~~~~~~~ - -The time it takes to compare two strings depends on their differences. This -can be used by an attacker when the two strings represent a password for -instance; it is known as a `Timing attack`_. - -Internally, when comparing two passwords, Symfony uses a constant-time -algorithm; you can use the same strategy in your own code thanks to the -:class:`Symfony\\Component\\Security\\Core\\Util\\StringUtils` class:: - - use Symfony\Component\Security\Core\Util\StringUtils; - - // is password1 equals to password2? - $bool = StringUtils::equals($password1, $password2); - -Generating a secure Random Number -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Whenever you need to generate a secure random number, you are highly -encouraged to use the Symfony -:class:`Symfony\\Component\\Security\\Core\\Util\\SecureRandom` class:: - - use Symfony\Component\Security\Core\Util\SecureRandom; - - $generator = new SecureRandom(); - $random = $generator->nextBytes(10); - -The -:method:`Symfony\\Component\\Security\\Core\\Util\\SecureRandom::nextBytes` -methods returns a random string composed of the number of characters passed as -an argument (10 in the above example). - -The SecureRandom class works better when OpenSSL is installed but when it's -not available, it falls back to an internal algorithm, which needs a seed file -to work correctly. Just pass a file name to enable it:: - - $generator = new SecureRandom('/some/path/to/store/the/seed.txt'); - $random = $generator->nextBytes(10); - -.. note:: - - You can also access a secure random instance directly from the Symfony - dependency injection container; its name is ``security.secure_random``. - -Final Words ------------ - -Security can be a deep and complex issue to solve correctly in your application. -Fortunately, Symfony's security component follows a well-proven security -model based around *authentication* and *authorization*. Authentication, -which always happens first, is handled by a firewall whose job is to determine -the identity of the user through several different methods (e.g. HTTP authentication, -login form, etc). In the cookbook, you'll find examples of other methods -for handling authentication, including how to implement a "remember me" cookie -functionality. - -Once a user is authenticated, the authorization layer can determine whether -or not the user should have access to a specific resource. Most commonly, -*roles* are applied to URLs, classes or methods and if the current user -doesn't have that role, access is denied. The authorization layer, however, -is much deeper, and follows a system of "voting" so that multiple parties -can determine if the current user should have access to a given resource. -Find out more about this and other topics in the cookbook. - -Learn more from the Cookbook ----------------------------- - -* :doc:`Forcing HTTP/HTTPS ` -* :doc:`Blacklist users by IP address with a custom voter ` -* :doc:`Access Control Lists (ACLs) ` -* :doc:`/cookbook/security/remember_me` - -.. _`Symfony's security component`: https://github.com/symfony/Security -.. _`JMSSecurityExtraBundle`: http://jmsyst.com/bundles/JMSSecurityExtraBundle/1.2 -.. _`FOSUserBundle`: https://github.com/FriendsOfSymfony/FOSUserBundle -.. _`implement the \Serializable interface`: http://php.net/manual/en/class.serializable.php -.. _`functions-online.com`: http://www.functions-online.com/sha1.html -.. _`Timing attack`: http://en.wikipedia.org/wiki/Timing_attack diff --git a/book/service_container.rst b/book/service_container.rst deleted file mode 100644 index 1de05025ee0..00000000000 --- a/book/service_container.rst +++ /dev/null @@ -1,956 +0,0 @@ -.. index:: - single: Service Container - single: Dependency Injection; Container - -Service Container -================= - -A modern PHP application is full of objects. One object may facilitate the -delivery of email messages while another may allow you to persist information -into a database. In your application, you may create an object that manages -your product inventory, or another object that processes data from a third-party -API. The point is that a modern application does many things and is organized -into many objects that handle each task. - -This chapter is about a special PHP object in Symfony2 that helps -you instantiate, organize and retrieve the many objects of your application. -This object, called a service container, will allow you to standardize and -centralize the way objects are constructed in your application. The container -makes your life easier, is super fast, and emphasizes an architecture that -promotes reusable and decoupled code. Since all core Symfony2 classes -use the container, you'll learn how to extend, configure and use any object -in Symfony2. In large part, the service container is the biggest contributor -to the speed and extensibility of Symfony2. - -Finally, configuring and using the service container is easy. By the end -of this chapter, you'll be comfortable creating your own objects via the -container and customizing objects from any third-party bundle. You'll begin -writing code that is more reusable, testable and decoupled, simply because -the service container makes writing good code so easy. - -.. tip:: - - If you want to know a lot more after reading this chapter, check out - the :doc:`Dependency Injection Component Documentation`. - -.. index:: - single: Service Container; What is a service? - -What is a Service? ------------------- - -Put simply, a :term:`Service` is any PHP object that performs some sort of -"global" task. It's a purposefully-generic name used in computer science -to describe an object that's created for a specific purpose (e.g. delivering -emails). Each service is used throughout your application whenever you need -the specific functionality it provides. You don't have to do anything special -to make a service: simply write a PHP class with some code that accomplishes -a specific task. Congratulations, you've just created a service! - -.. note:: - - As a rule, a PHP object is a service if it is used globally in your - application. A single ``Mailer`` service is used globally to send - email messages whereas the many ``Message`` objects that it delivers - are *not* services. Similarly, a ``Product`` object is not a service, - but an object that persists ``Product`` objects to a database *is* a service. - -So what's the big deal then? The advantage of thinking about "services" is -that you begin to think about separating each piece of functionality in your -application into a series of services. Since each service does just one job, -you can easily access each service and use its functionality wherever you -need it. Each service can also be more easily tested and configured since -it's separated from the other functionality in your application. This idea -is called `service-oriented architecture`_ and is not unique to Symfony2 -or even PHP. Structuring your application around a set of independent service -classes is a well-known and trusted object-oriented best-practice. These skills -are key to being a good developer in almost any language. - -.. index:: - single: Service Container; What is a service container? - -What is a Service Container? ----------------------------- - -A :term:`Service Container` (or *dependency injection container*) is simply -a PHP object that manages the instantiation of services (i.e. objects). - -For example, suppose you have a simple PHP class that delivers email messages. -Without a service container, you must manually create the object whenever -you need it:: - - use Acme\HelloBundle\Mailer; - - $mailer = new Mailer('sendmail'); - $mailer->send('ryan@foobar.net', ...); - -This is easy enough. The imaginary ``Mailer`` class allows you to configure -the method used to deliver the email messages (e.g. ``sendmail``, ``smtp``, etc). -But what if you wanted to use the mailer service somewhere else? You certainly -don't want to repeat the mailer configuration *every* time you need to use -the ``Mailer`` object. What if you needed to change the ``transport`` from -``sendmail`` to ``smtp`` everywhere in the application? You'd need to hunt -down every place you create a ``Mailer`` service and change it. - -.. index:: - single: Service Container; Configuring services - -Creating/Configuring Services in the Container ----------------------------------------------- - -A better answer is to let the service container create the ``Mailer`` object -for you. In order for this to work, you must *teach* the container how to -create the ``Mailer`` service. This is done via configuration, which can -be specified in YAML, XML or PHP: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - services: - my_mailer: - class: Acme\HelloBundle\Mailer - arguments: [sendmail] - - .. code-block:: xml - - - - - sendmail - - - - .. code-block:: php - - // app/config/config.php - use Symfony\Component\DependencyInjection\Definition; - - $container->setDefinition('my_mailer', new Definition( - 'Acme\HelloBundle\Mailer', - array('sendmail') - )); - -.. note:: - - When Symfony2 initializes, it builds the service container using the - application configuration (``app/config/config.yml`` by default). The - exact file that's loaded is dictated by the ``AppKernel::registerContainerConfiguration()`` - method, which loads an environment-specific configuration file (e.g. - ``config_dev.yml`` for the ``dev`` environment or ``config_prod.yml`` - for ``prod``). - -An instance of the ``Acme\HelloBundle\Mailer`` object is now available via -the service container. The container is available in any traditional Symfony2 -controller where you can access the services of the container via the ``get()`` -shortcut method:: - - class HelloController extends Controller - { - // ... - - public function sendEmailAction() - { - // ... - $mailer = $this->get('my_mailer'); - $mailer->send('ryan@foobar.net', ...); - } - } - -When you ask for the ``my_mailer`` service from the container, the container -constructs the object and returns it. This is another major advantage of -using the service container. Namely, a service is *never* constructed until -it's needed. If you define a service and never use it on a request, the service -is never created. This saves memory and increases the speed of your application. -This also means that there's very little or no performance hit for defining -lots of services. Services that are never used are never constructed. - -As an added bonus, the ``Mailer`` service is only created once and the same -instance is returned each time you ask for the service. This is almost always -the behavior you'll need (it's more flexible and powerful), but you'll learn -later how you can configure a service that has multiple instances in the -":doc:`/cookbook/service_container/scopes`" cookbook article. - -.. _book-service-container-parameters: - -Service Parameters ------------------- - -The creation of new services (i.e. objects) via the container is pretty -straightforward. Parameters make defining services more organized and flexible: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - parameters: - my_mailer.class: Acme\HelloBundle\Mailer - my_mailer.transport: sendmail - - services: - my_mailer: - class: "%my_mailer.class%" - arguments: ["%my_mailer.transport%"] - - .. code-block:: xml - - - - Acme\HelloBundle\Mailer - sendmail - - - - - %my_mailer.transport% - - - - .. code-block:: php - - // app/config/config.php - use Symfony\Component\DependencyInjection\Definition; - - $container->setParameter('my_mailer.class', 'Acme\HelloBundle\Mailer'); - $container->setParameter('my_mailer.transport', 'sendmail'); - - $container->setDefinition('my_mailer', new Definition( - '%my_mailer.class%', - array('%my_mailer.transport%') - )); - -The end result is exactly the same as before - the difference is only in -*how* you defined the service. By surrounding the ``my_mailer.class`` and -``my_mailer.transport`` strings in percent (``%``) signs, the container knows -to look for parameters with those names. When the container is built, it -looks up the value of each parameter and uses it in the service definition. - -.. versionadded:: 2.1 - Escaping the ``@`` character in YAML parameter values is new in Symfony 2.1.9 - and Symfony 2.2.1. - -.. note:: - - If you want to use a string that starts with an ``@`` sign as a parameter - value (i.e. a very safe mailer password) in a yaml file, you need to escape - it by adding another ``@`` sign (This only applies to the YAML format): - - .. code-block:: yaml - - # app/config/parameters.yml - parameters: - # This will be parsed as string "@securepass" - mailer_password: "@@securepass" - -.. note:: - - The percent sign inside a parameter or argument, as part of the string, must - be escaped with another percent sign: - - .. code-block:: xml - - http://symfony.com/?foo=%%s&bar=%%d - -.. caution:: - - You may receive a - :class:`Symfony\\Component\\DependencyInjection\\Exception\\ScopeWideningInjectionException` - when passing the ``request`` service as an argument. To understand this - problem better and learn how to solve it, refer to the cookbook article - :doc:`/cookbook/service_container/scopes`. - -The purpose of parameters is to feed information into services. Of course -there was nothing wrong with defining the service without using any parameters. -Parameters, however, have several advantages: - -* separation and organization of all service "options" under a single - ``parameters`` key; - -* parameter values can be used in multiple service definitions; - -* when creating a service in a bundle (this follows shortly), using parameters - allows the service to be easily customized in your application. - -The choice of using or not using parameters is up to you. High-quality -third-party bundles will *always* use parameters as they make the service -stored in the container more configurable. For the services in your application, -however, you may not need the flexibility of parameters. - -Array Parameters -~~~~~~~~~~~~~~~~ - -Parameters can also contain array values. See :ref:`component-di-parameters-array`. - -Importing other Container Configuration Resources -------------------------------------------------- - -.. tip:: - - In this section, service configuration files are referred to as *resources*. - This is to highlight the fact that, while most configuration resources - will be files (e.g. YAML, XML, PHP), Symfony2 is so flexible that configuration - could be loaded from anywhere (e.g. a database or even via an external - web service). - -The service container is built using a single configuration resource -(``app/config/config.yml`` by default). All other service configuration -(including the core Symfony2 and third-party bundle configuration) must -be imported from inside this file in one way or another. This gives you absolute -flexibility over the services in your application. - -External service configuration can be imported in two different ways. The -first - and most common method - is via the ``imports`` directive. Later, you'll -learn about the second method, which is the flexible and preferred method -for importing service configuration from third-party bundles. - -.. index:: - single: Service Container; Imports - -.. _service-container-imports-directive: - -Importing Configuration with ``imports`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -So far, you've placed your ``my_mailer`` service container definition directly -in the application configuration file (e.g. ``app/config/config.yml``). Of -course, since the ``Mailer`` class itself lives inside the ``AcmeHelloBundle``, -it makes more sense to put the ``my_mailer`` container definition inside the -bundle as well. - -First, move the ``my_mailer`` container definition into a new container resource -file inside ``AcmeHelloBundle``. If the ``Resources`` or ``Resources/config`` -directories don't exist, create them. - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/HelloBundle/Resources/config/services.yml - parameters: - my_mailer.class: Acme\HelloBundle\Mailer - my_mailer.transport: sendmail - - services: - my_mailer: - class: "%my_mailer.class%" - arguments: ["%my_mailer.transport%"] - - .. code-block:: xml - - - - Acme\HelloBundle\Mailer - sendmail - - - - - %my_mailer.transport% - - - - .. code-block:: php - - // src/Acme/HelloBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; - - $container->setParameter('my_mailer.class', 'Acme\HelloBundle\Mailer'); - $container->setParameter('my_mailer.transport', 'sendmail'); - - $container->setDefinition('my_mailer', new Definition( - '%my_mailer.class%', - array('%my_mailer.transport%') - )); - -The definition itself hasn't changed, only its location. Of course the service -container doesn't know about the new resource file. Fortunately, you can -easily import the resource file using the ``imports`` key in the application -configuration. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - imports: - - { resource: "@AcmeHelloBundle/Resources/config/services.yml" } - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $this->import('@AcmeHelloBundle/Resources/config/services.php'); - -The ``imports`` directive allows your application to include service container -configuration resources from any other location (most commonly from bundles). -The ``resource`` location, for files, is the absolute path to the resource -file. The special ``@AcmeHello`` syntax resolves the directory path of -the ``AcmeHelloBundle`` bundle. This helps you specify the path to the resource -without worrying later if you move the ``AcmeHelloBundle`` to a different -directory. - -.. index:: - single: Service Container; Extension configuration - -.. _service-container-extension-configuration: - -Importing Configuration via Container Extensions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When developing in Symfony2, you'll most commonly use the ``imports`` directive -to import container configuration from the bundles you've created specifically -for your application. Third-party bundle container configuration, including -Symfony2 core services, are usually loaded using another method that's more -flexible and easy to configure in your application. - -Here's how it works. Internally, each bundle defines its services very much -like you've seen so far. Namely, a bundle uses one or more configuration -resource files (usually XML) to specify the parameters and services for that -bundle. However, instead of importing each of these resources directly from -your application configuration using the ``imports`` directive, you can simply -invoke a *service container extension* inside the bundle that does the work for -you. A service container extension is a PHP class created by the bundle author -to accomplish two things: - -* import all service container resources needed to configure the services for - the bundle; - -* provide semantic, straightforward configuration so that the bundle can - be configured without interacting with the flat parameters of the bundle's - service container configuration. - -In other words, a service container extension configures the services for -a bundle on your behalf. And as you'll see in a moment, the extension provides -a sensible, high-level interface for configuring the bundle. - -Take the ``FrameworkBundle`` - the core Symfony2 framework bundle - as an -example. The presence of the following code in your application configuration -invokes the service container extension inside the ``FrameworkBundle``: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - secret: xxxxxxxxxx - form: true - csrf_protection: true - router: { resource: "%kernel.root_dir%/config/routing.yml" } - # ... - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - 'secret' => 'xxxxxxxxxx', - 'form' => array(), - 'csrf-protection' => array(), - 'router' => array('resource' => '%kernel.root_dir%/config/routing.php'), - - // ... - )); - -When the configuration is parsed, the container looks for an extension that -can handle the ``framework`` configuration directive. The extension in question, -which lives in the ``FrameworkBundle``, is invoked and the service configuration -for the ``FrameworkBundle`` is loaded. If you remove the ``framework`` key -from your application configuration file entirely, the core Symfony2 services -won't be loaded. The point is that you're in control: the Symfony2 framework -doesn't contain any magic or perform any actions that you don't have control -over. - -Of course you can do much more than simply "activate" the service container -extension of the ``FrameworkBundle``. Each extension allows you to easily -customize the bundle, without worrying about how the internal services are -defined. - -In this case, the extension allows you to customize the ``error_handler``, -``csrf_protection``, ``router`` configuration and much more. Internally, -the ``FrameworkBundle`` uses the options specified here to define and configure -the services specific to it. The bundle takes care of creating all the necessary -``parameters`` and ``services`` for the service container, while still allowing -much of the configuration to be easily customized. As an added bonus, most -service container extensions are also smart enough to perform validation - -notifying you of options that are missing or the wrong data type. - -When installing or configuring a bundle, see the bundle's documentation for -how the services for the bundle should be installed and configured. The options -available for the core bundles can be found inside the :doc:`Reference Guide`. - -.. note:: - - Natively, the service container only recognizes the ``parameters``, - ``services``, and ``imports`` directives. Any other directives - are handled by a service container extension. - -If you want to expose user friendly configuration in your own bundles, read the -":doc:`/cookbook/bundles/extension`" cookbook recipe. - -.. index:: - single: Service Container; Referencing services - -Referencing (Injecting) Services --------------------------------- - -So far, the original ``my_mailer`` service is simple: it takes just one argument -in its constructor, which is easily configurable. As you'll see, the real -power of the container is realized when you need to create a service that -depends on one or more other services in the container. - -As an example, suppose you have a new service, ``NewsletterManager``, -that helps to manage the preparation and delivery of an email message to -a collection of addresses. Of course the ``my_mailer`` service is already -really good at delivering email messages, so you'll use it inside ``NewsletterManager`` -to handle the actual delivery of the messages. This pretend class might look -something like this:: - - // src/Acme/HelloBundle/Newsletter/NewsletterManager.php - namespace Acme\HelloBundle\Newsletter; - - use Acme\HelloBundle\Mailer; - - class NewsletterManager - { - protected $mailer; - - public function __construct(Mailer $mailer) - { - $this->mailer = $mailer; - } - - // ... - } - -Without using the service container, you can create a new ``NewsletterManager`` -fairly easily from inside a controller:: - - use Acme\HelloBundle\Newsletter\NewsletterManager; - - // ... - - public function sendNewsletterAction() - { - $mailer = $this->get('my_mailer'); - $newsletter = new NewsletterManager($mailer); - // ... - } - -This approach is fine, but what if you decide later that the ``NewsletterManager`` -class needs a second or third constructor argument? What if you decide to -refactor your code and rename the class? In both cases, you'd need to find every -place where the ``NewsletterManager`` is instantiated and modify it. Of course, -the service container gives you a much more appealing option: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/HelloBundle/Resources/config/services.yml - parameters: - # ... - newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager - - services: - my_mailer: - # ... - newsletter_manager: - class: "%newsletter_manager.class%" - arguments: ["@my_mailer"] - - .. code-block:: xml - - - - - Acme\HelloBundle\Newsletter\NewsletterManager - - - - - - - - - - - - .. code-block:: php - - // src/Acme/HelloBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; - - // ... - $container->setParameter( - 'newsletter_manager.class', - 'Acme\HelloBundle\Newsletter\NewsletterManager' - ); - - $container->setDefinition('my_mailer', ...); - $container->setDefinition('newsletter_manager', new Definition( - '%newsletter_manager.class%', - array(new Reference('my_mailer')) - )); - -In YAML, the special ``@my_mailer`` syntax tells the container to look for -a service named ``my_mailer`` and to pass that object into the constructor -of ``NewsletterManager``. In this case, however, the specified service ``my_mailer`` -must exist. If it does not, an exception will be thrown. You can mark your -dependencies as optional - this will be discussed in the next section. - -Using references is a very powerful tool that allows you to create independent service -classes with well-defined dependencies. In this example, the ``newsletter_manager`` -service needs the ``my_mailer`` service in order to function. When you define -this dependency in the service container, the container takes care of all -the work of instantiating the objects. - -Optional Dependencies: Setter Injection -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Injecting dependencies into the constructor in this manner is an excellent -way of ensuring that the dependency is available to use. If you have optional -dependencies for a class, then "setter injection" may be a better option. This -means injecting the dependency using a method call rather than through the -constructor. The class would look like this:: - - namespace Acme\HelloBundle\Newsletter; - - use Acme\HelloBundle\Mailer; - - class NewsletterManager - { - protected $mailer; - - public function setMailer(Mailer $mailer) - { - $this->mailer = $mailer; - } - - // ... - } - -Injecting the dependency by the setter method just needs a change of syntax: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/HelloBundle/Resources/config/services.yml - parameters: - # ... - newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager - - services: - my_mailer: - # ... - newsletter_manager: - class: "%newsletter_manager.class%" - calls: - - [setMailer, ["@my_mailer"]] - - .. code-block:: xml - - - - - Acme\HelloBundle\Newsletter\NewsletterManager - - - - - - - - - - - - - - .. code-block:: php - - // src/Acme/HelloBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; - - // ... - $container->setParameter( - 'newsletter_manager.class', - 'Acme\HelloBundle\Newsletter\NewsletterManager' - ); - - $container->setDefinition('my_mailer', ...); - $container->setDefinition('newsletter_manager', new Definition( - '%newsletter_manager.class%' - ))->addMethodCall('setMailer', array( - new Reference('my_mailer'), - )); - -.. note:: - - The approaches presented in this section are called "constructor injection" - and "setter injection". The Symfony2 service container also supports - "property injection". - -Making References Optional --------------------------- - -Sometimes, one of your services may have an optional dependency, meaning -that the dependency is not required for your service to work properly. In -the example above, the ``my_mailer`` service *must* exist, otherwise an exception -will be thrown. By modifying the ``newsletter_manager`` service definition, -you can make this reference optional. The container will then inject it if -it exists and do nothing if it doesn't: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/HelloBundle/Resources/config/services.yml - parameters: - # ... - - services: - newsletter_manager: - class: "%newsletter_manager.class%" - arguments: ["@?my_mailer"] - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // src/Acme/HelloBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; - use Symfony\Component\DependencyInjection\ContainerInterface; - - // ... - $container->setParameter( - 'newsletter_manager.class', - 'Acme\HelloBundle\Newsletter\NewsletterManager' - ); - - $container->setDefinition('my_mailer', ...); - $container->setDefinition('newsletter_manager', new Definition( - '%newsletter_manager.class%', - array( - new Reference( - 'my_mailer', - ContainerInterface::IGNORE_ON_INVALID_REFERENCE - ) - ) - )); - -In YAML, the special ``@?`` syntax tells the service container that the dependency -is optional. Of course, the ``NewsletterManager`` must also be written to -allow for an optional dependency:: - - public function __construct(Mailer $mailer = null) - { - // ... - } - -Core Symfony and Third-Party Bundle Services --------------------------------------------- - -Since Symfony2 and all third-party bundles configure and retrieve their services -via the container, you can easily access them or even use them in your own -services. To keep things simple, Symfony2 by default does not require that -controllers be defined as services. Furthermore Symfony2 injects the entire -service container into your controller. For example, to handle the storage of -information on a user's session, Symfony2 provides a ``session`` service, -which you can access inside a standard controller as follows:: - - public function indexAction($bar) - { - $session = $this->get('session'); - $session->set('foo', $bar); - - // ... - } - -In Symfony2, you'll constantly use services provided by the Symfony core or -other third-party bundles to perform tasks such as rendering templates (``templating``), -sending emails (``mailer``), or accessing information on the request (``request``). - -You can take this a step further by using these services inside services that -you've created for your application. Beginning by modifying the ``NewsletterManager`` -to use the real Symfony2 ``mailer`` service (instead of the pretend ``my_mailer``). -Also pass the templating engine service to the ``NewsletterManager`` -so that it can generate the email content via a template:: - - namespace Acme\HelloBundle\Newsletter; - - use Symfony\Component\Templating\EngineInterface; - - class NewsletterManager - { - protected $mailer; - - protected $templating; - - public function __construct(\Swift_Mailer $mailer, EngineInterface $templating) - { - $this->mailer = $mailer; - $this->templating = $templating; - } - - // ... - } - -Configuring the service container is easy: - -.. configuration-block:: - - .. code-block:: yaml - - services: - newsletter_manager: - class: "%newsletter_manager.class%" - arguments: ["@mailer", "@templating"] - - .. code-block:: xml - - - - - - - .. code-block:: php - - $container->setDefinition('newsletter_manager', new Definition( - '%newsletter_manager.class%', - array( - new Reference('mailer'), - new Reference('templating'), - ) - )); - -The ``newsletter_manager`` service now has access to the core ``mailer`` -and ``templating`` services. This is a common way to create services specific -to your application that leverage the power of different services within -the framework. - -.. tip:: - - Be sure that the ``swiftmailer`` entry appears in your application - configuration. As was mentioned in :ref:`service-container-extension-configuration`, - the ``swiftmailer`` key invokes the service extension from the - ``SwiftmailerBundle``, which registers the ``mailer`` service. - -.. _book-service-container-tags: - -Tags ----- - -In the same way that a blog post on the Web might be tagged with things such -as "Symfony" or "PHP", services configured in your container can also be -tagged. In the service container, a tag implies that the service is meant -to be used for a specific purpose. Take the following example: - -.. configuration-block:: - - .. code-block:: yaml - - services: - foo.twig.extension: - class: Acme\HelloBundle\Extension\FooExtension - tags: - - { name: twig.extension } - - .. code-block:: xml - - - - - - .. code-block:: php - - $definition = new Definition('Acme\HelloBundle\Extension\FooExtension'); - $definition->addTag('twig.extension'); - $container->setDefinition('foo.twig.extension', $definition); - -The ``twig.extension`` tag is a special tag that the ``TwigBundle`` uses -during configuration. By giving the service this ``twig.extension`` tag, -the bundle knows that the ``foo.twig.extension`` service should be registered -as a Twig extension with Twig. In other words, Twig finds all services tagged -with ``twig.extension`` and automatically registers them as extensions. - -Tags, then, are a way to tell Symfony2 or other third-party bundles that -your service should be registered or used in some special way by the bundle. - -The following is a list of tags available with the core Symfony2 bundles. -Each of these has a different effect on your service and many tags require -additional arguments (beyond just the ``name`` parameter). - -For a list of all the tags available in the core Symfony Framework, check -out :doc:`/reference/dic_tags`. - -Debugging Services ------------------- - -You can find out what services are registered with the container using the -console. To show all services and the class for each service, run: - -.. code-block:: bash - - $ php app/console container:debug - -By default only public services are shown, but you can also view private services: - -.. code-block:: bash - - $ php app/console container:debug --show-private - -You can get more detailed information about a particular service by specifying -its id: - -.. code-block:: bash - - $ php app/console container:debug my_mailer - -Learn more ----------- - -* :doc:`/components/dependency_injection/parameters` -* :doc:`/components/dependency_injection/compilation` -* :doc:`/components/dependency_injection/definitions` -* :doc:`/components/dependency_injection/factories` -* :doc:`/components/dependency_injection/parentservices` -* :doc:`/components/dependency_injection/tags` -* :doc:`/cookbook/controller/service` -* :doc:`/cookbook/service_container/scopes` -* :doc:`/cookbook/service_container/compiler_passes` -* :doc:`/components/dependency_injection/advanced` - -.. _`service-oriented architecture`: http://wikipedia.org/wiki/Service-oriented_architecture diff --git a/book/stable_api.rst b/book/stable_api.rst deleted file mode 100644 index d7fbdaf481e..00000000000 --- a/book/stable_api.rst +++ /dev/null @@ -1,43 +0,0 @@ -.. index:: - single: Stable API - -The Symfony2 Stable API -======================= - -The Symfony2 stable API is a subset of all Symfony2 published public methods -(components and core bundles) that share the following properties: - -* The namespace and class name won't change; -* The method name won't change; -* The method signature (arguments and return value type) won't change; -* The semantic of what the method does won't change. - -The implementation itself can change though. The only valid case for a change -in the stable API is in order to fix a security issue. - -The stable API is based on a whitelist, tagged with `@api`. Therefore, -everything not tagged explicitly is not part of the stable API. - -.. tip:: - - Any third party bundle should also publish its own stable API. - -As of Symfony 2.0, the following components have a public tagged API: - -* BrowserKit -* ClassLoader -* Console -* CssSelector -* DependencyInjection -* DomCrawler -* EventDispatcher -* Finder -* HttpFoundation -* HttpKernel -* Locale -* Process -* Routing -* Templating -* Translation -* Validator -* Yaml diff --git a/book/templating.rst b/book/templating.rst deleted file mode 100644 index 656f790f262..00000000000 --- a/book/templating.rst +++ /dev/null @@ -1,1554 +0,0 @@ -.. index:: - single: Templating - -Creating and using Templates -============================ - -As you know, the :doc:`controller ` is responsible for -handling each request that comes into a Symfony2 application. In reality, -the controller delegates the most of the heavy work to other places so that -code can be tested and reused. When a controller needs to generate HTML, -CSS or any other content, it hands the work off to the templating engine. -In this chapter, you'll learn how to write powerful templates that can be -used to return content to the user, populate email bodies, and more. You'll -learn shortcuts, clever ways to extend templates and how to reuse template -code. - -.. note:: - - How to render templates is covered in the :ref:`controller ` - page of the book. - - -.. index:: - single: Templating; What is a template? - -Templates ---------- - -A template is simply a text file that can generate any text-based format -(HTML, XML, CSV, LaTeX ...). The most familiar type of template is a *PHP* -template - a text file parsed by PHP that contains a mix of text and PHP code: - -.. code-block:: html+php - - - - - Welcome to Symfony! - - -

- - - - - -.. index:: Twig; Introduction - -But Symfony2 packages an even more powerful templating language called `Twig`_. -Twig allows you to write concise, readable templates that are more friendly -to web designers and, in several ways, more powerful than PHP templates: - -.. code-block:: html+jinja - - - - - Welcome to Symfony! - - -

{{ page_title }}

- - - - - -Twig defines two types of special syntax: - -* ``{{ ... }}``: "Says something": prints a variable or the result of an - expression to the template; - -* ``{% ... %}``: "Does something": a **tag** that controls the logic of the - template; it is used to execute statements such as for-loops for example. - -.. note:: - - There is a third syntax used for creating comments: ``{# this is a comment #}``. - This syntax can be used across multiple lines like the PHP-equivalent - ``/* comment */`` syntax. - -Twig also contains **filters**, which modify content before being rendered. -The following makes the ``title`` variable all uppercase before rendering -it: - -.. code-block:: jinja - - {{ title|upper }} - -Twig comes with a long list of `tags`_ and `filters`_ that are available -by default. You can even `add your own extensions`_ to Twig as needed. - -.. tip:: - - Registering a Twig extension is as easy as creating a new service and tagging - it with ``twig.extension`` :ref:`tag`. - -As you'll see throughout the documentation, Twig also supports functions -and new functions can be easily added. For example, the following uses a -standard ``for`` tag and the ``cycle`` function to print ten div tags, with -alternating ``odd``, ``even`` classes: - -.. code-block:: html+jinja - - {% for i in 0..10 %} -
- -
- {% endfor %} - -Throughout this chapter, template examples will be shown in both Twig and PHP. - -.. tip:: - - If you *do* choose to not use Twig and you disable it, you'll need to implement - your own exception handler via the ``kernel.exception`` event. - -.. sidebar:: Why Twig? - - Twig templates are meant to be simple and won't process PHP tags. This - is by design: the Twig template system is meant to express presentation, - not program logic. The more you use Twig, the more you'll appreciate - and benefit from this distinction. And of course, you'll be loved by - web designers everywhere. - - Twig can also do things that PHP can't, such as whitespace control, - sandboxing, automatic and contextual output escaping, and the inclusion of - custom functions and filters that only affect templates. Twig contains - little features that make writing templates easier and more concise. Take - the following example, which combines a loop with a logical ``if`` - statement: - - .. code-block:: html+jinja - -
    - {% for user in users if user.active %} -
  • {{ user.username }}
  • - {% else %} -
  • No users found
  • - {% endfor %} -
- -.. index:: - pair: Twig; Cache - -Twig Template Caching -~~~~~~~~~~~~~~~~~~~~~ - -Twig is fast. Each Twig template is compiled down to a native PHP class -that is rendered at runtime. The compiled classes are located in the -``app/cache/{environment}/twig`` directory (where ``{environment}`` is the -environment, such as ``dev`` or ``prod``) and in some cases can be useful -while debugging. See :ref:`environments-summary` for more information on -environments. - -When ``debug`` mode is enabled (common in the ``dev`` environment), a Twig -template will be automatically recompiled when changes are made to it. This -means that during development you can happily make changes to a Twig template -and instantly see the changes without needing to worry about clearing any -cache. - -When ``debug`` mode is disabled (common in the ``prod`` environment), however, -you must clear the Twig cache directory so that the Twig templates will -regenerate. Remember to do this when deploying your application. - -.. index:: - single: Templating; Inheritance - -Template Inheritance and Layouts --------------------------------- - -More often than not, templates in a project share common elements, like the -header, footer, sidebar or more. In Symfony2, this problem is thought about -differently: a template can be decorated by another one. This works -exactly the same as PHP classes: template inheritance allows you to build -a base "layout" template that contains all the common elements of your site -defined as **blocks** (think "PHP class with base methods"). A child template -can extend the base layout and override any of its blocks (think "PHP subclass -that overrides certain methods of its parent class"). - -First, build a base layout file: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# app/Resources/views/base.html.twig #} - - - - - {% block title %}Test Application{% endblock %} - - - - -
- {% block body %}{% endblock %} -
- - - - .. code-block:: html+php - - - - - - - <?php $view['slots']->output('title', 'Test Application') ?> - - - - -
- output('body') ?> -
- - - -.. note:: - - Though the discussion about template inheritance will be in terms of Twig, - the philosophy is the same between Twig and PHP templates. - -This template defines the base HTML skeleton document of a simple two-column -page. In this example, three ``{% block %}`` areas are defined (``title``, -``sidebar`` and ``body``). Each block may be overridden by a child template -or left with its default implementation. This template could also be rendered -directly. In that case the ``title``, ``sidebar`` and ``body`` blocks would -simply retain the default values used in this template. - -A child template might look like this: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/BlogBundle/Resources/views/Blog/index.html.twig #} - {% extends '::base.html.twig' %} - - {% block title %}My cool blog posts{% endblock %} - - {% block body %} - {% for entry in blog_entries %} -

{{ entry.title }}

-

{{ entry.body }}

- {% endfor %} - {% endblock %} - - .. code-block:: html+php - - - extend('::base.html.php') ?> - - set('title', 'My cool blog posts') ?> - - start('body') ?> - -

getTitle() ?>

-

getBody() ?>

- - stop() ?> - -.. note:: - - The parent template is identified by a special string syntax - (``::base.html.twig``) that indicates that the template lives in the - ``app/Resources/views`` directory of the project. This naming convention is - explained fully in :ref:`template-naming-locations`. - -The key to template inheritance is the ``{% extends %}`` tag. This tells -the templating engine to first evaluate the base template, which sets up -the layout and defines several blocks. The child template is then rendered, -at which point the ``title`` and ``body`` blocks of the parent are replaced -by those from the child. Depending on the value of ``blog_entries``, the -output might look like this: - -.. code-block:: html - - - - - - My cool blog posts - - - - -
-

My first post

-

The body of the first post.

- -

Another post

-

The body of the second post.

-
- - - -Notice that since the child template didn't define a ``sidebar`` block, the -value from the parent template is used instead. Content within a ``{% block %}`` -tag in a parent template is always used by default. - -You can use as many levels of inheritance as you want. In the next section, -a common three-level inheritance model will be explained along with how templates -are organized inside a Symfony2 project. - -When working with template inheritance, here are some tips to keep in mind: - -* If you use ``{% extends %}`` in a template, it must be the first tag in - that template; - -* The more ``{% block %}`` tags you have in your base templates, the better. - Remember, child templates don't have to define all parent blocks, so create - as many blocks in your base templates as you want and give each a sensible - default. The more blocks your base templates have, the more flexible your - layout will be; - -* If you find yourself duplicating content in a number of templates, it probably - means you should move that content to a ``{% block %}`` in a parent template. - In some cases, a better solution may be to move the content to a new template - and ``include`` it (see :ref:`including-templates`); - -* If you need to get the content of a block from the parent template, you - can use the ``{{ parent() }}`` function. This is useful if you want to add - to the contents of a parent block instead of completely overriding it: - - .. code-block:: html+jinja - - {% block sidebar %} -

Table of Contents

- - {# ... #} - - {{ parent() }} - {% endblock %} - -.. index:: - single: Templating; Naming conventions - single: Templating; File locations - -.. _template-naming-locations: - -Template Naming and Locations ------------------------------ - -.. versionadded:: 2.2 - Namespaced path support was added in 2.2, allowing for template names - like ``@AcmeDemo/layout.html.twig``. See :doc:`/cookbook/templating/namespaced_paths` - for more details. - -By default, templates can live in two different locations: - -* ``app/Resources/views/``: The applications ``views`` directory can contain - application-wide base templates (i.e. your application's layouts) as well as - templates that override bundle templates (see - :ref:`overriding-bundle-templates`); - -* ``path/to/bundle/Resources/views/``: Each bundle houses its templates in its - ``Resources/views`` directory (and subdirectories). The majority of templates - will live inside a bundle. - -Symfony2 uses a **bundle**:**controller**:**template** string syntax for -templates. This allows for several different types of templates, each which -lives in a specific location: - -* ``AcmeBlogBundle:Blog:index.html.twig``: This syntax is used to specify a - template for a specific page. The three parts of the string, each separated - by a colon (``:``), mean the following: - - * ``AcmeBlogBundle``: (*bundle*) the template lives inside the - ``AcmeBlogBundle`` (e.g. ``src/Acme/BlogBundle``); - - * ``Blog``: (*controller*) indicates that the template lives inside the - ``Blog`` subdirectory of ``Resources/views``; - - * ``index.html.twig``: (*template*) the actual name of the file is - ``index.html.twig``. - - Assuming that the ``AcmeBlogBundle`` lives at ``src/Acme/BlogBundle``, the - final path to the layout would be ``src/Acme/BlogBundle/Resources/views/Blog/index.html.twig``. - -* ``AcmeBlogBundle::layout.html.twig``: This syntax refers to a base template - that's specific to the ``AcmeBlogBundle``. Since the middle, "controller", - portion is missing (e.g. ``Blog``), the template lives at - ``Resources/views/layout.html.twig`` inside ``AcmeBlogBundle``. - -* ``::base.html.twig``: This syntax refers to an application-wide base template - or layout. Notice that the string begins with two colons (``::``), meaning - that both the *bundle* and *controller* portions are missing. This means - that the template is not located in any bundle, but instead in the root - ``app/Resources/views/`` directory. - -In the :ref:`overriding-bundle-templates` section, you'll find out how each -template living inside the ``AcmeBlogBundle``, for example, can be overridden -by placing a template of the same name in the ``app/Resources/AcmeBlogBundle/views/`` -directory. This gives the power to override templates from any vendor bundle. - -.. tip:: - - Hopefully the template naming syntax looks familiar - it's the same naming - convention used to refer to :ref:`controller-string-syntax`. - -Template Suffix -~~~~~~~~~~~~~~~ - -The **bundle**:**controller**:**template** format of each template specifies -*where* the template file is located. Every template name also has two extensions -that specify the *format* and *engine* for that template. - -* **AcmeBlogBundle:Blog:index.html.twig** - HTML format, Twig engine - -* **AcmeBlogBundle:Blog:index.html.php** - HTML format, PHP engine - -* **AcmeBlogBundle:Blog:index.css.twig** - CSS format, Twig engine - -By default, any Symfony2 template can be written in either Twig or PHP, and -the last part of the extension (e.g. ``.twig`` or ``.php``) specifies which -of these two *engines* should be used. The first part of the extension, -(e.g. ``.html``, ``.css``, etc) is the final format that the template will -generate. Unlike the engine, which determines how Symfony2 parses the template, -this is simply an organizational tactic used in case the same resource needs -to be rendered as HTML (``index.html.twig``), XML (``index.xml.twig``), -or any other format. For more information, read the :ref:`template-formats` -section. - -.. note:: - - The available "engines" can be configured and even new engines added. - See :ref:`Templating Configuration` for more details. - -.. index:: - single: Templating; Tags and helpers - single: Templating; Helpers - -Tags and Helpers ----------------- - -You already understand the basics of templates, how they're named and how -to use template inheritance. The hardest parts are already behind you. In -this section, you'll learn about a large group of tools available to help -perform the most common template tasks such as including other templates, -linking to pages and including images. - -Symfony2 comes bundled with several specialized Twig tags and functions that -ease the work of the template designer. In PHP, the templating system provides -an extensible *helper* system that provides useful features in a template -context. - -You've already seen a few built-in Twig tags (``{% block %}`` & ``{% extends %}``) -as well as an example of a PHP helper (``$view['slots']``). Here you will learn a -few more. - -.. index:: - single: Templating; Including other templates - -.. _including-templates: - -Including other Templates -~~~~~~~~~~~~~~~~~~~~~~~~~ - -You'll often want to include the same template or code fragment on several -different pages. For example, in an application with "news articles", the -template code displaying an article might be used on the article detail page, -on a page displaying the most popular articles, or in a list of the latest -articles. - -When you need to reuse a chunk of PHP code, you typically move the code to -a new PHP class or function. The same is true for templates. By moving the -reused template code into its own template, it can be included from any other -template. First, create the template that you'll need to reuse. - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/ArticleBundle/Resources/views/Article/articleDetails.html.twig #} -

{{ article.title }}

- - -

- {{ article.body }} -

- - .. code-block:: html+php - - -

getTitle() ?>

- - -

- getBody() ?> -

- -Including this template from any other template is simple: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/ArticleBundle/Resources/views/Article/list.html.twig #} - {% extends 'AcmeArticleBundle::layout.html.twig' %} - - {% block body %} -

Recent Articles

- - {% for article in articles %} - {{ include('AcmeArticleBundle:Article:articleDetails.html.twig', {'article': article}) }} - {% endfor %} - {% endblock %} - - .. code-block:: html+php - - - extend('AcmeArticleBundle::layout.html.php') ?> - - start('body') ?> -

Recent Articles

- - - render( - 'AcmeArticleBundle:Article:articleDetails.html.php', - array('article' => $article) - ) ?> - - stop() ?> - -The template is included using the ``{{ include() }}`` function. Notice that the -template name follows the same typical convention. The ``articleDetails.html.twig`` -template uses an ``article`` variable. This is passed in by the ``list.html.twig`` -template using the ``with`` command. - -.. tip:: - - The ``{'article': article}`` syntax is the standard Twig syntax for hash - maps (i.e. an array with named keys). If you needed to pass in multiple - elements, it would look like this: ``{'foo': foo, 'bar': bar}``. - -.. versionadded:: 2.2 - The ``include()`` function is a new Twig feature that's available in - Symfony 2.2. Prior, the ``{% include %}`` tag was used. - -.. index:: - single: Templating; Embedding action - -.. _templating-embedding-controller: - -Embedding Controllers -~~~~~~~~~~~~~~~~~~~~~ - -In some cases, you need to do more than include a simple template. Suppose -you have a sidebar in your layout that contains the three most recent articles. -Retrieving the three articles may include querying the database or performing -other heavy logic that can't be done from within a template. - -The solution is to simply embed the result of an entire controller from your -template. First, create a controller that renders a certain number of recent -articles:: - - // src/Acme/ArticleBundle/Controller/ArticleController.php - class ArticleController extends Controller - { - public function recentArticlesAction($max = 3) - { - // make a database call or other logic - // to get the "$max" most recent articles - $articles = ...; - - return $this->render( - 'AcmeArticleBundle:Article:recentList.html.twig', - array('articles' => $articles) - ); - } - } - -The ``recentList`` template is perfectly straightforward: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/ArticleBundle/Resources/views/Article/recentList.html.twig #} - {% for article in articles %} - - {{ article.title }} - - {% endfor %} - - .. code-block:: html+php - - - - - getTitle() ?> - - - -.. note:: - - Notice that the article URL is hardcoded in this example - (e.g. ``/article/*slug*``). This is a bad practice. In the next section, - you'll learn how to do this correctly. - -To include the controller, you'll need to refer to it using the standard -string syntax for controllers (i.e. **bundle**:**controller**:**action**): - -.. configuration-block:: - - .. code-block:: html+jinja - - {# app/Resources/views/base.html.twig #} - - {# ... #} - - - .. code-block:: html+php - - - - - - -Whenever you find that you need a variable or a piece of information that -you don't have access to in a template, consider rendering a controller. -Controllers are fast to execute and promote good code organization and reuse. - -Asynchronous Content with hinclude.js -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.1 - hinclude.js support was added in Symfony 2.1 - -Controllers can be embedded asynchronously using the hinclude.js_ javascript library. -As the embedded content comes from another page (or controller for that matter), -Symfony2 uses the standard ``render`` helper to configure ``hinclude`` tags: - -.. configuration-block:: - - .. code-block:: jinja - - {% render url('...') with {}, {'standalone': 'js'} %} - - .. code-block:: php - - render( - new ControllerReference('...'), - array('renderer' => 'hinclude') - ) ?> - - render( - $view['router']->generate('...'), - array('renderer' => 'hinclude') - ) ?> - -.. note:: - - hinclude.js_ needs to be included in your page to work. - -.. note:: - - When using a controller instead of a URL, you must enable the Symfony - ``fragments`` configuration: - - .. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - # ... - fragments: { path: /_fragment } - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - // ... - 'fragments' => array('path' => '/_fragment'), - )); - -Default content (while loading or if javascript is disabled) can be set globally -in your application configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - # ... - templating: - hinclude_default_template: AcmeDemoBundle::hinclude.html.twig - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - // ... - 'templating' => array( - 'hinclude_default_template' => array('AcmeDemoBundle::hinclude.html.twig'), - ), - )); - -.. versionadded:: 2.2 - Default templates per render function was added in Symfony 2.2 - -You can define default templates per ``render`` function (which will override -any global default template that is defined): - -.. configuration-block:: - - .. code-block:: jinja - - {{ render_hinclude(controller('...'), {'default': 'AcmeDemoBundle:Default:content.html.twig'}) }} - - .. code-block:: php - - render( - new ControllerReference('...'), - array( - 'renderer' => 'hinclude', - 'default' => 'AcmeDemoBundle:Default:content.html.twig', - ) - ) ?> - -Or you can also specify a string to display as the default content: - -.. configuration-block:: - - .. code-block:: jinja - - {{ render_hinclude(controller('...'), {'default': 'Loading...'}) }} - - .. code-block:: php - - render( - new ControllerReference('...'), - array( - 'renderer' => 'hinclude', - 'default' => 'Loading...', - ) - ) ?> - -.. index:: - single: Templating; Linking to pages - -.. _book-templating-pages: - -Linking to Pages -~~~~~~~~~~~~~~~~ - -Creating links to other pages in your application is one of the most common -jobs for a template. Instead of hardcoding URLs in templates, use the ``path`` -Twig function (or the ``router`` helper in PHP) to generate URLs based on -the 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. - -First, link to the "_welcome" page, which is accessible via the following routing -configuration: - -.. configuration-block:: - - .. code-block:: yaml - - _welcome: - path: / - defaults: { _controller: AcmeDemoBundle:Welcome:index } - - .. code-block:: xml - - - AcmeDemoBundle:Welcome:index - - - .. code-block:: php - - $collection = new RouteCollection(); - $collection->add('_welcome', new Route('/', array( - '_controller' => 'AcmeDemoBundle:Welcome:index', - ))); - - return $collection; - -To link to the page, just use the ``path`` Twig function and refer to the route: - -.. configuration-block:: - - .. code-block:: html+jinja - - Home - - .. code-block:: html+php - - Home - -As expected, this will generate the URL ``/``. Now for a more complicated -route: - -.. configuration-block:: - - .. code-block:: yaml - - article_show: - path: /article/{slug} - defaults: { _controller: AcmeArticleBundle:Article:show } - - .. code-block:: xml - - - AcmeArticleBundle:Article:show - - - .. code-block:: php - - $collection = new RouteCollection(); - $collection->add('article_show', new Route('/article/{slug}', array( - '_controller' => 'AcmeArticleBundle:Article:show', - ))); - - return $collection; - -In this case, you need to specify both the route name (``article_show``) and -a value for the ``{slug}`` parameter. Using this route, revisit the -``recentList`` template from the previous section and link to the articles -correctly: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/ArticleBundle/Resources/views/Article/recentList.html.twig #} - {% for article in articles %} - - {{ article.title }} - - {% endfor %} - - .. code-block:: html+php - - - - - getTitle() ?> - - - -.. tip:: - - You can also generate an absolute URL by using the ``url`` Twig function: - - .. code-block:: html+jinja - - Home - - The same can be done in PHP templates by passing a third argument to - the ``generate()`` method: - - .. code-block:: html+php - - Home - -.. index:: - single: Templating; Linking to assets - -.. _book-templating-assets: - -Linking to Assets -~~~~~~~~~~~~~~~~~ - -Templates also commonly refer to images, Javascript, stylesheets and other -assets. Of course you could hard-code the path to these assets (e.g. ``/images/logo.png``), -but Symfony2 provides a more dynamic option via the ``asset`` Twig function: - -.. configuration-block:: - - .. code-block:: html+jinja - - Symfony! - - - - .. code-block:: html+php - - Symfony! - - - -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. http://example.com), -then the rendered paths should be ``/images/logo.png``. But if your application -lives in a subdirectory (e.g. http://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. - -Additionally, if you use the ``asset`` function, Symfony can automatically -append a query string to your asset, in order to guarantee that updated static -assets won't be cached when deployed. For example, ``/images/logo.png`` might -look like ``/images/logo.png?v2``. For more information, see the :ref:`ref-framework-assets-version` -configuration option. - -.. index:: - single: Templating; Including stylesheets and Javascripts - single: Stylesheets; Including stylesheets - single: Javascript; Including Javascripts - -Including Stylesheets and Javascripts in Twig ---------------------------------------------- - -No site would be complete without including Javascript files and stylesheets. -In Symfony, the inclusion of these assets is handled elegantly by taking -advantage of Symfony's template inheritance. - -.. tip:: - - This section will teach you the philosophy behind including stylesheet - and Javascript assets in Symfony. Symfony also packages another library, - called Assetic, which follows this philosophy but allows you to do much - more interesting things with those assets. For more information on - using Assetic see :doc:`/cookbook/assetic/asset_management`. - - -Start by adding two blocks to your base template that will hold your assets: -one called ``stylesheets`` inside the ``head`` tag and another called ``javascripts`` -just above the closing ``body`` tag. These blocks will contain all of the -stylesheets and Javascripts that you'll need throughout your site: - -.. code-block:: html+jinja - - {# app/Resources/views/base.html.twig #} - - - {# ... #} - - {% block stylesheets %} - - {% endblock %} - - - {# ... #} - - {% block javascripts %} - - {% endblock %} - - - -That's easy enough! But what if you need to include an extra stylesheet or -Javascript from a child template? For example, suppose you have a contact -page and you need to include a ``contact.css`` stylesheet *just* on that -page. From inside that contact page's template, do the following: - -.. code-block:: html+jinja - - {# src/Acme/DemoBundle/Resources/views/Contact/contact.html.twig #} - {% extends '::base.html.twig' %} - - {% block stylesheets %} - {{ parent() }} - - - {% endblock %} - - {# ... #} - -In the child template, you simply override the ``stylesheets`` block and -put your new stylesheet tag inside of that block. Of course, since you want -to add to the parent block's content (and not actually *replace* it), you -should use the ``parent()`` Twig function to include everything from the ``stylesheets`` -block of the base template. - -You can also include assets located in your bundles' ``Resources/public`` folder. -You will need to run the ``php app/console assets:install target [--symlink]`` -command, which moves (or symlinks) files into the correct location. (target -is by default "web"). - -.. code-block:: html+jinja - - - -The end result is a page that includes both the ``main.css`` and ``contact.css`` -stylesheets. - -Global Template Variables -------------------------- - -During each request, Symfony2 will set a global template variable ``app`` -in both Twig and PHP template engines by default. The ``app`` variable -is a :class:`Symfony\\Bundle\\FrameworkBundle\\Templating\\GlobalVariables` -instance which will give you access to some application specific variables -automatically: - -* ``app.security`` - The security context. -* ``app.user`` - The current user object. -* ``app.request`` - The request object. -* ``app.session`` - The session object. -* ``app.environment`` - The current environment (dev, prod, etc). -* ``app.debug`` - True if in debug mode. False otherwise. - -.. configuration-block:: - - .. code-block:: html+jinja - -

Username: {{ app.user.username }}

- {% if app.debug %} -

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

-

Application Environment: {{ app.environment }}

- {% endif %} - - .. code-block:: html+php - -

Username: getUser()->getUsername() ?>

- getDebug()): ?> -

Request method: getRequest()->getMethod() ?>

-

Application Environment: getEnvironment() ?>

- - -.. tip:: - - You can add your own global template variables. See the cookbook example - on :doc:`Global Variables`. - -.. index:: - single: Templating; The templating service - -Configuring and using the ``templating`` Service ------------------------------------------------- - -The heart of the template system in Symfony2 is the templating ``Engine``. -This special object is responsible for rendering templates and returning -their content. When you render a template in a controller, for example, -you're actually using the templating engine service. For example:: - - return $this->render('AcmeArticleBundle:Article:index.html.twig'); - -is equivalent to:: - - use Symfony\Component\HttpFoundation\Response; - - $engine = $this->container->get('templating'); - $content = $engine->render('AcmeArticleBundle:Article:index.html.twig'); - - return $response = new Response($content); - -.. _template-configuration: - -The templating engine (or "service") is preconfigured to work automatically -inside Symfony2. It can, of course, be configured further in the application -configuration file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - # ... - templating: { engines: ['twig'] } - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - // ... - - 'templating' => array( - 'engines' => array('twig'), - ), - )); - -Several configuration options are available and are covered in the -:doc:`Configuration Appendix`. - -.. note:: - - The ``twig`` engine is mandatory to use the webprofiler (as well as many - third-party bundles). - -.. index:: - single: Template; Overriding templates - -.. _overriding-bundle-templates: - -Overriding Bundle Templates ---------------------------- - -The Symfony2 community prides itself on creating and maintaining high quality -bundles (see `KnpBundles.com`_) for a large number of different features. -Once you use a third-party bundle, you'll likely need to override and customize -one or more of its templates. - -Suppose you've included the imaginary open-source ``AcmeBlogBundle`` in your -project (e.g. in the ``src/Acme/BlogBundle`` directory). And while you're -really happy with everything, you want to override the blog "list" page to -customize the markup specifically for your application. By digging into the -``Blog`` controller of the ``AcmeBlogBundle``, you find the following:: - - public function indexAction() - { - // some logic to retrieve the blogs - $blogs = ...; - - $this->render( - 'AcmeBlogBundle:Blog:index.html.twig', - array('blogs' => $blogs) - ); - } - -When the ``AcmeBlogBundle:Blog:index.html.twig`` is rendered, Symfony2 actually -looks in two different locations for the template: - -#. ``app/Resources/AcmeBlogBundle/views/Blog/index.html.twig`` -#. ``src/Acme/BlogBundle/Resources/views/Blog/index.html.twig`` - -To override the bundle template, just copy the ``index.html.twig`` template -from the bundle to ``app/Resources/AcmeBlogBundle/views/Blog/index.html.twig`` -(the ``app/Resources/AcmeBlogBundle`` directory won't exist, so you'll need -to create it). You're now free to customize the template. - -.. caution:: - - If you add a template in a new location, you *may* need to clear your - cache (``php app/console cache:clear``), even if you are in debug mode. - -This logic also applies to base bundle templates. Suppose also that each -template in ``AcmeBlogBundle`` inherits from a base template called -``AcmeBlogBundle::layout.html.twig``. Just as before, Symfony2 will look in -the following two places for the template: - -#. ``app/Resources/AcmeBlogBundle/views/layout.html.twig`` -#. ``src/Acme/BlogBundle/Resources/views/layout.html.twig`` - -Once again, to override the template, just copy it from the bundle to -``app/Resources/AcmeBlogBundle/views/layout.html.twig``. You're now free to -customize this copy as you see fit. - -If you take a step back, you'll see that Symfony2 always starts by looking in -the ``app/Resources/{BUNDLE_NAME}/views/`` directory for a template. If the -template doesn't exist there, it continues by checking inside the -``Resources/views`` directory of the bundle itself. This means that all bundle -templates can be overridden by placing them in the correct ``app/Resources`` -subdirectory. - -.. note:: - - You can also override templates from within a bundle by using bundle - inheritance. For more information, see :doc:`/cookbook/bundles/inheritance`. - -.. _templating-overriding-core-templates: - -.. index:: - single: Template; Overriding exception templates - -Overriding Core Templates -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Since the Symfony2 framework itself is just a bundle, core templates can be -overridden in the same way. For example, the core ``TwigBundle`` contains -a number of different "exception" and "error" templates that can be overridden -by copying each from the ``Resources/views/Exception`` directory of the -``TwigBundle`` to, you guessed it, the -``app/Resources/TwigBundle/views/Exception`` directory. - -.. index:: - single: Templating; Three-level inheritance pattern - -Three-level Inheritance ------------------------ - -One common way to use inheritance is to use a three-level approach. This -method works perfectly with the three different types of templates that were just -covered: - -* Create a ``app/Resources/views/base.html.twig`` file that contains the main - layout for your application (like in the previous example). Internally, this - template is called ``::base.html.twig``; - -* Create a template for each "section" of your site. For example, an ``AcmeBlogBundle``, - would have a template called ``AcmeBlogBundle::layout.html.twig`` that contains - only blog section-specific elements; - - .. code-block:: html+jinja - - {# src/Acme/BlogBundle/Resources/views/layout.html.twig #} - {% extends '::base.html.twig' %} - - {% block body %} -

Blog Application

- - {% block content %}{% endblock %} - {% endblock %} - -* Create individual templates for each page and make each extend the appropriate - section template. For example, the "index" page would be called something - close to ``AcmeBlogBundle:Blog:index.html.twig`` and list the actual blog posts. - - .. code-block:: html+jinja - - {# src/Acme/BlogBundle/Resources/views/Blog/index.html.twig #} - {% extends 'AcmeBlogBundle::layout.html.twig' %} - - {% block content %} - {% for entry in blog_entries %} -

{{ entry.title }}

-

{{ entry.body }}

- {% endfor %} - {% endblock %} - -Notice that this template extends the section template -(``AcmeBlogBundle::layout.html.twig``) -which in-turn extends the base application layout (``::base.html.twig``). -This is the common three-level inheritance model. - -When building your application, you may choose to follow this method or simply -make each page template extend the base application template directly -(e.g. ``{% extends '::base.html.twig' %}``). The three-template model is -a best-practice method used by vendor bundles so that the base template for -a bundle can be easily overridden to properly extend your application's base -layout. - -.. index:: - single: Templating; Output escaping - -Output Escaping ---------------- - -When generating HTML from a template, there is always a risk that a template -variable may output unintended HTML or dangerous client-side code. The result -is that dynamic content could break the HTML of the resulting page or allow -a malicious user to perform a `Cross Site Scripting`_ (XSS) attack. Consider -this classic example: - -.. configuration-block:: - - .. code-block:: html+jinja - - Hello {{ name }} - - .. code-block:: html+php - - Hello - -Imagine that the user enters the following code as his/her name: - -.. code-block:: text - - - -Without any output escaping, the resulting template will cause a JavaScript -alert box to pop up: - -.. code-block:: html - - Hello - -And while this seems harmless, if a user can get this far, that same user -should also be able to write JavaScript that performs malicious actions -inside the secure area of an unknowing, legitimate user. - -The answer to the problem is output escaping. With output escaping on, the -same template will render harmlessly, and literally print the ``script`` -tag to the screen: - -.. code-block:: html - - Hello <script>alert('helloe')</script> - -The Twig and PHP templating systems approach the problem in different ways. -If you're using Twig, output escaping is on by default and you're protected. -In PHP, output escaping is not automatic, meaning you'll need to manually -escape where necessary. - -Output Escaping in Twig -~~~~~~~~~~~~~~~~~~~~~~~ - -If you're using Twig templates, then output escaping is on by default. This -means that you're protected out-of-the-box from the unintentional consequences -of user-submitted code. By default, the output escaping assumes that content -is being escaped for HTML output. - -In some cases, you'll need to disable output escaping when you're rendering -a variable that is trusted and contains markup that should not be escaped. -Suppose that administrative users are able to write articles that contain -HTML code. By default, Twig will escape the article body. - -To render it normally, add the ``raw`` filter: - -.. code-block:: jinja - - {{ article.body|raw }} - -You can also disable output escaping inside a ``{% block %}`` area or -for an entire template. For more information, see `Output Escaping`_ in -the Twig documentation. - -Output Escaping in PHP -~~~~~~~~~~~~~~~~~~~~~~ - -Output escaping is not automatic when using PHP templates. This means that -unless you explicitly choose to escape a variable, you're not protected. To -use output escaping, use the special ``escape()`` view method: - -.. code-block:: html+php - - Hello escape($name) ?> - -By default, the ``escape()`` method assumes that the variable is being rendered -within an HTML context (and thus the variable is escaped to be safe for HTML). -The second argument lets you change the context. For example, to output something -in a JavaScript string, use the ``js`` context: - -.. code-block:: html+php - - var myMsg = 'Hello escape($name, 'js') ?>'; - -.. index:: - single: Templating; Formats - -.. _template-formats: - -Debugging ---------- - -.. versionadded:: 2.0.9 - This feature is available as of Twig ``1.5.x``, which was first shipped - with Symfony 2.0.9. - -When using PHP, you can use ``var_dump()`` if you need to quickly find the -value of a variable passed. This is useful, for example, inside your controller. -The same can be achieved when using Twig by using the debug extension. This -needs to be enabled in the config: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - services: - acme_hello.twig.extension.debug: - class: Twig_Extension_Debug - tags: - - { name: 'twig.extension' } - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/config.php - use Symfony\Component\DependencyInjection\Definition; - - $definition = new Definition('Twig_Extension_Debug'); - $definition->addTag('twig.extension'); - $container->setDefinition('acme_hello.twig.extension.debug', $definition); - -Template parameters can then be dumped using the ``dump`` function: - -.. code-block:: html+jinja - - {# src/Acme/ArticleBundle/Resources/views/Article/recentList.html.twig #} - {{ dump(articles) }} - - {% for article in articles %} - - {{ article.title }} - - {% endfor %} - - -The variables will only be dumped if Twig's ``debug`` setting (in ``config.yml``) -is ``true``. By default this means that the variables will be dumped in the -``dev`` environment but not the ``prod`` environment. - -Syntax Checking ---------------- - -.. versionadded:: 2.1 - The ``twig:lint`` command was added in Symfony 2.1 - -You can check for syntax errors in Twig templates using the ``twig:lint`` -console command: - -.. code-block:: bash - - # You can check by filename: - $ php app/console twig:lint src/Acme/ArticleBundle/Resources/views/Article/recentList.html.twig - - # or by directory: - $ php app/console twig:lint src/Acme/ArticleBundle/Resources/views - - # or using the bundle name: - $ php app/console twig:lint @AcmeArticleBundle - -Template Formats ----------------- - -Templates are a generic way to render content in *any* format. And while in -most cases you'll use templates to render HTML content, a template can just -as easily generate JavaScript, CSS, XML or any other format you can dream of. - -For example, the same "resource" is often rendered in several different formats. -To render an article index page in XML, simply include the format in the -template name: - -* *XML template name*: ``AcmeArticleBundle:Article:index.xml.twig`` -* *XML template filename*: ``index.xml.twig`` - -In reality, this is nothing more than a naming convention and the template -isn't actually rendered differently based on its format. - -In many cases, you may want to allow a single controller to render multiple -different formats based on the "request format". For that reason, a common -pattern is to do the following:: - - public function indexAction() - { - $format = $this->getRequest()->getRequestFormat(); - - return $this->render('AcmeBlogBundle:Blog:index.'.$format.'.twig'); - } - -The ``getRequestFormat`` on the ``Request`` object defaults to ``html``, -but can return any other format based on the format requested by the user. -The request format is most often managed by the routing, where a route can -be configured so that ``/contact`` sets the request format to ``html`` while -``/contact.xml`` sets the format to ``xml``. For more information, see the -:ref:`Advanced Example in the Routing chapter `. - -To create links that include the format parameter, include a ``_format`` -key in the parameter hash: - -.. configuration-block:: - - .. code-block:: html+jinja - - - PDF Version - - - .. code-block:: html+php - - - PDF Version - - -Final Thoughts --------------- - -The templating engine in Symfony is a powerful tool that can be used each time -you need to generate presentational content in HTML, XML or any other format. -And though templates are a common way to generate content in a controller, -their use is not mandatory. The ``Response`` object returned by a controller -can be created with or without the use of a template:: - - // creates a Response object whose content is the rendered template - $response = $this->render('AcmeArticleBundle:Article:index.html.twig'); - - // creates a Response object whose content is simple text - $response = new Response('response content'); - -Symfony's templating engine is very flexible and two different template -renderers are available by default: the traditional *PHP* templates and the -sleek and powerful *Twig* templates. Both support a template hierarchy and -come packaged with a rich set of helper functions capable of performing -the most common tasks. - -Overall, the topic of templating should be thought of as a powerful tool -that's at your disposal. In some cases, you may not need to render a template, -and in Symfony2, that's absolutely fine. - -Learn more from the Cookbook ----------------------------- - -* :doc:`/cookbook/templating/PHP` -* :doc:`/cookbook/controller/error_pages` -* :doc:`/cookbook/templating/twig_extension` - -.. _`Twig`: http://twig.sensiolabs.org -.. _`KnpBundles.com`: http://knpbundles.com -.. _`Cross Site Scripting`: http://en.wikipedia.org/wiki/Cross-site_scripting -.. _`Output Escaping`: http://twig.sensiolabs.org/doc/api.html#escaper-extension -.. _`tags`: http://twig.sensiolabs.org/doc/tags/index.html -.. _`filters`: http://twig.sensiolabs.org/doc/filters/index.html -.. _`add your own extensions`: http://twig.sensiolabs.org/doc/advanced.html#creating-an-extension -.. _`hinclude.js`: http://mnot.github.com/hinclude/ diff --git a/book/testing.rst b/book/testing.rst deleted file mode 100644 index d042b7cefb2..00000000000 --- a/book/testing.rst +++ /dev/null @@ -1,819 +0,0 @@ -.. index:: - single: Tests - -Testing -======= - -Whenever you write a new line of code, you also potentially add new bugs. -To build better and more reliable applications, you should test your code -using both functional and unit tests. - -The PHPUnit Testing Framework ------------------------------ - -Symfony2 integrates with an independent library - called PHPUnit - to give -you a rich testing framework. This chapter won't cover PHPUnit itself, but -it has its own excellent `documentation`_. - -.. note:: - - Symfony2 works with PHPUnit 3.5.11 or later, though version 3.6.4 is - needed to test the Symfony core code itself. - -Each test - whether it's a unit test or a functional test - is a PHP class -that should live in the `Tests/` subdirectory of your bundles. If you follow -this rule, then you can run all of your application's tests with the following -command: - -.. code-block:: bash - - # specify the configuration directory on the command line - $ phpunit -c app/ - -The ``-c`` option tells PHPUnit to look in the ``app/`` directory for a configuration -file. If you're curious about the PHPUnit options, check out the ``app/phpunit.xml.dist`` -file. - -.. tip:: - - Code coverage can be generated with the ``--coverage-html`` option. - -.. index:: - single: Tests; Unit tests - -Unit Tests ----------- - -A unit test is usually a test against a specific PHP class. If you want to -test the overall behavior of your application, see the section about `Functional Tests`_. - -Writing Symfony2 unit tests is no different than writing standard PHPUnit -unit tests. Suppose, for example, that you have an *incredibly* simple class -called ``Calculator`` in the ``Utility/`` directory of your bundle:: - - // src/Acme/DemoBundle/Utility/Calculator.php - namespace Acme\DemoBundle\Utility; - - class Calculator - { - public function add($a, $b) - { - return $a + $b; - } - } - -To test this, create a ``CalculatorTest`` file in the ``Tests/Utility`` directory -of your bundle:: - - // src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php - namespace Acme\DemoBundle\Tests\Utility; - - use Acme\DemoBundle\Utility\Calculator; - - class CalculatorTest extends \PHPUnit_Framework_TestCase - { - public function testAdd() - { - $calc = new Calculator(); - $result = $calc->add(30, 12); - - // assert that your calculator added the numbers correctly! - $this->assertEquals(42, $result); - } - } - -.. note:: - - By convention, the ``Tests/`` sub-directory should replicate the directory - of your bundle. So, if you're testing a class in your bundle's ``Utility/`` - directory, put the test in the ``Tests/Utility/`` directory. - -Just like in your real application - autoloading is automatically enabled -via the ``bootstrap.php.cache`` file (as configured by default in the ``phpunit.xml.dist`` -file). - -Running tests for a given file or directory is also very easy: - -.. code-block:: bash - - # run all tests in the Utility directory - $ phpunit -c app src/Acme/DemoBundle/Tests/Utility/ - - # run tests for the Calculator class - $ phpunit -c app src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php - - # run all tests for the entire Bundle - $ phpunit -c app src/Acme/DemoBundle/ - -.. index:: - single: Tests; Functional tests - -Functional Tests ----------------- - -Functional tests check the integration of the different layers of an -application (from the routing to the views). They are no different from unit -tests as far as PHPUnit is concerned, but they have a very specific workflow: - -* Make a request; -* Test the response; -* Click on a link or submit a form; -* Test the response; -* Rinse and repeat. - -Your First Functional Test -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Functional tests are simple PHP files that typically live in the ``Tests/Controller`` -directory of your bundle. If you want to test the pages handled by your -``DemoController`` class, start by creating a new ``DemoControllerTest.php`` -file that extends a special ``WebTestCase`` class. - -For example, the Symfony2 Standard Edition provides a simple functional test -for its ``DemoController`` (`DemoControllerTest`_) that reads as follows:: - - // src/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php - namespace Acme\DemoBundle\Tests\Controller; - - use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - - class DemoControllerTest extends WebTestCase - { - public function testIndex() - { - $client = static::createClient(); - - $crawler = $client->request('GET', '/demo/hello/Fabien'); - - $this->assertGreaterThan( - 0, - $crawler->filter('html:contains("Hello Fabien")')->count() - ); - } - } - -.. tip:: - - To run your functional tests, the ``WebTestCase`` class bootstraps the - kernel of your application. In most cases, this happens automatically. - However, if your kernel is in a non-standard directory, you'll need - to modify your ``phpunit.xml.dist`` file to set the ``KERNEL_DIR`` environment - variable to the directory of your kernel: - - .. code-block:: xml - - - - - - - - - -The ``createClient()`` method returns a client, which is like a browser that -you'll use to crawl your site:: - - $crawler = $client->request('GET', '/demo/hello/Fabien'); - -The ``request()`` method (see :ref:`more about the request method`) -returns a :class:`Symfony\\Component\\DomCrawler\\Crawler` object which can -be used to select elements in the Response, click on links, and submit forms. - -.. tip:: - - The Crawler only works when the response is an XML or an HTML document. - To get the raw content response, call ``$client->getResponse()->getContent()``. - -Click on a link by first selecting it with the Crawler using either an XPath -expression or a CSS selector, then use the Client to click on it. For example, -the following code finds all links with the text ``Greet``, then selects -the second one, and ultimately clicks on it:: - - $link = $crawler->filter('a:contains("Greet")')->eq(1)->link(); - - $crawler = $client->click($link); - -Submitting a form is very similar; select a form button, optionally override -some form values, and submit the corresponding form:: - - $form = $crawler->selectButton('submit')->form(); - - // set some values - $form['name'] = 'Lucas'; - $form['form_name[subject]'] = 'Hey there!'; - - // submit the form - $crawler = $client->submit($form); - -.. tip:: - - The form can also handle uploads and contains methods to fill in different types - of form fields (e.g. ``select()`` and ``tick()``). For details, see the - `Forms`_ section below. - -Now that you can easily navigate through an application, use assertions to test -that it actually does what you expect it to. Use the Crawler to make assertions -on the DOM:: - - // Assert that the response matches a given CSS selector. - $this->assertGreaterThan(0, $crawler->filter('h1')->count()); - -Or, test against the Response content directly if you just want to assert that -the content contains some text, or if the Response is not an XML/HTML -document:: - - $this->assertRegExp( - '/Hello Fabien/', - $client->getResponse()->getContent() - ); - -.. _book-testing-request-method-sidebar: - -.. sidebar:: More about the ``request()`` method: - - The full signature of the ``request()`` method is:: - - request( - $method, - $uri, - array $parameters = array(), - array $files = array(), - array $server = array(), - $content = null, - $changeHistory = true - ) - - The ``server`` array is the raw values that you'd expect to normally - find in the PHP `$_SERVER`_ superglobal. For example, to set the `Content-Type`, - `Referer` and `X-Requested-With' HTTP headers, you'd pass the following (mind - the `HTTP_` prefix for non standard headers):: - - $client->request( - 'GET', - '/demo/hello/Fabien', - array(), - array(), - array( - 'CONTENT_TYPE' => 'application/json', - 'HTTP_REFERER' => '/foo/bar', - 'HTTP_X-Requested-With' => 'XMLHttpRequest', - ) - ); - -.. index:: - single: Tests; Assertions - -.. sidebar:: Useful Assertions - - To get you started faster, here is a list of the most common and - useful test assertions:: - - // Assert that there is at least one h2 tag - // with the class "subtitle" - $this->assertGreaterThan( - 0, - $crawler->filter('h2.subtitle')->count() - ); - - // Assert that there are exactly 4 h2 tags on the page - $this->assertCount(4, $crawler->filter('h2')); - - // Assert that the "Content-Type" header is "application/json" - $this->assertTrue( - $client->getResponse()->headers->contains( - 'Content-Type', - 'application/json' - ) - ); - - // Assert that the response content matches a regexp. - $this->assertRegExp('/foo/', $client->getResponse()->getContent()); - - // Assert that the response status code is 2xx - $this->assertTrue($client->getResponse()->isSuccessful()); - // Assert that the response status code is 404 - $this->assertTrue($client->getResponse()->isNotFound()); - // Assert a specific 200 status code - $this->assertEquals( - 200, - $client->getResponse()->getStatusCode() - ); - - // Assert that the response is a redirect to /demo/contact - $this->assertTrue( - $client->getResponse()->isRedirect('/demo/contact') - ); - // or simply check that the response is a redirect to any URL - $this->assertTrue($client->getResponse()->isRedirect()); - -.. index:: - single: Tests; Client - -Working with the Test Client ------------------------------ - -The Test Client simulates an HTTP client like a browser and makes requests -into your Symfony2 application:: - - $crawler = $client->request('GET', '/hello/Fabien'); - -The ``request()`` method takes the HTTP method and a URL as arguments and -returns a ``Crawler`` instance. - -Use the Crawler to find DOM elements in the Response. These elements can then -be used to click on links and submit forms:: - - $link = $crawler->selectLink('Go elsewhere...')->link(); - $crawler = $client->click($link); - - $form = $crawler->selectButton('validate')->form(); - $crawler = $client->submit($form, array('name' => 'Fabien')); - -The ``click()`` and ``submit()`` methods both return a ``Crawler`` object. -These methods are the best way to browse your application as it takes care -of a lot of things for you, like detecting the HTTP method from a form and -giving you a nice API for uploading files. - -.. tip:: - - You will learn more about the ``Link`` and ``Form`` objects in the - :ref:`Crawler` section below. - -The ``request`` method can also be used to simulate form submissions directly -or perform more complex requests:: - - // Directly submit a form (but using the Crawler is easier!) - $client->request('POST', '/submit', array('name' => 'Fabien')); - - // Submit a raw JSON string in the request body - $client->request( - 'POST', - '/submit', - array(), - array(), - array('CONTENT_TYPE' => 'application/json'), - '{"name":"Fabien"}' - ); - - // Form submission with a file upload - use Symfony\Component\HttpFoundation\File\UploadedFile; - - $photo = new UploadedFile( - '/path/to/photo.jpg', - 'photo.jpg', - 'image/jpeg', - 123 - ); - $client->request( - 'POST', - '/submit', - array('name' => 'Fabien'), - array('photo' => $photo) - ); - - // Perform a DELETE requests, and pass HTTP headers - $client->request( - 'DELETE', - '/post/12', - array(), - array(), - array('PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word') - ); - -Last but not least, you can force each request to be executed in its own PHP -process to avoid any side-effects when working with several clients in the same -script:: - - $client->insulate(); - -Browsing -~~~~~~~~ - -The Client supports many operations that can be done in a real browser:: - - $client->back(); - $client->forward(); - $client->reload(); - - // Clears all cookies and the history - $client->restart(); - -Accessing Internal Objects -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.3 - The ``getInternalRequest()`` and ``getInternalResponse()`` method were - added in Symfony 2.3. - -If you use the client to test your application, you might want to access the -client's internal objects:: - - $history = $client->getHistory(); - $cookieJar = $client->getCookieJar(); - -You can also get the objects related to the latest request:: - - // the HttpKernel request instance - $request = $client->getRequest(); - - // the BrowserKit request instance - $request = $client->getInternalRequest(); - - // the HttpKernel response instance - $response = $client->getResponse(); - - // the BrowserKit response instance - $response = $client->getInternalResponse(); - - $crawler = $client->getCrawler(); - -If your requests are not insulated, you can also access the ``Container`` and -the ``Kernel``:: - - $container = $client->getContainer(); - $kernel = $client->getKernel(); - -Accessing the Container -~~~~~~~~~~~~~~~~~~~~~~~ - -It's highly recommended that a functional test only tests the Response. But -under certain very rare circumstances, you might want to access some internal -objects to write assertions. In such cases, you can access the dependency -injection container:: - - $container = $client->getContainer(); - -Be warned that this does not work if you insulate the client or if you use an -HTTP layer. For a list of services available in your application, use the -``container:debug`` console task. - -.. tip:: - - If the information you need to check is available from the profiler, use - it instead. - -Accessing the Profiler Data -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -On each request, you can enable the Symfony profiler to collect data about the -internal handling of that request. For example, the profiler could be used to -verify that a given page executes less than a certain number of database -queries when loading. - -To get the Profiler for the last request, do the following:: - - // enable the profiler for the very next request - $client->enableProfiler(); - - $crawler = $client->request('GET', '/profiler'); - - // get the profile - $profile = $client->getProfile(); - -For specific details on using the profiler inside a test, see the -:doc:`/cookbook/testing/profiling` cookbook entry. - -Redirecting -~~~~~~~~~~~ - -When a request returns a redirect response, the client does not follow -it automatically. You can examine the response and force a redirection -afterwards with the ``followRedirect()`` method:: - - $crawler = $client->followRedirect(); - -If you want the client to automatically follow all redirects, you can -force him with the ``followRedirects()`` method:: - - $client->followRedirects(); - -.. index:: - single: Tests; Crawler - -.. _book-testing-crawler: - -The Crawler ------------ - -A Crawler instance is returned each time you make a request with the Client. -It allows you to traverse HTML documents, select nodes, find links and forms. - -Traversing -~~~~~~~~~~ - -Like jQuery, the Crawler has methods to traverse the DOM of an HTML/XML -document. For example, the following finds all ``input[type=submit]`` elements, -selects the last one on the page, and then selects its immediate parent element:: - - $newCrawler = $crawler->filter('input[type=submit]') - ->last() - ->parents() - ->first() - ; - -Many other methods are also available: - -+------------------------+----------------------------------------------------+ -| Method | Description | -+========================+====================================================+ -| ``filter('h1.title')`` | Nodes that match the CSS selector | -+------------------------+----------------------------------------------------+ -| ``filterXpath('h1')`` | Nodes that match the XPath expression | -+------------------------+----------------------------------------------------+ -| ``eq(1)`` | Node for the specified index | -+------------------------+----------------------------------------------------+ -| ``first()`` | First node | -+------------------------+----------------------------------------------------+ -| ``last()`` | Last node | -+------------------------+----------------------------------------------------+ -| ``siblings()`` | Siblings | -+------------------------+----------------------------------------------------+ -| ``nextAll()`` | All following siblings | -+------------------------+----------------------------------------------------+ -| ``previousAll()`` | All preceding siblings | -+------------------------+----------------------------------------------------+ -| ``parents()`` | Returns the parent nodes | -+------------------------+----------------------------------------------------+ -| ``children()`` | Returns children nodes | -+------------------------+----------------------------------------------------+ -| ``reduce($lambda)`` | Nodes for which the callable does not return false | -+------------------------+----------------------------------------------------+ - -Since each of these methods returns a new ``Crawler`` instance, you can -narrow down your node selection by chaining the method calls:: - - $crawler - ->filter('h1') - ->reduce(function ($node, $i) - { - if (!$node->getAttribute('class')) { - return false; - } - }) - ->first(); - -.. tip:: - - Use the ``count()`` function to get the number of nodes stored in a Crawler: - ``count($crawler)`` - -Extracting Information -~~~~~~~~~~~~~~~~~~~~~~ - -The Crawler can extract information from the nodes:: - - // Returns the attribute value for the first node - $crawler->attr('class'); - - // Returns the node value for the first node - $crawler->text(); - - // Extracts an array of attributes for all nodes - // (_text returns the node value) - // returns an array for each element in crawler, - // each with the value and href - $info = $crawler->extract(array('_text', 'href')); - - // Executes a lambda for each node and return an array of results - $data = $crawler->each(function ($node, $i) - { - return $node->attr('href'); - }); - -Links -~~~~~ - -To select links, you can use the traversing methods above or the convenient -``selectLink()`` shortcut:: - - $crawler->selectLink('Click here'); - -This selects all links that contain the given text, or clickable images for -which the ``alt`` attribute contains the given text. Like the other filtering -methods, this returns another ``Crawler`` object. - -Once you've selected a link, you have access to a special ``Link`` object, -which has helpful methods specific to links (such as ``getMethod()`` and -``getUri()``). To click on the link, use the Client's ``click()`` method -and pass it a ``Link`` object:: - - $link = $crawler->selectLink('Click here')->link(); - - $client->click($link); - -Forms -~~~~~ - -Just like links, you select forms with the ``selectButton()`` method:: - - $buttonCrawlerNode = $crawler->selectButton('submit'); - -.. note:: - - Notice that you select form buttons and not forms as a form can have several - buttons; if you use the traversing API, keep in mind that you must look for a - button. - -The ``selectButton()`` method can select ``button`` tags and submit ``input`` -tags. It uses several different parts of the buttons to find them: - -* The ``value`` attribute value; - -* The ``id`` or ``alt`` attribute value for images; - -* The ``id`` or ``name`` attribute value for ``button`` tags. - -Once you have a Crawler representing a button, call the ``form()`` method -to get a ``Form`` instance for the form wrapping the button node:: - - $form = $buttonCrawlerNode->form(); - -When calling the ``form()`` method, you can also pass an array of field values -that overrides the default ones:: - - $form = $buttonCrawlerNode->form(array( - 'name' => 'Fabien', - 'my_form[subject]' => 'Symfony rocks!', - )); - -And if you want to simulate a specific HTTP method for the form, pass it as a -second argument:: - - $form = $buttonCrawlerNode->form(array(), 'DELETE'); - -The Client can submit ``Form`` instances:: - - $client->submit($form); - -The field values can also be passed as a second argument of the ``submit()`` -method:: - - $client->submit($form, array( - 'name' => 'Fabien', - 'my_form[subject]' => 'Symfony rocks!', - )); - -For more complex situations, use the ``Form`` instance as an array to set the -value of each field individually:: - - // Change the value of a field - $form['name'] = 'Fabien'; - $form['my_form[subject]'] = 'Symfony rocks!'; - -There is also a nice API to manipulate the values of the fields according to -their type:: - - // Select an option or a radio - $form['country']->select('France'); - - // Tick a checkbox - $form['like_symfony']->tick(); - - // Upload a file - $form['photo']->upload('/path/to/lucas.jpg'); - -.. tip:: - - You can get the values that will be submitted by calling the ``getValues()`` - method on the ``Form`` object. The uploaded files are available in a - separate array returned by ``getFiles()``. The ``getPhpValues()`` and - ``getPhpFiles()`` methods also return the submitted values, but in the - PHP format (it converts the keys with square brackets notation - e.g. - ``my_form[subject]`` - to PHP arrays). - -.. index:: - pair: Tests; Configuration - -Testing Configuration ---------------------- - -The Client used by functional tests creates a Kernel that runs in a special -``test`` environment. Since Symfony loads the ``app/config/config_test.yml`` -in the ``test`` environment, you can tweak any of your application's settings -specifically for testing. - -For example, by default, the swiftmailer is configured to *not* actually -deliver emails in the ``test`` environment. You can see this under the ``swiftmailer`` -configuration option: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_test.yml - - # ... - swiftmailer: - disable_delivery: true - - .. code-block:: xml - - - - - - - - .. code-block:: php - - // app/config/config_test.php - - // ... - $container->loadFromExtension('swiftmailer', array( - 'disable_delivery' => true, - )); - -You can also use a different environment entirely, or override the default -debug mode (``true``) by passing each as options to the ``createClient()`` -method:: - - $client = static::createClient(array( - 'environment' => 'my_test_env', - 'debug' => false, - )); - -If your application behaves according to some HTTP headers, pass them as the -second argument of ``createClient()``:: - - $client = static::createClient(array(), array( - 'HTTP_HOST' => 'en.example.com', - 'HTTP_USER_AGENT' => 'MySuperBrowser/1.0', - )); - -You can also override HTTP headers on a per request basis:: - - $client->request('GET', '/', array(), array(), array( - 'HTTP_HOST' => 'en.example.com', - 'HTTP_USER_AGENT' => 'MySuperBrowser/1.0', - )); - -.. tip:: - - The test client is available as a service in the container in the ``test`` - environment (or wherever the :ref:`framework.test` - option is enabled). This means you can override the service entirely - if you need to. - -.. index:: - pair: PHPUnit; Configuration - -PHPUnit Configuration -~~~~~~~~~~~~~~~~~~~~~ - -Each application has its own PHPUnit configuration, stored in the -``phpunit.xml.dist`` file. You can edit this file to change the defaults or -create a ``phpunit.xml`` file to tweak the configuration for your local machine. - -.. tip:: - - Store the ``phpunit.xml.dist`` file in your code repository, and ignore the - ``phpunit.xml`` file. - -By default, only the tests stored in "standard" bundles are run by the -``phpunit`` command (standard being tests in the ``src/*/Bundle/Tests`` or -``src/*/Bundle/*Bundle/Tests`` directories) But you can easily add more -directories. For instance, the following configuration adds the tests from -the installed third-party bundles: - -.. code-block:: xml - - - - - ../src/*/*Bundle/Tests - ../src/Acme/Bundle/*Bundle/Tests - - - -To include other directories in the code coverage, also edit the ```` -section: - -.. code-block:: xml - - - - - ../src - - ../src/*/*Bundle/Resources - ../src/*/*Bundle/Tests - ../src/Acme/Bundle/*Bundle/Resources - ../src/Acme/Bundle/*Bundle/Tests - - - - -Learn more ----------- - -* :doc:`/components/dom_crawler` -* :doc:`/components/css_selector` -* :doc:`/cookbook/testing/http_authentication` -* :doc:`/cookbook/testing/insulating_clients` -* :doc:`/cookbook/testing/profiling` -* :doc:`/cookbook/testing/bootstrap` - - -.. _`DemoControllerTest`: https://github.com/symfony/symfony-standard/blob/master/src/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php -.. _`$_SERVER`: http://php.net/manual/en/reserved.variables.server.php -.. _`documentation`: http://www.phpunit.de/manual/3.5/en/ diff --git a/book/translation.rst b/book/translation.rst deleted file mode 100644 index a44347d826c..00000000000 --- a/book/translation.rst +++ /dev/null @@ -1,1012 +0,0 @@ -.. index:: - single: Translations - -Translations -============ - -The term "internationalization" (often abbreviated `i18n`_) refers to the process -of abstracting strings and other locale-specific pieces out of your application -and into a layer where they can be translated and converted based on the user's -locale (i.e. language and country). For text, this means wrapping each with a -function capable of translating the text (or "message") into the language of -the user:: - - // text will *always* print out in English - echo 'Hello World'; - - // text can be translated into the end-user's language or - // default to English - echo $translator->trans('Hello World'); - -.. note:: - - The term *locale* refers roughly to the user's language and country. It - can be any string that your application uses to manage translations - and other format differences (e.g. currency format). The - `ISO639-1`_ *language* code, an underscore (``_``), then the `ISO3166 Alpha-2`_ *country* - code (e.g. ``fr_FR`` for French/France) is recommended. - -In this chapter, you'll learn how to prepare an application to support multiple -locales and then how to create translations for multiple locales. Overall, -the process has several common steps: - -#. Enable and configure Symfony's ``Translation`` component; - -#. Abstract strings (i.e. "messages") by wrapping them in calls to the ``Translator``; - -#. Create translation resources for each supported locale that translate - each message in the application; - -#. Determine, set and manage the user's locale for the request and optionally - on the user's entire session. - -.. index:: - single: Translations; Configuration - -Configuration -------------- - -Translations are handled by a ``Translator`` :term:`service` that uses the -user's locale to lookup and return translated messages. Before using it, -enable the ``Translator`` in your configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - translator: { fallback: en } - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - 'translator' => array('fallback' => 'en'), - )); - -The ``fallback`` option defines the fallback locale when a translation does -not exist in the user's locale. - -.. tip:: - - When a translation does not exist for a locale, the translator first tries - to find the translation for the language (``fr`` if the locale is - ``fr_FR`` for instance). If this also fails, it looks for a translation - using the fallback locale. - -The locale used in translations is the one stored on the request. This is -typically set via a ``_locale`` attribute on your routes (see :ref:`book-translation-locale-url`). - -.. index:: - single: Translations; Basic translation - -Basic Translation ------------------ - -Translation of text is done through the ``translator`` service -(:class:`Symfony\\Component\\Translation\\Translator`). To translate a block -of text (called a *message*), use the -:method:`Symfony\\Component\\Translation\\Translator::trans` method. Suppose, -for example, that you're translating a simple message from inside a controller:: - - // ... - use Symfony\Component\HttpFoundation\Response; - - public function indexAction() - { - $t = $this->get('translator')->trans('Symfony2 is great'); - - return new Response($t); - } - -When this code is executed, Symfony2 will attempt to translate the message -"Symfony2 is great" based on the ``locale`` of the user. For this to work, -you need to tell Symfony2 how to translate the message via a "translation -resource", which is a collection of message translations for a given locale. -This "dictionary" of translations can be created in several different formats, -XLIFF being the recommended format: - -.. configuration-block:: - - .. code-block:: xml - - - - - - - - Symfony2 is great - J'aime Symfony2 - - - - - - .. code-block:: php - - // messages.fr.php - return array( - 'Symfony2 is great' => 'J\'aime Symfony2', - ); - - .. code-block:: yaml - - # messages.fr.yml - Symfony2 is great: J'aime Symfony2 - -Now, if the language of the user's locale is French (e.g. ``fr_FR`` or ``fr_BE``), -the message will be translated into ``J'aime Symfony2``. - -The Translation Process -~~~~~~~~~~~~~~~~~~~~~~~ - -To actually translate the message, Symfony2 uses a simple process: - -* The ``locale`` of the current user, which is stored on the request (or - stored as ``_locale`` on the session), is determined; - -* A catalog of translated messages is loaded from translation resources defined - for the ``locale`` (e.g. ``fr_FR``). Messages from the fallback locale are - also loaded and added to the catalog if they don't already exist. The end - result is a large "dictionary" of translations. See `Message Catalogues`_ - for more details; - -* If the message is located in the catalog, the translation is returned. If - not, the translator returns the original message. - -When using the ``trans()`` method, Symfony2 looks for the exact string inside -the appropriate message catalog and returns it (if it exists). - -.. index:: - single: Translations; Message placeholders - -Message Placeholders -~~~~~~~~~~~~~~~~~~~~ - -Sometimes, a message containing a variable needs to be translated:: - - // ... - use Symfony\Component\HttpFoundation\Response; - - public function indexAction($name) - { - $t = $this->get('translator')->trans('Hello '.$name); - - return new Response($t); - } - -However, creating a translation for this string is impossible since the translator -will try to look up the exact message, including the variable portions -(e.g. "Hello Ryan" or "Hello Fabien"). Instead of writing a translation -for every possible iteration of the ``$name`` variable, you can replace the -variable with a "placeholder":: - - // ... - use Symfony\Component\HttpFoundation\Response; - - public function indexAction($name) - { - $t = $this->get('translator')->trans( - 'Hello %name%', - array('%name%' => $name) - ); - - return new Response($t); - } - -Symfony2 will now look for a translation of the raw message (``Hello %name%``) -and *then* replace the placeholders with their values. Creating a translation -is done just as before: - -.. configuration-block:: - - .. code-block:: xml - - - - - - - - Hello %name% - Bonjour %name% - - - - - - .. code-block:: php - - // messages.fr.php - return array( - 'Hello %name%' => 'Bonjour %name%', - ); - - .. code-block:: yaml - - # messages.fr.yml - 'Hello %name%': Bonjour %name% - -.. note:: - - The placeholders can take on any form as the full message is reconstructed - using the PHP `strtr function`_. However, the ``%var%`` notation is - required when translating in Twig templates, and is overall a sensible - convention to follow. - -As you've seen, creating a translation is a two-step process: - -#. Abstract the message that needs to be translated by processing it through - the ``Translator``. - -#. Create a translation for the message in each locale that you choose to - support. - -The second step is done by creating message catalogues that define the translations -for any number of different locales. - -.. index:: - single: Translations; Message catalogues - -Message Catalogues ------------------- - -When a message is translated, Symfony2 compiles a message catalogue for the -user's locale and looks in it for a translation of the message. A message -catalogue is like a dictionary of translations for a specific locale. For -example, the catalogue for the ``fr_FR`` locale might contain the following -translation: - -.. code-block:: text - - Symfony2 is Great => J'aime Symfony2 - -It's the responsibility of the developer (or translator) of an internationalized -application to create these translations. Translations are stored on the -filesystem and discovered by Symfony, thanks to some conventions. - -.. tip:: - - Each time you create a *new* translation resource (or install a bundle - that includes a translation resource), be sure to clear your cache so - that Symfony can discover the new translation resource: - - .. code-block:: bash - - $ php app/console cache:clear - -.. index:: - single: Translations; Translation resource locations - -Translation Locations and Naming Conventions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Symfony2 looks for message files (i.e. translations) in the following locations: - -* the ``/Resources/translations`` directory; - -* the ``/Resources//translations`` directory; - -* the ``Resources/translations/`` directory of the bundle. - -The locations are listed with the highest priority first. That is you can -override the translation messages of a bundle in any of the top 2 directories. - -The override mechanism works at a key level: only the overridden keys need -to be listed in a higher priority message file. When a key is not found -in a message file, the translator will automatically fall back to the lower -priority message files. - -The filename of the translations is also important as Symfony2 uses a convention -to determine details about the translations. Each message file must be named -according to the following path: ``domain.locale.loader``: - -* **domain**: An optional way to organize messages into groups (e.g. ``admin``, - ``navigation`` or the default ``messages``) - see `Using Message Domains`_; - -* **locale**: The locale that the translations are for (e.g. ``en_GB``, ``en``, etc); - -* **loader**: How Symfony2 should load and parse the file (e.g. ``xliff``, - ``php`` or ``yml``). - -The loader can be the name of any registered loader. By default, Symfony -provides the following loaders: - -* ``xliff``: XLIFF file; -* ``php``: PHP file; -* ``yml``: YAML file. - -The choice of which loader to use is entirely up to you and is a matter of -taste. - -.. note:: - - You can also store translations in a database, or any other storage by - providing a custom class implementing the - :class:`Symfony\\Component\\Translation\\Loader\\LoaderInterface` interface. - -.. index:: - single: Translations; Creating translation resources - -Creating Translations -~~~~~~~~~~~~~~~~~~~~~ - -The act of creating translation files is an important part of "localization" -(often abbreviated `L10n`_). Translation files consist of a series of -id-translation pairs for the given domain and locale. The source is the identifier -for the individual translation, and can be the message in the main locale (e.g. -"Symfony is great") of your application or a unique identifier (e.g. -"symfony2.great" - see the sidebar below): - -.. configuration-block:: - - .. code-block:: xml - - - - - - - - Symfony2 is great - J'aime Symfony2 - - - symfony2.great - J'aime Symfony2 - - - - - - .. code-block:: php - - // src/Acme/DemoBundle/Resources/translations/messages.fr.php - return array( - 'Symfony2 is great' => 'J\'aime Symfony2', - 'symfony2.great' => 'J\'aime Symfony2', - ); - - .. code-block:: yaml - - # src/Acme/DemoBundle/Resources/translations/messages.fr.yml - Symfony2 is great: J'aime Symfony2 - symfony2.great: J'aime Symfony2 - -Symfony2 will discover these files and use them when translating either -"Symfony2 is great" or "symfony2.great" into a French language locale (e.g. -``fr_FR`` or ``fr_BE``). - -.. sidebar:: Using Real or Keyword Messages - - This example illustrates the two different philosophies when creating - messages to be translated:: - - $t = $translator->trans('Symfony2 is great'); - - $t = $translator->trans('symfony2.great'); - - In the first method, messages are written in the language of the default - locale (English in this case). That message is then used as the "id" - when creating translations. - - In the second method, messages are actually "keywords" that convey the - idea of the message. The keyword message is then used as the "id" for - any translations. In this case, translations must be made for the default - locale (i.e. to translate ``symfony2.great`` to ``Symfony2 is great``). - - The second method is handy because the message key won't need to be changed - in every translation file if you decide that the message should actually - read "Symfony2 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. - - Additionally, the ``php`` and ``yaml`` file formats support nested ids to - avoid repeating yourself if you use keywords instead of real text for your - ids: - - .. configuration-block:: - - .. code-block:: yaml - - symfony2: - is: - great: Symfony2 is great - amazing: Symfony2 is amazing - has: - bundles: Symfony2 has bundles - user: - login: Login - - .. code-block:: php - - return array( - 'symfony2' => array( - 'is' => array( - 'great' => 'Symfony2 is great', - 'amazing' => 'Symfony2 is amazing', - ), - 'has' => array( - 'bundles' => 'Symfony2 has bundles', - ), - ), - 'user' => array( - 'login' => 'Login', - ), - ); - - The multiple levels are flattened into single id/translation pairs by - adding a dot (.) between every level, therefore the above examples are - equivalent to the following: - - .. configuration-block:: - - .. code-block:: yaml - - symfony2.is.great: Symfony2 is great - symfony2.is.amazing: Symfony2 is amazing - symfony2.has.bundles: Symfony2 has bundles - user.login: Login - - .. code-block:: php - - return array( - 'symfony2.is.great' => 'Symfony2 is great', - 'symfony2.is.amazing' => 'Symfony2 is amazing', - 'symfony2.has.bundles' => 'Symfony2 has bundles', - 'user.login' => 'Login', - ); - -.. index:: - single: Translations; Message domains - -Using Message Domains ---------------------- - -As you've seen, message files are organized into the different locales that -they translate. The message files can also be organized further into "domains". -When creating message files, the domain is the first portion of the filename. -The default domain is ``messages``. For example, suppose that, for organization, -translations were split into three different domains: ``messages``, ``admin`` -and ``navigation``. The French translation would have the following message -files: - -* ``messages.fr.xliff`` -* ``admin.fr.xliff`` -* ``navigation.fr.xliff`` - -When translating strings that are not in the default domain (``messages``), -you must specify the domain as the third argument of ``trans()``:: - - $this->get('translator')->trans('Symfony2 is great', array(), 'admin'); - -Symfony2 will now look for the message in the ``admin`` domain of the user's -locale. - -.. index:: - single: Translations; User's locale - -Handling the User's Locale --------------------------- - -The locale of the current user is stored in the request and is accessible -via the ``request`` object:: - - // access the request object in a standard controller - $request = $this->getRequest(); - - $locale = $request->getLocale(); - - $request->setLocale('en_US'); - -.. index:: - single: Translations; Fallback and default locale - -It is also possible to store the locale in the session instead of on a per -request basis. If you do this, each subsequent request will have this locale. - -.. code-block:: php - - $this->get('session')->set('_locale', 'en_US'); - -See the :ref:`book-translation-locale-url` section below about setting the -locale via routing. - -Fallback and Default Locale -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If the locale hasn't been set explicitly in the session, the ``fallback_locale`` -configuration parameter will be used by the ``Translator``. The parameter -defaults to ``en`` (see `Configuration`_). - -Alternatively, you can guarantee that a locale is set on each user's request -by defining a ``default_locale`` for the framework: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - default_locale: en - - .. code-block:: xml - - - - en - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - 'default_locale' => 'en', - )); - -.. versionadded:: 2.1 - The ``default_locale`` parameter was defined under the session key - originally, however, as of 2.1 this has been moved. This is because the - locale is now set on the request instead of the session. - -.. _book-translation-locale-url: - -The Locale and the URL -~~~~~~~~~~~~~~~~~~~~~~ - -Since you can store the locale of the user in the session, it may be tempting -to use the same URL to display a resource in many different languages based -on the user's locale. For example, ``http://www.example.com/contact`` could -show content in English for one user and French for another user. Unfortunately, -this violates a fundamental rule of the Web: that a particular URL returns -the same resource regardless of the user. To further muddy the problem, which -version of the content would be indexed by search engines? - -A better policy is to include the locale in the URL. This is fully-supported -by the routing system using the special ``_locale`` parameter: - -.. configuration-block:: - - .. code-block:: yaml - - contact: - path: /{_locale}/contact - defaults: { _controller: AcmeDemoBundle:Contact:index, _locale: en } - requirements: - _locale: en|fr|de - - .. code-block:: xml - - - AcmeDemoBundle:Contact:index - en - en|fr|de - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('contact', new Route('/{_locale}/contact', array( - '_controller' => 'AcmeDemoBundle:Contact:index', - '_locale' => 'en', - ), array( - '_locale' => 'en|fr|de', - ))); - - return $collection; - -When using the special `_locale` parameter in a route, the matched locale -will *automatically be set on the user's session*. In other words, if a user -visits the URI ``/fr/contact``, the locale ``fr`` will automatically be set -as the locale for the user's session. - -You can now use the user's locale to create routes to other translated pages -in your application. - -.. index:: - single: Translations; Pluralization - -Pluralization -------------- - -Message pluralization is a tough topic as the rules can be quite complex. For -instance, here is the mathematic representation of the Russian pluralization -rules:: - - (($number % 10 == 1) && ($number % 100 != 11)) - ? 0 - : ((($number % 10 >= 2) - && ($number % 10 <= 4) - && (($number % 100 < 10) - || ($number % 100 >= 20))) - ? 1 - : 2 - ); - -As you can see, in Russian, you can have three different plural forms, each -given an index of 0, 1 or 2. For each form, the plural is different, and -so the translation is also different. - -When a translation has different forms due to pluralization, you can provide -all the forms as a string separated by a pipe (``|``):: - - 'There is one apple|There are %count% apples' - -To translate pluralized messages, use the -:method:`Symfony\\Component\\Translation\\Translator::transChoice` method:: - - $t = $this->get('translator')->transChoice( - 'There is one apple|There are %count% apples', - 10, - array('%count%' => 10) - ); - -The second argument (``10`` in this example), is the *number* of objects being -described and is used to determine which translation to use and also to populate -the ``%count%`` placeholder. - -Based on the given number, the translator chooses the right plural form. -In English, most words have a singular form when there is exactly one object -and a plural form for all other numbers (0, 2, 3...). So, if ``count`` is -``1``, the translator will use the first string (``There is one apple``) -as the translation. Otherwise it will use ``There are %count% apples``. - -Here is the French translation:: - - 'Il y a %count% pomme|Il y a %count% pommes' - -Even if the string looks similar (it is made of two sub-strings separated by a -pipe), the French rules are different: the first form (no plural) is used when -``count`` is ``0`` or ``1``. So, the translator will automatically use the -first string (``Il y a %count% pomme``) when ``count`` is ``0`` or ``1``. - -Each locale has its own set of rules, with some having as many as six different -plural forms with complex rules behind which numbers map to which plural form. -The rules are quite simple for English and French, but for Russian, you'd -may want a hint to know which rule matches which string. To help translators, -you can optionally "tag" each string:: - - 'one: There is one apple|some: There are %count% apples' - - 'none_or_one: Il y a %count% pomme|some: Il y a %count% pommes' - -The tags are really only hints for translators and don't affect the logic -used to determine which plural form to use. The tags can be any descriptive -string that ends with a colon (``:``). The tags also do not need to be the -same in the original message as in the translated one. - -.. tip:: - - As tags are optional, the translator doesn't use them (the translator will - only get a string based on its position in the string). - -Explicit Interval Pluralization -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The easiest way to pluralize a message is to let Symfony2 use internal logic -to choose which string to use based on a given number. Sometimes, you'll -need more control or want a different translation for specific cases (for -``0``, or when the count is negative, for example). For such cases, you can -use explicit math intervals:: - - '{0} There are no apples|{1} There is one apple|]1,19] There are %count% apples|[20,Inf] There are many apples' - -The intervals follow the `ISO 31-11`_ notation. The above string specifies -four different intervals: exactly ``0``, exactly ``1``, ``2-19``, and ``20`` -and higher. - -You can also mix explicit math rules and standard rules. In this case, if -the count is not matched by a specific interval, the standard rules take -effect after removing the explicit rules:: - - '{0} There are no apples|[20,Inf] There are many apples|There is one apple|a_few: There are %count% apples' - -For example, for ``1`` apple, the standard rule ``There is one apple`` will -be used. For ``2-19`` apples, the second standard rule ``There are %count% -apples`` will be selected. - -An :class:`Symfony\\Component\\Translation\\Interval` can represent a finite set -of numbers:: - - {1,2,3,4} - -Or numbers between two other numbers:: - - [1, +Inf[ - ]-1,2[ - -The left delimiter can be ``[`` (inclusive) or ``]`` (exclusive). The right -delimiter can be ``[`` (exclusive) or ``]`` (inclusive). Beside numbers, you -can use ``-Inf`` and ``+Inf`` for the infinite. - -.. index:: - single: Translations; In templates - -Translations in Templates -------------------------- - -Most of the time, translation occurs in templates. Symfony2 provides native -support for both Twig and PHP templates. - -.. _book-translation-tags: - -Twig Templates -~~~~~~~~~~~~~~ - -Symfony2 provides specialized Twig tags (``trans`` and ``transchoice``) to -help with message translation of *static blocks of text*: - -.. code-block:: jinja - - {% trans %}Hello %name%{% endtrans %} - - {% transchoice count %} - {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples - {% endtranschoice %} - -The ``transchoice`` tag automatically gets the ``%count%`` variable from -the current context and passes it to the translator. This mechanism only -works when you use a placeholder following the ``%var%`` pattern. - -.. tip:: - - If you need to use the percent character (``%``) in a string, escape it by - doubling it: ``{% trans %}Percent: %percent%%%{% endtrans %}`` - -You can also specify the message domain and pass some additional variables: - -.. code-block:: jinja - - {% trans with {'%name%': 'Fabien'} from "app" %}Hello %name%{% endtrans %} - - {% trans with {'%name%': 'Fabien'} from "app" into "fr" %}Hello %name%{% endtrans %} - - {% transchoice count with {'%name%': 'Fabien'} from "app" %} - {0} %name%, there are no apples|{1} %name%, there is one apple|]1,Inf] %name%, there are %count% apples - {% endtranschoice %} - -.. _book-translation-filters: - -The ``trans`` and ``transchoice`` filters can be used to translate *variable -texts* and complex expressions: - -.. code-block:: jinja - - {{ message|trans }} - - {{ message|transchoice(5) }} - - {{ message|trans({'%name%': 'Fabien'}, "app") }} - - {{ message|transchoice(5, {'%name%': 'Fabien'}, 'app') }} - -.. tip:: - - Using the translation tags or filters have the same effect, but with - one subtle difference: automatic output escaping is only applied to - translations using a filter. In other words, if you need to be sure - that your translated is *not* output escaped, you must apply the - ``raw`` filter after the translation filter: - - .. code-block:: jinja - - {# text translated between tags is never escaped #} - {% trans %} -

foo

- {% endtrans %} - - {% set message = '

foo

' %} - - {# strings and variables translated via a filter is escaped by default #} - {{ message|trans|raw }} - {{ '

bar

'|trans|raw }} - -.. tip:: - - You can set the translation domain for an entire Twig template with a single tag: - - .. code-block:: jinja - - {% trans_default_domain "app" %} - - Note that this only influences the current template, not any "included" - templates (in order to avoid side effects). - -.. versionadded:: 2.1 - The ``trans_default_domain`` tag is new in Symfony2.1 - -PHP Templates -~~~~~~~~~~~~~ - -The translator service is accessible in PHP templates through the -``translator`` helper: - -.. code-block:: html+php - - trans('Symfony2 is great') ?> - - transChoice( - '{0} There is no apples|{1} There is one apple|]1,Inf[ There are %count% apples', - 10, - array('%count%' => 10) - ) ?> - -Forcing the Translator Locale ------------------------------ - -When translating a message, Symfony2 uses the locale from the current request -or the ``fallback`` locale if necessary. You can also manually specify the -locale to use for translation:: - - $this->get('translator')->trans( - 'Symfony2 is great', - array(), - 'messages', - 'fr_FR' - ); - - $this->get('translator')->transChoice( - '{0} There are no apples|{1} There is one apple|]1,Inf[ There are %count% apples', - 10, - array('%count%' => 10), - 'messages', - 'fr_FR' - ); - -Translating Database Content ----------------------------- - -The translation of database content should be handled by Doctrine through -the `Translatable Extension`_. For more information, see the documentation -for that library. - -.. _book-translation-constraint-messages: - -Translating Constraint Messages -------------------------------- - -The best way to understand constraint translation is to see it in action. To start, -suppose you've created a plain-old-PHP object that you need to use somewhere in -your application:: - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - - class Author - { - public $name; - } - -Add constraints though any of the supported methods. Set the message option to the -translation source text. For example, to guarantee that the $name property is not -empty, add the following: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - name: - - NotBlank: { message: "author.name.not_blank" } - - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\NotBlank(message = "author.name.not_blank") - */ - public $name; - } - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - // src/Acme/BlogBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\NotBlank; - - class Author - { - public $name; - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('name', new NotBlank(array( - 'message' => 'author.name.not_blank', - ))); - } - } - -Create a translation file under the ``validators`` catalog for the constraint messages, typically in the ``Resources/translations/`` directory of the bundle. See `Message Catalogues`_ for more details. - -.. configuration-block:: - - .. code-block:: xml - - - - - - - - author.name.not_blank - Please enter an author name. - - - - - - .. code-block:: php - - // validators.en.php - return array( - 'author.name.not_blank' => 'Please enter an author name.', - ); - - .. code-block:: yaml - - # validators.en.yml - author.name.not_blank: Please enter an author name. - -Summary -------- - -With the Symfony2 Translation component, creating an internationalized application -no longer needs to be a painful process and boils down to just a few basic -steps: - -* Abstract messages in your application by wrapping each in either the - :method:`Symfony\\Component\\Translation\\Translator::trans` or - :method:`Symfony\\Component\\Translation\\Translator::transChoice` methods; - -* Translate each message into multiple locales by creating translation message - files. Symfony2 discovers and processes each file because its name follows - a specific convention; - -* Manage the user's locale, which is stored on the request, but can also - be set on the user's session. - -.. _`i18n`: http://en.wikipedia.org/wiki/Internationalization_and_localization -.. _`L10n`: http://en.wikipedia.org/wiki/Internationalization_and_localization -.. _`strtr function`: http://www.php.net/manual/en/function.strtr.php -.. _`ISO 31-11`: http://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals -.. _`Translatable Extension`: https://github.com/l3pp4rd/DoctrineExtensions -.. _`ISO3166 Alpha-2`: http://en.wikipedia.org/wiki/ISO_3166-1#Current_codes -.. _`ISO639-1`: http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes diff --git a/book/validation.rst b/book/validation.rst deleted file mode 100644 index 6f7956ddcce..00000000000 --- a/book/validation.rst +++ /dev/null @@ -1,861 +0,0 @@ -.. index:: - single: Validation - -Validation -========== - -Validation is a very common task in web applications. Data entered in forms -needs to be validated. Data also needs to be validated before it is written -into a database or passed to a web service. - -Symfony2 ships with a `Validator`_ component that makes this task easy and -transparent. This component is based on the -`JSR303 Bean Validation specification`_. - -.. index:: - single: Validation; The basics - -The Basics of Validation ------------------------- - -The best way to understand validation is to see it in action. To start, suppose -you've created a plain-old-PHP object that you need to use somewhere in -your application:: - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - - class Author - { - public $name; - } - -So far, this is just an ordinary class that serves some purpose inside your -application. The goal of validation is to tell you whether or not the data -of an object is valid. For this to work, you'll configure a list of rules -(called :ref:`constraints`) that the object must -follow in order to be valid. These rules can be specified via a number of -different formats (YAML, XML, annotations, or PHP). - -For example, to guarantee that the ``$name`` property is not empty, add the -following: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - name: - - NotBlank: ~ - - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\NotBlank() - */ - public $name; - } - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // src/Acme/BlogBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\NotBlank; - - class Author - { - public $name; - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('name', new NotBlank()); - } - } - -.. tip:: - - Protected and private properties can also be validated, as well as "getter" - methods (see `validator-constraint-targets`). - -.. index:: - single: Validation; Using the validator - -Using the ``validator`` Service -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Next, to actually validate an ``Author`` object, use the ``validate`` method -on the ``validator`` service (class :class:`Symfony\\Component\\Validator\\Validator`). -The job of the ``validator`` is easy: to read the constraints (i.e. rules) -of a class and verify whether or not the data on the object satisfies those -constraints. If validation fails, an array of errors is returned. Take this -simple example from inside a controller:: - - // ... - use Symfony\Component\HttpFoundation\Response; - use Acme\BlogBundle\Entity\Author; - - public function indexAction() - { - $author = new Author(); - // ... do something to the $author object - - $validator = $this->get('validator'); - $errors = $validator->validate($author); - - if (count($errors) > 0) { - return new Response(print_r($errors, true)); - } else { - return new Response('The author is valid! Yes!'); - } - } - -If the ``$name`` property is empty, you will see the following error -message: - -.. code-block:: text - - Acme\BlogBundle\Author.name: - This value should not be blank - -If you insert a value into the ``name`` property, the happy success message -will appear. - -.. tip:: - - Most of the time, you won't interact directly with the ``validator`` - service or need to worry about printing out the errors. Most of the time, - you'll use validation indirectly when handling submitted form data. For - more information, see the :ref:`book-validation-forms`. - -You could also pass the collection of errors into a template. - -.. code-block:: php - - if (count($errors) > 0) { - return $this->render('AcmeBlogBundle:Author:validate.html.twig', array( - 'errors' => $errors, - )); - } else { - // ... - } - -Inside the template, you can output the list of errors exactly as needed: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/BlogBundle/Resources/views/Author/validate.html.twig #} -

The author has the following errors

-
    - {% for error in errors %} -
  • {{ error.message }}
  • - {% endfor %} -
- - .. code-block:: html+php - - -

The author has the following errors

-
    - -
  • getMessage() ?>
  • - -
- -.. note:: - - Each validation error (called a "constraint violation"), is represented by - a :class:`Symfony\\Component\\Validator\\ConstraintViolation` object. - -.. index:: - single: Validation; Validation with forms - -.. _book-validation-forms: - -Validation and Forms -~~~~~~~~~~~~~~~~~~~~ - -The ``validator`` service can be used at any time to validate any object. -In reality, however, you'll usually work with the ``validator`` indirectly -when working with forms. Symfony's form library uses the ``validator`` service -internally to validate the underlying object after values have been submitted. -The constraint violations on the object are converted into ``FieldError`` -objects that can easily be displayed with your form. The typical form submission -workflow looks like the following from inside a controller:: - - // ... - use Acme\BlogBundle\Entity\Author; - use Acme\BlogBundle\Form\AuthorType; - use Symfony\Component\HttpFoundation\Request; - - public function updateAction(Request $request) - { - $author = new Author(); - $form = $this->createForm(new AuthorType(), $author); - - $form->handleRequest($request); - - if ($form->isValid()) { - // the validation passed, do something with the $author object - - return $this->redirect($this->generateUrl(...)); - } - - return $this->render('BlogBundle:Author:form.html.twig', array( - 'form' => $form->createView(), - )); - } - -.. note:: - - This example uses an ``AuthorType`` form class, which is not shown here. - -For more information, see the :doc:`Forms` chapter. - -.. index:: - pair: Validation; Configuration - -.. _book-validation-configuration: - -Configuration -------------- - -The Symfony2 validator is enabled by default, but you must explicitly enable -annotations if you're using the annotation method to specify your constraints: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - validation: { enable_annotations: true } - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - 'validation' => array( - 'enable_annotations' => true, - ), - )); - -.. index:: - single: Validation; Constraints - -.. _validation-constraints: - -Constraints ------------ - -The ``validator`` is designed to validate objects against *constraints* (i.e. -rules). In order to validate an object, simply map one or more constraints -to its class and then pass it to the ``validator`` service. - -Behind the scenes, a constraint is simply a PHP object that makes an assertive -statement. In real life, a constraint could be: "The cake must not be burned". -In Symfony2, constraints are similar: they are assertions that a condition -is true. Given a value, a constraint will tell you whether or not that value -adheres to the rules of the constraint. - -Supported Constraints -~~~~~~~~~~~~~~~~~~~~~ - -Symfony2 packages a large number of the most commonly-needed constraints: - -.. include:: /reference/constraints/map.rst.inc - -You can also create your own custom constraints. This topic is covered in -the ":doc:`/cookbook/validation/custom_constraint`" article of the cookbook. - -.. index:: - single: Validation; Constraints configuration - -.. _book-validation-constraint-configuration: - -Constraint Configuration -~~~~~~~~~~~~~~~~~~~~~~~~ - -Some constraints, like :doc:`NotBlank`, -are simple whereas others, like the :doc:`Choice` -constraint, have several configuration options available. Suppose that the -``Author`` class has another property, ``gender`` that can be set to either -"male" or "female": - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - gender: - - Choice: { choices: [male, female], message: Choose a valid gender. } - - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\Choice( - * choices = { "male", "female" }, - * message = "Choose a valid gender." - * ) - */ - public $gender; - } - - .. code-block:: xml - - - - - - - - - - - - - - - - .. code-block:: php - - // src/Acme/BlogBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\Choice; - - class Author - { - public $gender; - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('gender', new Choice(array( - 'choices' => array('male', 'female'), - 'message' => 'Choose a valid gender.', - ))); - } - } - -.. _validation-default-option: - -The options of a constraint can always be passed in as an array. Some constraints, -however, also allow you to pass the value of one, "*default*", option in place -of the array. In the case of the ``Choice`` constraint, the ``choices`` -options can be specified in this way. - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - gender: - - Choice: [male, female] - - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\Choice({"male", "female"}) - */ - protected $gender; - } - - .. code-block:: xml - - - - - - - - - male - female - - - - - - .. code-block:: php - - // src/Acme/BlogBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\Choice; - - class Author - { - protected $gender; - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint( - 'gender', - new Choice(array('male', 'female')) - ); - } - } - -This is purely meant to make the configuration of the most common option of -a constraint shorter and quicker. - -If you're ever unsure of how to specify an option, either check the API documentation -for the constraint or play it safe by always passing in an array of options -(the first method shown above). - -Translation Constraint Messages -------------------------------- - -For information on translating the constraint messages, see -:ref:`book-translation-constraint-messages`. - -.. index:: - single: Validation; Constraint targets - -.. _validator-constraint-targets: - -Constraint Targets ------------------- - -Constraints can be applied to a class property (e.g. ``name``) or a public -getter method (e.g. ``getFullName``). The first is the most common and easy -to use, but the second allows you to specify more complex validation rules. - -.. index:: - single: Validation; Property constraints - -.. _validation-property-target: - -Properties -~~~~~~~~~~ - -Validating class properties is the most basic validation technique. Symfony2 -allows you to validate private, protected or public properties. The next -listing shows you how to configure the ``$firstName`` property of an ``Author`` -class to have at least 3 characters. - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - firstName: - - NotBlank: ~ - - Length: - min: 3 - - .. code-block:: php-annotations - - // Acme/BlogBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\NotBlank() - * @Assert\Length(min = "3") - */ - private $firstName; - } - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - // src/Acme/BlogBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\NotBlank; - use Symfony\Component\Validator\Constraints\Length; - - class Author - { - private $firstName; - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('firstName', new NotBlank()); - $metadata->addPropertyConstraint( - 'firstName', - new Length(array("min" => 3))); - } - } - -.. index:: - single: Validation; Getter constraints - -Getters -~~~~~~~ - -Constraints can also be applied to the return value of a method. Symfony2 -allows you to add a constraint to any public method whose name starts with -"get" or "is". In this guide, both of these types of methods are referred -to as "getters". - -The benefit of this technique is that it allows you to validate your object -dynamically. For example, suppose you want to make sure that a password field -doesn't match the first name of the user (for security reasons). You can -do this by creating an ``isPasswordLegal`` method, and then asserting that -this method must return ``true``: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - getters: - passwordLegal: - - "True": { message: "The password cannot match your first name" } - - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\True(message = "The password cannot match your first name") - */ - public function isPasswordLegal() - { - // return true or false - } - } - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // src/Acme/BlogBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\True; - - class Author - { - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addGetterConstraint('passwordLegal', new True(array( - 'message' => 'The password cannot match your first name', - ))); - } - } - -Now, create the ``isPasswordLegal()`` method, and include the logic you need:: - - public function isPasswordLegal() - { - return ($this->firstName != $this->password); - } - -.. note:: - - The keen-eyed among you will have noticed that the prefix of the getter - ("get" or "is") is omitted in the mapping. This allows you to move the - constraint to a property with the same name later (or vice versa) without - changing your validation logic. - -.. _validation-class-target: - -Classes -~~~~~~~ - -Some constraints apply to the entire class being validated. For example, -the :doc:`Callback` constraint is a generic -constraint that's applied to the class itself. When that class is validated, -methods specified by that constraint are simply executed so that each can -provide more custom validation. - -.. _book-validation-validation-groups: - -Validation Groups ------------------ - -So far, you've been able to add constraints to a class and ask whether or -not that class passes all of the defined constraints. In some cases, however, -you'll need to validate an object against only *some* of the constraints -on that class. To do this, you can organize each constraint into one or more -"validation groups", and then apply validation against just one group of -constraints. - -For example, suppose you have a ``User`` class, which is used both when a -user registers and when a user updates his/her contact information later: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\User: - properties: - email: - - Email: { groups: [registration] } - password: - - NotBlank: { groups: [registration] } - - Length: { min: 7, groups: [registration] } - city: - - Length: - min: 2 - - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/User.php - namespace Acme\BlogBundle\Entity; - - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Validator\Constraints as Assert; - - class User implements UserInterface - { - /** - * @Assert\Email(groups={"registration"}) - */ - private $email; - - /** - * @Assert\NotBlank(groups={"registration"}) - * @Assert\Length(min=7, groups={"registration"}) - */ - private $password; - - /** - * @Assert\Length(min = "2") - */ - private $city; - } - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - - - - - - - .. code-block:: php - - // src/Acme/BlogBundle/Entity/User.php - namespace Acme\BlogBundle\Entity; - - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\Email; - use Symfony\Component\Validator\Constraints\NotBlank; - use Symfony\Component\Validator\Constraints\Length; - - class User - { - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('email', new Email(array( - 'groups' => array('registration'), - ))); - - $metadata->addPropertyConstraint('password', new NotBlank(array( - 'groups' => array('registration'), - ))); - $metadata->addPropertyConstraint('password', new Length(array( - 'min' => 7, - 'groups' => array('registration') - ))); - - $metadata->addPropertyConstraint( - 'city', - Length(array("min" => 3))); - } - } - -With this configuration, there are two validation groups: - -* ``Default`` - contains the constraints not assigned to any other group; - -* ``registration`` - contains the constraints on the ``email`` and ``password`` - fields only. - -To tell the validator to use a specific group, pass one or more group names -as the second argument to the ``validate()`` method:: - - $errors = $validator->validate($author, array('registration')); - -Of course, you'll usually work with validation indirectly through the form -library. For information on how to use validation groups inside forms, see -:ref:`book-forms-validation-groups`. - -.. index:: - single: Validation; Validating raw values - -.. _book-validation-raw-values: - -Validating Values and Arrays ----------------------------- - -So far, you've seen how you can validate entire objects. But sometimes, you -just want to validate a simple value - like to verify that a string is a valid -email address. This is actually pretty easy to do. From inside a controller, -it looks like this:: - - use Symfony\Component\Validator\Constraints\Email; - // ... - - public function addEmailAction($email) - { - $emailConstraint = new Email(); - // all constraint "options" can be set this way - $emailConstraint->message = 'Invalid email address'; - - // use the validator to validate the value - $errorList = $this->get('validator')->validateValue( - $email, - $emailConstraint - ); - - if (count($errorList) == 0) { - // this IS a valid email address, do something - } else { - // this is *not* a valid email address - $errorMessage = $errorList[0]->getMessage(); - - // ... do something with the error - } - - // ... - } - -By calling ``validateValue`` on the validator, you can pass in a raw value and -the constraint object that you want to validate that value against. A full -list of the available constraints - as well as the full class name for each -constraint - is available in the :doc:`constraints reference` -section . - -The ``validateValue`` method returns a :class:`Symfony\\Component\\Validator\\ConstraintViolationList` -object, which acts just like an array of errors. Each error in the collection -is a :class:`Symfony\\Component\\Validator\\ConstraintViolation` object, -which holds the error message on its `getMessage` method. - -Final Thoughts --------------- - -The Symfony2 ``validator`` is a powerful tool that can be leveraged to -guarantee that the data of any object is "valid". The power behind validation -lies in "constraints", which are rules that you can apply to properties or -getter methods of your object. And while you'll most commonly use the validation -framework indirectly when using forms, remember that it can be used anywhere -to validate any object. - -Learn more from the Cookbook ----------------------------- - -* :doc:`/cookbook/validation/custom_constraint` - -.. _Validator: https://github.com/symfony/Validator -.. _JSR303 Bean Validation specification: http://jcp.org/en/jsr/detail?id=303 diff --git a/bundles.rst b/bundles.rst new file mode 100644 index 00000000000..878bee3af4a --- /dev/null +++ b/bundles.rst @@ -0,0 +1,171 @@ +.. _page-creation-bundles: + +The Bundle System +================= + +.. warning:: + + In Symfony versions prior to 4.0, it was recommended to organize your own + application code using bundles. This is :ref:`no longer recommended ` and bundles + should only be used to share code and features between multiple applications. + +A bundle is similar to a plugin in other software, but even better. The core +features of Symfony framework are implemented with bundles (FrameworkBundle, +SecurityBundle, DebugBundle, etc.) They are also used to add new features in +your application via `third-party bundles`_. + +Bundles used in your applications must be enabled per +:ref:`environment ` in the ``config/bundles.php`` +file:: + + // config/bundles.php + return [ + // 'all' means that the bundle is enabled for any Symfony environment + Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], + // ... + + // this bundle is enabled only in 'dev' + Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], + // ... + + // this bundle is enabled only in 'dev' and 'test', so you can't use it in 'prod' + Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + // ... + ]; + +.. tip:: + + 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. + +Creating a Bundle +----------------- + +This section creates and enables a new bundle to show there are only a few steps required. +The new bundle is called AcmeBlogBundle, where the ``Acme`` portion is an example +name that should be replaced by some "vendor" name that represents you or your +organization (e.g. AbcBlogBundle for some company named ``Abc``). + +Start by creating a new class called ``AcmeBlogBundle``:: + + // src/AcmeBlogBundle.php + namespace Acme\BlogBundle; + + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + + class AcmeBlogBundle extends AbstractBundle + { + } + +.. warning:: + + If your bundle must be compatible with previous Symfony versions you have to + extend from the :class:`Symfony\\Component\\HttpKernel\\Bundle\\Bundle` instead. + +.. tip:: + + The name AcmeBlogBundle follows the standard + :ref:`Bundle naming conventions `. You could + also choose to shorten the name of the bundle to simply BlogBundle by naming + this class BlogBundle (and naming the file ``BlogBundle.php``). + +This empty class is the only piece you need to create the new bundle. Though +commonly empty, this class is powerful and can be used to customize the behavior +of the bundle. Now that you've created the bundle, enable it:: + + // config/bundles.php + return [ + // ... + Acme\BlogBundle\AcmeBlogBundle::class => ['all' => true], + ]; + +And while it doesn't do anything yet, AcmeBlogBundle is now ready to be used. + +.. _bundles-directory-structure: + +Bundle Directory Structure +-------------------------- + +The directory structure of a bundle is meant to help to keep code consistent +between all Symfony bundles. It follows a set of conventions, but is flexible +to be adjusted if needed: + +``assets/`` + Contains the web asset sources like JavaScript and TypeScript files, CSS and + Sass files, but also images and other assets related to the bundle that are + not in ``public/`` (e.g. Stimulus controllers). + +``config/`` + Houses configuration, including routing configuration (e.g. ``routes.php``). + +``public/`` + Contains web assets (images, compiled CSS and JavaScript files, etc.) and is + copied or symbolically linked into the project ``public/`` directory via the + ``assets:install`` console command. + +``src/`` + Contains all PHP classes related to the bundle logic (e.g. ``Controller/CategoryController.php``). + +``templates/`` + Holds templates organized by controller name (e.g. ``category/show.html.twig``). + +``tests/`` + Holds all tests for the bundle. + +``translations/`` + Holds translations organized by domain and locale (e.g. ``AcmeBlogBundle.en.xlf``). + +.. _bundles-legacy-directory-structure: + +.. warning:: + + The recommended bundle structure was changed in Symfony 5, read the + `Symfony 4.4 bundle documentation`_ for information about the old + structure. + + When using the new ``AbstractBundle`` class, the bundle defaults to the + new structure. Override the ``Bundle::getPath()`` method to change to + the old structure:: + + class AcmeBlogBundle extends AbstractBundle + { + public function getPath(): string + { + return __DIR__; + } + } + +.. tip:: + + It's recommended to use the `PSR-4`_ autoload standard: use the namespace as key, + and the location of the bundle's main class (relative to ``composer.json``) + as value. As the main class is located in the ``src/`` directory of the bundle: + + .. code-block:: json + + { + "autoload": { + "psr-4": { + "Acme\\BlogBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Acme\\BlogBundle\\Tests\\": "tests/" + } + } + } + +Learn more +---------- + +* :doc:`/bundles/override` +* :doc:`/bundles/best_practices` +* :doc:`/bundles/configuration` +* :doc:`/bundles/extension` +* :doc:`/bundles/prepend_extension` + +.. _`third-party bundles`: https://github.com/search?q=topic%3Asymfony-bundle&type=Repositories +.. _`Symfony 4.4 bundle documentation`: https://symfony.com/doc/4.4/bundles.html#bundle-directory-structure +.. _`PSR-4`: https://www.php-fig.org/psr/psr-4/ diff --git a/bundles/best_practices.rst b/bundles/best_practices.rst new file mode 100644 index 00000000000..37dc386b8e4 --- /dev/null +++ b/bundles/best_practices.rst @@ -0,0 +1,571 @@ +Best Practices for Reusable Bundles +=================================== + +This article is all about how to structure your **reusable bundles** to be +configurable and extendable. Reusable bundles are those meant to be shared +privately across many company projects or publicly so any Symfony project can +install them. + +.. _bundles-naming-conventions: + +Bundle Name +----------- + +A bundle is also a PHP namespace. The namespace must follow the `PSR-4`_ +interoperability standard for PHP namespaces and class names: it starts with a +vendor segment, followed by zero or more category segments, and it ends with the +namespace short name, which must end with ``Bundle``. + +A namespace becomes a bundle as soon as you add "a bundle class" to it (which is +a class that extends :class:`Symfony\\Component\\HttpKernel\\Bundle\\Bundle`). +The bundle class name must follow these rules: + +* Use only alphanumeric characters and underscores; +* Use a StudlyCaps name (i.e. camelCase with an uppercase first letter); +* Use a descriptive and short name (no more than two words); +* Prefix the name with the concatenation of the vendor (and optionally the + category namespaces); +* Suffix the name with ``Bundle``. + +Here are some valid bundle namespaces and class names: + +========================== ================== +Namespace Bundle Class Name +========================== ================== +``Acme\Bundle\BlogBundle`` AcmeBlogBundle +``Acme\BlogBundle`` AcmeBlogBundle +========================== ================== + +By convention, the ``getName()`` method of the bundle class should return the +class name. + +.. note:: + + If you share your bundle publicly, you must use the bundle class name as + the name of the repository (AcmeBlogBundle and not BlogBundle for instance). + +.. note:: + + Symfony core Bundles do not prefix the Bundle class with ``Symfony`` + and always add a ``Bundle`` sub-namespace; for example: + :class:`Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle`. + +Each bundle has an alias, which is the lower-cased short version of the bundle +name using underscores (``acme_blog`` for AcmeBlogBundle). This alias +is used to enforce uniqueness within a project and for defining bundle's +configuration options (see below for some usage examples). + +Directory Structure +------------------- + +The following is the recommended directory structure of an AcmeBlogBundle: + +.. code-block:: text + + / + ├── assets/ + ├── config/ + ├── docs/ + │ └─ index.md + ├── public/ + ├── src/ + │ ├── Controller/ + │ ├── DependencyInjection/ + │ └── AcmeBlogBundle.php + ├── templates/ + ├── tests/ + ├── translations/ + ├── LICENSE + └── README.md + +.. note:: + + This directory structure is used by default when your bundle class extends + the recommended :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle`. + If your bundle extends the :class:`Symfony\\Component\\HttpKernel\\Bundle\\Bundle` + class, you have to override the ``getPath()`` method as follows:: + + use Symfony\Component\HttpKernel\Bundle\Bundle; + + class AcmeBlogBundle extends Bundle + { + public function getPath(): string + { + return \dirname(__DIR__); + } + } + +**The following files are mandatory**, because they ensure a structure convention +that automated tools can rely on: + +* ``src/AcmeBlogBundle.php``: This is the class that transforms a plain directory + into a Symfony bundle (change this to your bundle's name); +* ``README.md``: This file contains the basic description of the bundle and it + usually shows some basic examples and links to its full documentation (it + can use any of the markup formats supported by GitHub, such as ``README.rst``); +* ``LICENSE``: The full contents of the license used by the code. Most third-party + bundles are published under the MIT license, but you can `choose any license`_; +* ``docs/index.md``: The root file for the Bundle documentation. + +The depth of subdirectories should be kept to a minimum for the most used +classes and files. Two levels is the maximum. + +The bundle directory is read-only. If you need to write temporary files, store +them under the ``cache/`` or ``log/`` directory of the host application. Tools +can generate files in the bundle directory structure, but only if the generated +files are going to be part of the repository. + +The following classes and files have specific emplacements (some are mandatory +and others are just conventions followed by most developers): + +=================================================== ======================================== +Type Directory +=================================================== ======================================== +Commands ``src/Command/`` +Controllers ``src/Controller/`` +Service Container Extensions ``src/DependencyInjection/`` +Doctrine ORM entities ``src/Entity/`` +Doctrine ODM documents ``src/Document/`` +Event Listeners ``src/EventListener/`` +Configuration (routes, services, etc.) ``config/`` +Web Assets (compiled CSS and JS, images) ``public/`` +Web Asset sources (``.scss``, ``.ts``, Stimulus) ``assets/`` +Translation files ``translations/`` +Validation (when not using attributes) ``config/validation/`` +Serialization (when not using attributes) ``config/serialization/`` +Templates ``templates/`` +Unit and Functional Tests ``tests/`` +=================================================== ======================================== + +Classes +------- + +The bundle directory structure is used as the namespace hierarchy. For +instance, a ``ContentController`` controller which is stored in +``src/Controller/ContentController.php`` would have the fully +qualified class name of ``Acme\BlogBundle\Controller\ContentController``. + +All classes and files must follow the :doc:`Symfony coding standards `. + +Some classes should be seen as facades and should be as short as possible, like +Commands, Helpers, Listeners and Controllers. + +Classes that connect to the event dispatcher should be suffixed with +``Listener``. + +Exception classes should be stored in an ``Exception`` sub-namespace. + +Vendors +------- + +A bundle must not embed third-party PHP libraries. It should rely on the +standard Symfony autoloading instead. + +A bundle should also not embed third-party libraries written in JavaScript, +CSS or any other language. + +Doctrine Entities/Documents +--------------------------- + +If the bundle includes Doctrine ORM entities and/or ODM documents, it's +recommended to define their mapping using XML files stored in +``config/doctrine/``. This allows to override that mapping using the +:doc:`standard Symfony mechanism to override bundle parts `. +This is not possible when using attributes to define the mapping. + +Tests +----- + +A bundle should come with a test suite written with PHPUnit and stored under +the ``tests/`` directory. Tests should follow the following principles: + +* The test suite must be executable with a simple ``phpunit`` command run from + a sample application; +* The functional tests should only be used to test the response output and + some profiling information if you have some; +* The tests should cover at least 95% of the code base. + +.. note:: + + A test suite must not contain ``AllTests.php`` scripts, but must rely on the + existence of a ``phpunit.xml.dist`` file. + +Continuous Integration +---------------------- + +Testing bundle code continuously, including all its commits and pull requests, +is a good practice called Continuous Integration. There are several services +providing this feature for free for open source projects, like `GitHub Actions`_ +and `Travis CI`_. + +A bundle should at least test: + +* The lower bound of their dependencies (by running ``composer update --prefer-lowest``); +* The supported PHP versions; +* All supported major Symfony versions (e.g. both ``4.x`` and ``5.x`` if + support is claimed for both). + +Thus, a bundle supporting PHP 7.3, 7.4 and 8.0, and Symfony 4.4 and 5.x should +have at least this test matrix: + +=========== =============== =================== +PHP version Symfony version Composer flags +=========== =============== =================== +7.3 ``4.*`` ``--prefer-lowest`` +7.4 ``5.*`` +8.0 ``5.*`` +=========== =============== =================== + +.. tip:: + + The tests should be run with the ``SYMFONY_DEPRECATIONS_HELPER`` + env variable set to ``max[direct]=0``. This ensures no code in the + bundle uses deprecated features directly. + + The lowest dependency tests can be run with this variable set to + ``disabled=1``. + +Require a Specific Symfony Version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can use the special ``SYMFONY_REQUIRE`` environment variable together +with Symfony Flex to install a specific Symfony version: + +.. code-block:: bash + + # this requires Symfony 5.x for all Symfony packages + export SYMFONY_REQUIRE=5.* + # alternatively you can run this command to update composer.json config + # composer config extra.symfony.require "5.*" + + # install Symfony Flex in the CI environment + composer global config --no-plugins allow-plugins.symfony/flex true + composer global require --no-progress --no-scripts --no-plugins symfony/flex + + # install the dependencies (using --prefer-dist and --no-progress is + # recommended to have a better output and faster download time) + composer update --prefer-dist --no-progress + +.. warning:: + + If you want to cache your Composer dependencies, **do not** cache the + ``vendor/`` directory as this has side-effects. Instead cache + ``$HOME/.composer/cache/files``. + +Installation +------------ + +Bundles should set ``"type": "symfony-bundle"`` in their ``composer.json`` file. +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 +``.gitignore``, etc), then you should create a `Symfony Flex recipe`_. + +Documentation +------------- + +All classes and functions must come with full PHPDoc. + +Extensive documentation should also be provided in the ``docs/`` +directory. +The index file (for example ``docs/index.rst`` or +``docs/index.md``) is the only mandatory file and must be the entry +point for the documentation. The +:doc:`reStructuredText (rST) ` is the format +used to render the documentation on the Symfony website. + +Installation Instructions +~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to ease the installation of third-party bundles, consider using the +following standardized instructions in your ``README.md`` file. + +.. configuration-block:: + + .. code-block:: markdown + + Installation + ============ + + Make sure Composer is installed globally, as explained in the + [installation chapter](https://getcomposer.org/doc/00-intro.md) + of the Composer documentation. + + Applications that use Symfony Flex + ---------------------------------- + + Open a command console, enter your project directory and execute: + + ```console + composer require + ``` + + Applications that don't use Symfony Flex + ---------------------------------------- + + ### Step 1: Download the Bundle + + Open a command console, enter your project directory and execute the + following command to download the latest stable version of this bundle: + + ```console + composer require + ``` + + ### Step 2: Enable the Bundle + + Then, enable the bundle by adding it to the list of registered bundles + in the `config/bundles.php` file of your project: + + ```php + // config/bundles.php + + return [ + // ... + \\::class => ['all' => true], + ]; + ``` + + .. code-block:: rst + + Installation + ============ + + Make sure Composer is installed globally, as explained in the + `installation chapter`_ of the Composer documentation. + + ---------------------------------- + + Open a command console, enter your project directory and execute: + + .. code-block:: terminal + + composer require + + Applications that don't use Symfony Flex + ---------------------------------------- + + Step 1: Download the Bundle + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Open a command console, enter your project directory and execute the + following command to download the latest stable version of this bundle: + + .. code-block:: terminal + + composer require + + Step 2: Enable the Bundle + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + Then, enable the bundle by adding it to the list of registered bundles + in the ``config/bundles.php`` file of your project:: + + // config/bundles.php + return [ + // ... + \\::class => ['all' => true], + ]; + + .. _`installation chapter`: https://getcomposer.org/doc/00-intro.md + +The example above assumes that you are installing the latest stable version of +the bundle, where you don't have to provide the package version number +(e.g. ``composer require friendsofsymfony/user-bundle``). If the installation +instructions refer to some past bundle version or to some unstable version, +include the version constraint (e.g. ``composer require friendsofsymfony/user-bundle "~2.0@dev"``). + +Optionally, you can add more installation steps (*Step 3*, *Step 4*, etc.) to +explain other required installation tasks, such as registering routes or +dumping assets. + +Routing +------- + +If the bundle provides routes, they must be prefixed with the bundle alias. +For example, if your bundle is called AcmeBlogBundle, all its routes must be +prefixed with ``acme_blog_``. + +Templates +--------- + +If a bundle provides templates, they must use Twig. A bundle must not provide +a main layout, except if it provides a full working application. + +Translation Files +----------------- + +If a bundle provides message translations, they must be defined in the XLIFF +format; the domain should be named after the bundle name (``AcmeBlog``). + +A bundle must not override existing messages from another bundle. + +The translation domain must match the translation file names. For example, +if the translation domain is ``AcmeBlog``, the English translation file name +should be ``AcmeBlog.en.xlf``. + +Configuration +------------- + +To provide more flexibility, a bundle can provide configurable settings by +using the Symfony built-in mechanisms. + +For simple configuration settings, rely on the default ``parameters`` entry of +the Symfony configuration. Symfony parameters are simple key/value pairs; a +value being any valid PHP value. Each parameter name should start with the +bundle alias, though this is just a best-practice suggestion. The rest of the +parameter name will use a period (``.``) to separate different parts (e.g. +``acme_blog.author.email``). + +The end user can provide values in any configuration file: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + acme_blog.author.email: 'fabien@example.com' + + .. code-block:: xml + + + + + + fabien@example.com + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return static function (ContainerConfigurator $container): void { + $container->parameters() + ->set('acme_blog.author.email', 'fabien@example.com') + ; + }; + +Retrieve the configuration parameters in your code from the container:: + + $container->getParameter('acme_blog.author.email'); + +While this mechanism requires the least effort, you should consider using the +more advanced :doc:`semantic bundle configuration ` to +make your configuration more robust. + +Versioning +---------- + +Bundles must be versioned following the `Semantic Versioning Standard`_. + +Services +-------- + +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, +:ref:`aliases should be created ` from the interface/class +to the service id. For example, in MonologBundle, an alias is created from +``Psr\Log\LoggerInterface`` to ``logger`` so that the ``LoggerInterface`` type-hint +can be used for autowiring. + +Services should not use autowiring or autoconfiguration. Instead, all services should +be defined explicitly. + +.. tip:: + + If there is no intention for the service id to be used by the end user, you can + mark it as *hidden* by prefixing it with a dot (e.g. ``.acme_blog.logger``). + This prevents the service from being listed in the default ``debug:container`` + command output. + +.. seealso:: + + You can learn much more about service loading in bundles reading this article: + :doc:`How to Load Service Configuration inside a Bundle `. + +Composer Metadata +----------------- + +The ``composer.json`` file should include at least the following metadata: + +``name`` + Consists of the vendor and the short bundle name. If you are releasing the + bundle on your own instead of on behalf of a company, use your personal name + (e.g. ``johnsmith/blog-bundle``). Exclude the vendor name from the bundle + short name and separate each word with a hyphen. For example: AcmeBlogBundle + is transformed into ``blog-bundle`` and AcmeSocialConnectBundle is + transformed into ``social-connect-bundle``. + +``description`` + A brief explanation of the purpose of the bundle. + +``type`` + Use the ``symfony-bundle`` value. + +``license`` + a string (or array of strings) with a `valid license identifier`_, such as ``MIT``. + +``autoload`` + This information is used by Symfony to load the classes of the bundle. It's + recommended to use the `PSR-4`_ autoload standard: use the namespace as key, + and the location of the bundle's main class (relative to ``composer.json``) + as value. As the main class is located in the ``src/`` directory of the bundle: + + .. code-block:: json + + { + "autoload": { + "psr-4": { + "Acme\\BlogBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Acme\\BlogBundle\\Tests\\": "tests/" + } + } + } + +In order to make it easier for developers to find your bundle, register it on +`Packagist`_, the official repository for Composer packages. + +Resources +--------- + +If the bundle references any resources (config files, translation files, etc.), +you can use physical paths (e.g. ``__DIR__/config/services.xml``). + +In the past, we recommended to only use logical paths (e.g. +``@AcmeBlogBundle/config/services.xml``) and resolve them with the +:ref:`resource locator ` provided by the Symfony +kernel, but this is no longer a recommended practice. + +Learn more +---------- + +* :doc:`/bundles/extension` +* :doc:`/bundles/configuration` + +.. _`PSR-4`: https://www.php-fig.org/psr/psr-4/ +.. _`Symfony Flex recipe`: https://github.com/symfony/recipes +.. _`Semantic Versioning Standard`: https://semver.org/ +.. _`Packagist`: https://packagist.org/ +.. _`choose any license`: https://choosealicense.com/ +.. _`valid license identifier`: https://spdx.org/licenses/ +.. _`GitHub Actions`: https://docs.github.com/en/free-pro-team@latest/actions +.. _`Travis CI`: https://docs.travis-ci.com/ diff --git a/bundles/configuration.rst b/bundles/configuration.rst new file mode 100644 index 00000000000..dedfada2ea2 --- /dev/null +++ b/bundles/configuration.rst @@ -0,0 +1,539 @@ +How to Create Friendly Configuration for a Bundle +================================================= + +If you open your main application configuration directory (usually +``config/packages/``), you'll see a number of different files, such as +``framework.yaml``, ``twig.yaml`` and ``doctrine.yaml``. Each of these +configures a specific bundle, allowing you to define options at a high level and +then let the bundle make all the low-level, complex changes based on your +settings. + +For example, the following configuration tells the FrameworkBundle to enable the +form integration, which involves the definition of quite a few services as well +as integration of other related components: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + form: true + + .. code-block:: xml + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->form()->enabled(true); + }; + +There are two different ways of creating friendly configuration for a bundle: + +#. :ref:`Using the main bundle class `: + this is recommended for new bundles and for bundles following the + :ref:`recommended directory structure `; +#. :ref:`Using the Bundle extension class `: + this was the traditional way of doing it, but nowadays it's only recommended for + bundles following the :ref:`legacy directory structure `. + +.. _using-the-bundle-class: +.. _bundle-friendly-config-bundle-class: + +Using the AbstractBundle Class +------------------------------ + +In bundles extending the :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` +class, you can add all the logic related to processing the configuration in that class:: + + // src/AcmeSocialBundle.php + namespace Acme\SocialBundle; + + use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + + class AcmeSocialBundle extends AbstractBundle + { + public function configure(DefinitionConfigurator $definition): void + { + $definition->rootNode() + ->children() + ->arrayNode('twitter') + ->children() + ->integerNode('client_id')->end() + ->scalarNode('client_secret')->end() + ->end() + ->end() // twitter + ->end() + ; + } + + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + // the "$config" variable is already merged and processed so you can + // use it directly to configure the service container (when defining an + // extension class, you also have to do this merging and processing) + $container->services() + ->get('acme_social.twitter_client') + ->arg(0, $config['twitter']['client_id']) + ->arg(1, $config['twitter']['client_secret']) + ; + } + } + +.. note:: + + The ``configure()`` and ``loadExtension()`` methods are called only at compile time. + +.. tip:: + + The ``AbstractBundle::configure()`` method also allows to import the + configuration definition from one or more files:: + + // src/AcmeSocialBundle.php + namespace Acme\SocialBundle; + + // ... + class AcmeSocialBundle extends AbstractBundle + { + public function configure(DefinitionConfigurator $definition): void + { + $definition->import('../config/definition.php'); + // you can also use glob patterns + //$definition->import('../config/definition/*.php'); + } + + // ... + } + + .. code-block:: php + + // config/definition.php + use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; + + return static function (DefinitionConfigurator $definition): void { + $definition->rootNode() + ->children() + ->scalarNode('foo')->defaultValue('bar')->end() + ->end() + ; + }; + +.. _bundle-friendly-config-extension: + +Using the Bundle Extension +-------------------------- + +This is the traditional way of creating friendly configuration for bundles. For new +bundles it's recommended to :ref:`use the main bundle class `, +but the traditional way of creating an extension class still works. + +Imagine you are creating a new bundle - AcmeSocialBundle - which provides +integration with X/Twitter. To make your bundle configurable to the user, you +can add some configuration that looks like this: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/acme_social.yaml + acme_social: + twitter: + client_id: 123 + client_secret: your_secret + + .. code-block:: xml + + + + + + + + + + .. code-block:: php + + // config/packages/acme_social.php + use Symfony\Config\AcmeSocialConfig; + + return static function (AcmeSocialConfig $acmeSocial): void { + $acmeSocial->twitter() + ->clientId(123) + ->clientSecret('your_secret'); + }; + +The basic idea is that instead of having the user override individual +parameters, you let the user configure just a few, specifically created, +options. As the bundle developer, you then parse through that configuration and +load correct services and parameters inside an "Extension" class. + +.. note:: + + The root key of your bundle configuration (``acme_social`` in the previous + example) is automatically determined from your bundle name (it's the + `snake case`_ of the bundle name without the ``Bundle`` suffix). + +.. seealso:: + + Read more about the extension in :doc:`/bundles/extension`. + +.. tip:: + + If a bundle provides an Extension class, then you should *not* generally + override any service container parameters from that bundle. The idea + is that if an extension class is present, every setting that should be + configurable should be present in the configuration made available by + that class. In other words, the extension class defines all the public + configuration settings for which backward compatibility will be maintained. + +.. seealso:: + + For parameter handling within a dependency injection container see + :doc:`/configuration/using_parameters_in_dic`. + +Processing the ``$configs`` Array +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First things first, you have to create an extension class as explained in +:doc:`/bundles/extension`. + +Whenever a user includes the ``acme_social`` key (which is the DI alias) in a +configuration file, the configuration under it is added to an array of +configurations and passed to the ``load()`` method of your extension (Symfony +automatically converts XML and YAML to an array). + +For the configuration example in the previous section, the array passed to your +``load()`` method will look like this:: + + [ + [ + 'twitter' => [ + 'client_id' => 123, + 'client_secret' => 'your_secret', + ], + ], + ] + +Notice that this is an *array of arrays*, not just a single flat array of the +configuration values. This is intentional, as it allows Symfony to parse several +configuration resources. For example, if ``acme_social`` appears in another +configuration file - say ``config/packages/dev/acme_social.yaml`` - with +different values beneath it, the incoming array might look like this:: + + [ + // values from config/packages/acme_social.yaml + [ + 'twitter' => [ + 'client_id' => 123, + 'client_secret' => 'your_secret', + ], + ], + // values from config/packages/dev/acme_social.yaml + [ + 'twitter' => [ + 'client_id' => 456, + ], + ], + ] + +The order of the two arrays depends on which one is set first. + +But don't worry! Symfony's Config component will help you merge these values, +provide defaults and give the user validation errors on bad configuration. +Here's how it works. Create a ``Configuration`` class in the +``DependencyInjection`` directory and build a tree that defines the structure +of your bundle's configuration. + +The ``Configuration`` class to handle the sample configuration looks like:: + + // src/DependencyInjection/Configuration.php + namespace Acme\SocialBundle\DependencyInjection; + + use Symfony\Component\Config\Definition\Builder\TreeBuilder; + use Symfony\Component\Config\Definition\ConfigurationInterface; + + class Configuration implements ConfigurationInterface + { + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('acme_social'); + + $treeBuilder->getRootNode() + ->children() + ->arrayNode('twitter') + ->children() + ->integerNode('client_id')->end() + ->scalarNode('client_secret')->end() + ->end() + ->end() // twitter + ->end() + ; + + return $treeBuilder; + } + } + +.. seealso:: + + The ``Configuration`` class can be much more complicated than shown here, + supporting "prototype" nodes, advanced validation, XML-specific normalization + and advanced merging. You can read more about this in + :doc:`the Config component documentation `. You + can also see it in action by checking out some core Configuration + classes, such as the one from the `FrameworkBundle Configuration`_ or the + `TwigBundle Configuration`_. + +This class can now be used in your ``load()`` method to merge configurations and +force validation (e.g. if an additional option was passed, an exception will be +thrown):: + + // src/DependencyInjection/AcmeSocialExtension.php + public function load(array $configs, ContainerBuilder $container): void + { + $configuration = new Configuration(); + + $config = $this->processConfiguration($configuration, $configs); + + // you now have these 2 config keys + // $config['twitter']['client_id'] and $config['twitter']['client_secret'] + } + +The ``processConfiguration()`` method uses the configuration tree you've defined +in the ``Configuration`` class to validate, normalize and merge all the +configuration arrays together. + +Now, you can use the ``$config`` variable to modify a service provided by your bundle. +For example, imagine your bundle has the following example config: + +.. code-block:: xml + + + + + + + + + + + + +In your extension, you can load this and dynamically set its arguments:: + + // src/DependencyInjection/AcmeSocialExtension.php + namespace Acme\SocialBundle\DependencyInjection; + + use Symfony\Component\Config\FileLocator; + use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; + + public function load(array $configs, ContainerBuilder $container): void + { + $loader = new XmlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config')); + $loader->load('services.xml'); + + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + $definition = $container->getDefinition('acme_social.twitter_client'); + $definition->replaceArgument(0, $config['twitter']['client_id']); + $definition->replaceArgument(1, $config['twitter']['client_secret']); + } + +.. tip:: + + Instead of calling ``processConfiguration()`` in your extension each time you + provide some configuration options, you might want to use the + :class:`Symfony\\Component\\HttpKernel\\DependencyInjection\\ConfigurableExtension` + to do this automatically for you:: + + // src/DependencyInjection/HelloExtension.php + namespace Acme\HelloBundle\DependencyInjection; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension; + + class AcmeHelloExtension extends ConfigurableExtension + { + // note that this method is called loadInternal and not load + protected function loadInternal(array $mergedConfig, ContainerBuilder $container): void + { + // ... + } + } + + This class uses the ``getConfiguration()`` method to get the Configuration + instance. + +.. sidebar:: Processing the Configuration yourself + + Using the Config component is fully optional. The ``load()`` method gets an + array of configuration values. You can instead parse these arrays yourself + (e.g. by overriding configurations and using :phpfunction:`isset` to check + for the existence of a value). Be aware that it'll be very hard to support XML:: + + public function load(array $configs, ContainerBuilder $container): void + { + $config = []; + // let resources override the previous set value + foreach ($configs as $subConfig) { + $config = array_merge($config, $subConfig); + } + + // ... now use the flat $config array + } + +Modifying the Configuration of Another Bundle +--------------------------------------------- + +If you have multiple bundles that depend on each other, it may be useful to +allow one ``Extension`` class to modify the configuration passed to another +bundle's ``Extension`` class. This can be achieved using a prepend extension. +For more details, see :doc:`/bundles/prepend_extension`. + +Dump the Configuration +---------------------- + +The ``config:dump-reference`` command dumps the default configuration of a +bundle in the console using the Yaml format. + +As long as your bundle's configuration is located in the standard location +(``/src/DependencyInjection/Configuration``) and does not have +a constructor, it will work automatically. If you +have something different, your ``Extension`` class must override the +:method:`Extension::getConfiguration() ` +method and return an instance of your ``Configuration``. + +Supporting XML +-------------- + +Symfony allows people to provide the configuration in three different formats: +Yaml, XML and PHP. Both Yaml and PHP use the same syntax and are supported by +default when using the Config component. Supporting XML requires you to do some +more things. But when sharing your bundle with others, it is recommended that +you follow these steps. + +Make your Config Tree ready for XML +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Config component provides some methods by default to allow it to correctly +process XML configuration. See ":ref:`component-config-normalization`" of the +component documentation. However, you can do some optional things as well, this +will improve the experience of using XML configuration: + +Choosing an XML Namespace +~~~~~~~~~~~~~~~~~~~~~~~~~ + +In XML, the `XML namespace`_ is used to determine which elements belong to the +configuration of a specific bundle. The namespace is returned from the +:method:`Extension::getNamespace() ` +method. By convention, the namespace is a URL (it doesn't have to be a valid +URL nor does it need to exist). By default, the namespace for a bundle is +``http://example.org/schema/dic/DI_ALIAS``, where ``DI_ALIAS`` is the DI alias of +the extension. You might want to change this to a more professional URL:: + + // src/DependencyInjection/AcmeHelloExtension.php + namespace Acme\HelloBundle\DependencyInjection; + + // ... + class AcmeHelloExtension extends Extension + { + // ... + + public function getNamespace(): string + { + return 'http://acme_company.com/schema/dic/hello'; + } + } + +Providing an XML Schema +~~~~~~~~~~~~~~~~~~~~~~~ + +XML has a very useful feature called `XML schema`_. This allows you to +describe all possible elements and attributes and their values in an XML Schema +Definition (an XSD file). This XSD file is used by IDEs for auto completion and +it is used by the Config component to validate the elements. + +In order to use the schema, the XML configuration file must provide an +``xsi:schemaLocation`` attribute pointing to the XSD file for a certain XML +namespace. This location always starts with the XML namespace. This XML +namespace is then replaced with the XSD validation base path returned from +:method:`Extension::getXsdValidationBasePath() ` +method. This namespace is then followed by the rest of the path from the base +path to the file itself. + +By convention, the XSD file lives in ``config/schema/`` directory, but you +can place it anywhere you like. You should return this path as the base path:: + + // src/DependencyInjection/AcmeHelloExtension.php + namespace Acme\HelloBundle\DependencyInjection; + + // ... + class AcmeHelloExtension extends Extension + { + // ... + + public function getXsdValidationBasePath(): string + { + return __DIR__.'/../config/schema'; + } + } + +Assuming the XSD file is called ``hello-1.0.xsd``, the schema location will be +``https://acme_company.com/schema/dic/hello/hello-1.0.xsd``: + +.. code-block:: xml + + + + + + + + + + + +.. _`FrameworkBundle Configuration`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +.. _`TwigBundle Configuration`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php +.. _`XML namespace`: https://en.wikipedia.org/wiki/XML_namespace +.. _`XML schema`: https://en.wikipedia.org/wiki/XML_schema +.. _`snake case`: https://en.wikipedia.org/wiki/Snake_case diff --git a/bundles/extension.rst b/bundles/extension.rst new file mode 100644 index 00000000000..d2792efc477 --- /dev/null +++ b/bundles/extension.rst @@ -0,0 +1,208 @@ +How to Load Service Configuration inside a Bundle +================================================= + +Services created by bundles are not defined in the main ``config/services.yaml`` +file used by the application but in the bundles themselves. This article +explains how to create and load service files using the bundle directory +structure. + +There are two different ways of doing it: + +#. :ref:`Load your services in the main bundle class `: + this is recommended for new bundles and for bundles following the + :ref:`recommended directory structure `; +#. :ref:`Create an extension class to load the service configuration files `: + this was the traditional way of doing it, but nowadays it's only recommended for + bundles following the :ref:`legacy directory structure `. + +.. _bundle-load-services-bundle-class: + +Loading Services Directly in your Bundle Class +---------------------------------------------- + +In bundles extending the :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` +class, you can define the :method:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle::loadExtension` +method to load service definitions from configuration files:: + + // ... + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + + class AcmeHelloBundle extends AbstractBundle + { + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + // load an XML, PHP or YAML file + $container->import('../config/services.xml'); + + // you can also add or replace parameters and services + $container->parameters() + ->set('acme_hello.phrase', $config['phrase']) + ; + + if ($config['scream']) { + $container->services() + ->get('acme_hello.printer') + ->class(ScreamingPrinter::class) + ; + } + } + } + +This method works similar to the ``Extension::load()`` method explained below, +but it uses a new simpler API to define and import service configuration. + +.. note:: + + Contrary to the ``$configs`` parameter in ``Extension::load()``, the + ``$config`` parameter is already merged and processed by the + ``AbstractBundle``. + +.. note:: + + The ``loadExtension()`` is called only at compile time. + +.. _bundle-load-services-extension: + +Creating an Extension Class +--------------------------- + +This is the traditional way of loading service definitions in bundles. For new +bundles it's recommended to :ref:`load your services in the main bundle class `, +but the traditional way of creating an extension class still works. + +A dependency injection extension is defined as a class that follows these +conventions (later you'll learn how to skip them if needed): + +* It has to live in the ``DependencyInjection`` namespace of the bundle; + +* It has to implement the :class:`Symfony\\Component\\DependencyInjection\\Extension\\ExtensionInterface`, + which is usually achieved by extending the + :class:`Symfony\\Component\\DependencyInjection\\Extension\\Extension` class; + +* The name is equal to the bundle name with the ``Bundle`` suffix replaced by + ``Extension`` (e.g. the extension class of the AcmeBundle would be called + ``AcmeExtension`` and the one for AcmeHelloBundle would be called + ``AcmeHelloExtension``). + +This is how the extension of an AcmeHelloBundle should look like:: + + // src/DependencyInjection/AcmeHelloExtension.php + namespace Acme\HelloBundle\DependencyInjection; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Extension\Extension; + + class AcmeHelloExtension extends Extension + { + public function load(array $configs, ContainerBuilder $container): void + { + // ... you'll load the files here later + } + } + +Manually Registering an Extension Class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When not following the conventions, you will have to manually register your +extension. To do this, you should override the +:method:`Bundle::getContainerExtension() ` +method to return the instance of the extension:: + + // ... + use Acme\HelloBundle\DependencyInjection\UnconventionalExtensionClass; + use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; + + class AcmeHelloBundle extends Bundle + { + public function getContainerExtension(): ?ExtensionInterface + { + return new UnconventionalExtensionClass(); + } + } + +In addition, when the new Extension class name doesn't follow the naming +conventions, you must also override the +:method:`Extension::getAlias() ` +method to return the correct DI alias. The DI alias is the name used to refer to +the bundle in the container (e.g. in the ``config/packages/`` files). By +default, this is done by removing the ``Extension`` suffix and converting the +class name to underscores (e.g. ``AcmeHelloExtension``'s DI alias is +``acme_hello``). + +Using the ``load()`` Method +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the ``load()`` method, all services and parameters related to this extension +will be loaded. This method doesn't get the actual container instance, but a +copy. This container only has the parameters from the actual container. After +loading the services and parameters, the copy will be merged into the actual +container, to ensure all services and parameters are also added to the actual +container. + +In the ``load()`` method, you can use PHP code to register service definitions, +but it is more common if you put these definitions in a configuration file +(using the YAML, XML or PHP format). + +For instance, assume you have a file called ``services.xml`` in the +``config/`` directory of your bundle, your ``load()`` method looks like:: + + use Symfony\Component\Config\FileLocator; + use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; + + // ... + public function load(array $configs, ContainerBuilder $container): void + { + $loader = new XmlFileLoader( + $container, + new FileLocator(__DIR__.'/../../config') + ); + $loader->load('services.xml'); + } + +The other available loaders are ``YamlFileLoader`` and ``PhpFileLoader``. + +Using Configuration to Change the Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Extension is also the class that handles the configuration for that +particular bundle (e.g. the configuration in ``config/packages/.yaml``). +To read more about it, see the ":doc:`/bundles/configuration`" article. + +Adding Classes to Compile +------------------------- + +Bundles can hint Symfony about which of their classes contain annotations so +they are compiled when generating the application cache to improve the overall +performance. Define the list of annotated classes to compile in the +``addAnnotatedClassesToCompile()`` method:: + + public function load(array $configs, ContainerBuilder $container): void + { + // ... + + $this->addAnnotatedClassesToCompile([ + // you can define the fully qualified class names... + 'Acme\\BlogBundle\\Controller\\AuthorController', + // ... but glob patterns are also supported: + 'Acme\\BlogBundle\\Form\\**', + + // ... + ]); + } + +.. note:: + + If some class extends from other classes, all its parents are automatically + included in the list of classes to compile. + +Patterns are transformed into the actual class namespaces using the classmap +generated by Composer. Therefore, before using these patterns, you must generate +the full classmap executing the ``dump-autoload`` command of Composer. + +.. warning:: + + This technique can't be used when the classes to compile use the ``__DIR__`` + or ``__FILE__`` constants, because their values will change when loading + these classes from the ``classes.php`` file. diff --git a/bundles/index.rst b/bundles/index.rst index d8f1298f5d8..58bcd13761e 100644 --- a/bundles/index.rst +++ b/bundles/index.rst @@ -1,13 +1,11 @@ -The Symfony Standard Edition Bundles -==================================== +Bundles +======= .. toctree:: - :hidden: + :maxdepth: 2 - SensioFrameworkExtraBundle/index - SensioGeneratorBundle/index - DoctrineFixturesBundle/index - DoctrineMigrationsBundle/index - DoctrineMongoDBBundle/index - -.. include:: /bundles/map.rst.inc + override + best_practices + configuration + extension + prepend_extension diff --git a/bundles/map.rst.inc b/bundles/map.rst.inc deleted file mode 100644 index 44424cc6a1d..00000000000 --- a/bundles/map.rst.inc +++ /dev/null @@ -1,10 +0,0 @@ -* :doc:`SensioFrameworkExtraBundle ` -* :doc:`SensioGeneratorBundle ` -* `JMSSecurityExtraBundle`_ -* `JMSDiExtraBundle`_ -* :doc:`DoctrineFixturesBundle ` -* :doc:`DoctrineMigrationsBundle ` -* :doc:`DoctrineMongoDBBundle ` - -.. _`JMSSecurityExtraBundle`: http://jmsyst.com/bundles/JMSSecurityExtraBundle/1.2 -.. _`JMSDiExtraBundle`: http://jmsyst.com/bundles/JMSDiExtraBundle/1.1 diff --git a/bundles/override.rst b/bundles/override.rst new file mode 100644 index 00000000000..f25bd785373 --- /dev/null +++ b/bundles/override.rst @@ -0,0 +1,168 @@ +How to Override any Part of a Bundle +==================================== + +When using a third-party bundle, you might want to customize or override some of +its features. This document describes ways of overriding the most common +features of a bundle. + +.. _override-templates: + +Templates +--------- + +Third-party bundle templates can be overridden in the +``/templates/bundles//`` directory. The new templates +must use the same name and path (relative to ``/templates/``) as +the original templates. + +For example, to override the ``templates/registration/confirmed.html.twig`` +template from the AcmeUserBundle, create this template: +``/templates/bundles/AcmeUserBundle/registration/confirmed.html.twig`` + +.. warning:: + + If you add a template in a new location, you *may* need to clear your + cache (``php bin/console cache:clear``), even if you are in debug mode. + +Instead of overriding an entire template, you may just want to override one or +more blocks. However, since you are overriding the template you want to extend +from, you would end up in an infinite loop error. The solution is to use the +special ``!`` prefix in the template name to tell Symfony that you want to +extend from the original template, not from the overridden one: + +.. code-block:: twig + + {# templates/bundles/AcmeUserBundle/registration/confirmed.html.twig #} + {# the special '!' prefix avoids errors when extending from an overridden template #} + {% extends "@!AcmeUser/registration/confirmed.html.twig" %} + + {% block some_block %} + ... + {% endblock %} + +.. _templating-overriding-core-templates: + +.. tip:: + + Symfony internals use some bundles too, so you can apply the same technique + to override the core Symfony templates. For example, you can + :doc:`customize error pages ` overriding TwigBundle + templates. + +Routing +------- + +Routing is never automatically imported in Symfony. If you want to include +the routes from any bundle, then they must be manually imported from somewhere +in your application (e.g. ``config/routes.yaml``). + +The easiest way to "override" a bundle's routing is to never import it at +all. Instead of importing a third-party bundle's routing, copy +that routing file into your application, modify it, and import it instead. + +Controllers +----------- + +If the controller is a service, see the next section on how to override it. +Otherwise, define a new route + controller with the same path associated to the +controller you want to override (and make sure that the new route is loaded +before the bundle one). + +Services & Configuration +------------------------ + +If you want to modify the services created by a bundle, you can use +:doc:`service decoration `. + +If you want to do more advanced manipulations, like removing services created by +other bundles, you must work with :doc:`service definitions ` +inside a :doc:`compiler pass `. + +Entities & Entity Mapping +------------------------- + +Overriding entity mapping is only possible if a bundle provides a mapped +superclass (such as the ``User`` entity in the FOSUserBundle). It's possible to +override attributes and associations in this way. Learn more about this feature +and its limitations in `the Doctrine documentation`_. + +Forms +----- + +Existing form types can be modified defining +:doc:`form type extensions `. + +.. _override-validation: + +Validation Metadata +------------------- + +Symfony loads all validation configuration files from every bundle and +combines them into one validation metadata tree. This means you are able to +add new constraints to a property, but you cannot override them. + +To overcome this, the 3rd party bundle needs to have configuration for +:doc:`validation groups `. For instance, the FOSUserBundle +has this configuration. To create your own validation, add the constraints +to a new validation group: + +.. configuration-block:: + + .. code-block:: yaml + + # config/validator/validation.yaml + FOS\UserBundle\Model\User: + properties: + plainPassword: + - NotBlank: + groups: [AcmeValidation] + - Length: + min: 6 + minMessage: fos_user.password.short + groups: [AcmeValidation] + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + +Now, update the FOSUserBundle configuration, so it uses your validation groups +instead of the original ones. + +.. _override-translations: + +Translations +------------ + +Translations are not related to bundles, but to translation domains. +For this reason, you can override any bundle translation file from the main +``translations/`` directory, as long as the new file uses the same domain. + +For example, to override the translations defined in the +``translations/AcmeUserBundle.es.yaml`` file of the AcmeUserBundle, +create a ``/translations/AcmeUserBundle.es.yaml`` file. + +.. _`the Doctrine documentation`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/inheritance-mapping.html#overrides diff --git a/bundles/prepend_extension.rst b/bundles/prepend_extension.rst new file mode 100644 index 00000000000..e4099d9f81a --- /dev/null +++ b/bundles/prepend_extension.rst @@ -0,0 +1,223 @@ +How to Simplify Configuration of Multiple Bundles +================================================= + +When building reusable and extensible applications, developers are often +faced with a choice: either create a single large bundle or multiple smaller +bundles. Creating a single bundle has the drawback that it's impossible for +users to remove unused functionality. Creating multiple +bundles has the drawback that configuration becomes more tedious and settings +often need to be repeated for various bundles. + +It is possible to remove the disadvantage of the multiple bundle approach by +enabling a single Extension to prepend the settings for any bundle. It can use +the settings defined in the ``config/*`` files to prepend settings just as if +they had been written explicitly by the user in the application configuration. + +For example, this could be used to configure the entity manager name to use in +multiple bundles. Or it can be used to enable an optional feature that depends +on another bundle being loaded as well. + +To give an Extension the power to do this, it needs to implement +:class:`Symfony\\Component\\DependencyInjection\\Extension\\PrependExtensionInterface`:: + + // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php + namespace Acme\HelloBundle\DependencyInjection; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; + use Symfony\Component\HttpKernel\DependencyInjection\Extension; + + class AcmeHelloExtension extends Extension implements PrependExtensionInterface + { + // ... + + public function prepend(ContainerBuilder $container): void + { + // ... + } + } + +Inside the :method:`Symfony\\Component\\DependencyInjection\\Extension\\PrependExtensionInterface::prepend` +method, developers have full access to the :class:`Symfony\\Component\\DependencyInjection\\ContainerBuilder` +instance just before the :method:`Symfony\\Component\\DependencyInjection\\Extension\\ExtensionInterface::load` +method is called on each of the registered bundle Extensions. In order to +prepend settings to a bundle extension developers can use the +:method:`Symfony\\Component\\DependencyInjection\\ContainerBuilder::prependExtensionConfig` +method on the :class:`Symfony\\Component\\DependencyInjection\\ContainerBuilder` +instance. As this method only prepends settings, any other settings done explicitly +inside the ``config/*`` files would override these prepended settings. + +The following example illustrates how to prepend +a configuration setting in multiple bundles as well as disable a flag in multiple bundles +in case a specific other bundle is not registered:: + + // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php + public function prepend(ContainerBuilder $container): void + { + // get all bundles + $bundles = $container->getParameter('kernel.bundles'); + // determine if AcmeGoodbyeBundle is registered + if (!isset($bundles['AcmeGoodbyeBundle'])) { + // disable AcmeGoodbyeBundle in bundles + $config = ['use_acme_goodbye' => false]; + foreach ($container->getExtensions() as $name => $extension) { + match ($name) { + // set use_acme_goodbye to false in the config of + // acme_something and acme_other + // + // note that if the user manually configured + // use_acme_goodbye to true in config/services.yaml + // then the setting would in the end be true and not false + 'acme_something', 'acme_other' => $container->prependExtensionConfig($name, $config), + default => null + }; + } + } + + // get the configuration of AcmeHelloExtension (it's a list of configuration) + $configs = $container->getExtensionConfig($this->getAlias()); + + // iterate in reverse to preserve the original order after prepending the config + foreach (array_reverse($configs) as $config) { + // check if entity_manager_name is set in the "acme_hello" configuration + if (isset($config['entity_manager_name'])) { + // prepend the acme_something settings with the entity_manager_name + $container->prependExtensionConfig('acme_something', [ + 'entity_manager_name' => $config['entity_manager_name'], + ]); + } + } + } + +The above would be the equivalent of writing the following into the +``config/packages/acme_something.yaml`` in case AcmeGoodbyeBundle is not +registered and the ``entity_manager_name`` setting for ``acme_hello`` is set to +``non_default``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/acme_something.yaml + acme_something: + # ... + use_acme_goodbye: false + entity_manager_name: non_default + + acme_other: + # ... + use_acme_goodbye: false + + .. code-block:: xml + + + + + + + non_default + + + + + + + + + .. code-block:: php + + // config/packages/acme_something.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return static function (ContainerConfigurator $container): void { + $container->extension('acme_something', [ + // ... + 'use_acme_goodbye' => false, + 'entity_manager_name' => 'non_default', + ]); + $container->extension('acme_other', [ + // ... + 'use_acme_goodbye' => false, + ]); + }; + +Prepending Extension in the Bundle Class +---------------------------------------- + +You can also prepend extension configuration directly in your +Bundle class if you extend from the :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` +class and define the :method:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle::prependExtension` +method:: + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + + class FooBundle extends AbstractBundle + { + public function prependExtension(ContainerConfigurator $containerConfigurator, ContainerBuilder $containerBuilder): void + { + // prepend + $containerBuilder->prependExtensionConfig('framework', [ + 'cache' => ['prefix_seed' => 'foo/bar'], + ]); + + // prepend config from a file + $containerConfigurator->import('../config/packages/cache.php'); + } + } + +.. note:: + + The ``prependExtension()`` method, like ``prepend()``, is called only at compile time. + +.. versionadded:: 7.1 + + Starting from Symfony 7.1, calling the :method:`Symfony\\Component\\DependencyInjection\\Loader\\Configurator\\ContainerConfigurator::import` + method inside ``prependExtension()`` will prepend the given configuration. + In previous Symfony versions, this method appended the configuration. + +Alternatively, you can use the ``prepend`` parameter of the +:method:`Symfony\\Component\\DependencyInjection\\Loader\\Configurator\\ContainerConfigurator::extension` +method:: + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + + class FooBundle extends AbstractBundle + { + public function prependExtension(ContainerConfigurator $containerConfigurator, ContainerBuilder $containerBuilder): void + { + // ... + + $containerConfigurator->extension('framework', [ + 'cache' => ['prefix_seed' => 'foo/bar'], + ], prepend: true); + + // ... + } + } + +.. versionadded:: 7.1 + + The ``prepend`` parameter of the + :method:`Symfony\\Component\\DependencyInjection\\Loader\\Configurator\\ContainerConfigurator::extension` + method was added in Symfony 7.1. + +More than one Bundle using PrependExtensionInterface +---------------------------------------------------- + +If there is more than one bundle that prepends the same extension and defines +the same key, the bundle that is registered **first** will take priority: +next bundles won't override this specific config setting. diff --git a/cache.rst b/cache.rst new file mode 100644 index 00000000000..83bb5b4cedc --- /dev/null +++ b/cache.rst @@ -0,0 +1,980 @@ +Cache +===== + +Using a cache is a great way of making your application run quicker. The Symfony cache +component ships with many adapters to different storages. Every adapter is +developed for high performance. + +The following example shows a typical usage of the cache:: + + use Symfony\Contracts\Cache\ItemInterface; + + // The callable will only be executed on a cache miss. + $value = $pool->get('my_cache_key', function (ItemInterface $item): string { + $item->expiresAfter(3600); + + // ... do some HTTP request or heavy computations + $computedValue = 'foobar'; + + return $computedValue; + }); + + echo $value; // 'foobar' + + // ... and to remove the cache key + $pool->delete('my_cache_key'); + +Symfony supports Cache Contracts and PSR-6/16 interfaces. +You can read more about these at the :doc:`component documentation `. + +.. _cache-configuration-with-frameworkbundle: + +Configuring Cache with FrameworkBundle +-------------------------------------- + +When configuring the cache component there are a few concepts you should know +of: + +**Pool** + This is a service that you will interact with. Each pool will always have + its own namespace and cache items. There is never a conflict between pools. +**Adapter** + An adapter is a *template* that you use to create pools. +**Provider** + A provider is a service that some adapters use to connect to the storage. + Redis and Memcached are examples of such adapters. If a DSN is used as the + provider then a service is automatically created. + +.. _cache-app-system: + +There are two pools that are always enabled by default. They are ``cache.app`` and +``cache.system``. The system cache is used for things like annotations, serializer, +and validation. The ``cache.app`` can be used in your code. You can configure which +adapter (template) they use by using the ``app`` and ``system`` key like: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/cache.yaml + framework: + cache: + app: cache.adapter.filesystem + system: cache.adapter.system + + .. code-block:: xml + + + + + + + + + + .. code-block:: php + + // config/packages/cache.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->cache() + ->app('cache.adapter.filesystem') + ->system('cache.adapter.system') + ; + }; + +.. tip:: + + While it is possible to reconfigure the ``system`` cache, it's recommended + to keep the default configuration applied to it by Symfony. + +The Cache component comes with a series of adapters pre-configured: + +* :doc:`cache.adapter.apcu ` +* :doc:`cache.adapter.array ` +* :doc:`cache.adapter.doctrine_dbal ` +* :doc:`cache.adapter.filesystem ` +* :doc:`cache.adapter.memcached ` +* :doc:`cache.adapter.pdo ` +* :doc:`cache.adapter.psr6 ` +* :doc:`cache.adapter.redis ` +* :ref:`cache.adapter.redis_tag_aware ` (Redis adapter optimized to work with tags) + +.. note:: + + There's also a special ``cache.adapter.system`` adapter. It's recommended to + use it for the :ref:`system cache `. This adapter uses some + logic to dynamically select the best possible storage based on your system + (either PHP files or APCu). + +Some of these adapters could be configured via shortcuts. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/cache.yaml + framework: + cache: + directory: '%kernel.cache_dir%/pools' # Only used with cache.adapter.filesystem + + default_doctrine_dbal_provider: 'doctrine.dbal.default_connection' + default_psr6_provider: 'app.my_psr6_service' + default_redis_provider: 'redis://localhost' + default_memcached_provider: 'memcached://localhost' + default_pdo_provider: 'pgsql:host=localhost' + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/cache.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->cache() + // Only used with cache.adapter.filesystem + ->directory('%kernel.cache_dir%/pools') + + ->defaultDoctrineDbalProvider('doctrine.dbal.default_connection') + ->defaultPsr6Provider('app.my_psr6_service') + ->defaultRedisProvider('redis://localhost') + ->defaultMemcachedProvider('memcached://localhost') + ->defaultPdoProvider('pgsql:host=localhost') + ; + }; + +.. versionadded:: 7.1 + + Using a DSN as the provider for the PDO adapter was introduced in Symfony 7.1. + +.. _cache-create-pools: + +Creating Custom (Namespaced) Pools +---------------------------------- + +You can also create more customized pools: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/cache.yaml + framework: + cache: + default_memcached_provider: 'memcached://localhost' + + pools: + # creates a "custom_thing.cache" service + # autowireable via "CacheInterface $customThingCache" + # uses the "app" cache configuration + custom_thing.cache: + adapter: cache.app + + # creates a "my_cache_pool" service + # autowireable via "CacheInterface $myCachePool" + my_cache_pool: + adapter: cache.adapter.filesystem + + # uses the default_memcached_provider from above + acme.cache: + adapter: cache.adapter.memcached + + # control adapter's configuration + foobar.cache: + adapter: cache.adapter.memcached + provider: 'memcached://user:password@example.com' + + # uses the "foobar.cache" pool as its backend but controls + # the lifetime and (like all pools) has a separate cache namespace + short_cache: + adapter: foobar.cache + default_lifetime: 60 + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/cache.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $cache = $framework->cache(); + $cache->defaultMemcachedProvider('memcached://localhost'); + + // creates a "custom_thing.cache" service + // autowireable via "CacheInterface $customThingCache" + // uses the "app" cache configuration + $cache->pool('custom_thing.cache') + ->adapters(['cache.app']); + + // creates a "my_cache_pool" service + // autowireable via "CacheInterface $myCachePool" + $cache->pool('my_cache_pool') + ->adapters(['cache.adapter.filesystem']); + + // uses the default_memcached_provider from above + $cache->pool('acme.cache') + ->adapters(['cache.adapter.memcached']); + + // control adapter's configuration + $cache->pool('foobar.cache') + ->adapters(['cache.adapter.memcached']) + ->provider('memcached://user:password@example.com'); + + $cache->pool('short_cache') + ->adapters(['foobar.cache']) + ->defaultLifetime(60); + }; + +Each pool manages a set of independent cache keys: keys from different pools +*never* collide, even if they share the same backend. This is achieved by prefixing +keys with a namespace that's generated by hashing the name of the pool, the name +of the cache adapter class and a :ref:`configurable seed ` +that defaults to the project directory and compiled container class. + +Each custom pool becomes a service whose service ID is the name of the pool +(e.g. ``custom_thing.cache``). An autowiring alias is also created for each pool +using the camel case version of its name - e.g. ``custom_thing.cache`` can be +injected automatically by naming the argument ``$customThingCache`` and type-hinting it +with either :class:`Symfony\\Contracts\\Cache\\CacheInterface` or +``Psr\Cache\CacheItemPoolInterface``:: + + use Symfony\Contracts\Cache\CacheInterface; + // ... + + // from a controller method + public function listProducts(CacheInterface $customThingCache): Response + { + // ... + } + + // in a service + public function __construct(private CacheInterface $customThingCache) + { + // ... + } + +.. tip:: + + If you need the namespace to be interoperable with a third-party app, + you can take control over auto-generation by setting the ``namespace`` + attribute of the ``cache.pool`` service tag. For example, you can + override the service definition of the adapter: + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + app.cache.adapter.redis: + parent: 'cache.adapter.redis' + tags: + - { name: 'cache.pool', namespace: 'my_custom_namespace' } + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $container): void { + $container->services() + // ... + + ->set('app.cache.adapter.redis') + ->parent('cache.adapter.redis') + ->tag('cache.pool', ['namespace' => 'my_custom_namespace']) + ; + }; + +Custom Provider Options +----------------------- + +Some providers have specific options that can be configured. The +:doc:`RedisAdapter ` allows you to +create providers with the options ``timeout``, ``retry_interval``. etc. To use these +options with non-default values you need to create your own ``\Redis`` provider +and use that when configuring the pool. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/cache.yaml + framework: + cache: + pools: + cache.my_redis: + adapter: cache.adapter.redis + provider: app.my_custom_redis_provider + + services: + app.my_custom_redis_provider: + class: \Redis + factory: ['Symfony\Component\Cache\Adapter\RedisAdapter', 'createConnection'] + arguments: + - 'redis://localhost' + - { retry_interval: 2, timeout: 10 } + + .. code-block:: xml + + + + + + + + + + + + + + redis://localhost + + 2 + 10 + + + + + + .. code-block:: php + + // config/packages/cache.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\Cache\Adapter\RedisAdapter; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container, FrameworkConfig $framework): void { + $framework->cache() + ->pool('cache.my_redis') + ->adapters(['cache.adapter.redis']) + ->provider('app.my_custom_redis_provider'); + + $container->register('app.my_custom_redis_provider', \Redis::class) + ->setFactory([RedisAdapter::class, 'createConnection']) + ->addArgument('redis://localhost') + ->addArgument([ + 'retry_interval' => 2, + 'timeout' => 10 + ]) + ; + }; + +Creating a Cache Chain +---------------------- + +Different cache adapters have different strengths and weaknesses. Some might be +really quick but optimized to store small items and some may be able to contain +a lot of data but are quite slow. To get the best of both worlds you may use a +chain of adapters. + +A cache chain combines several cache pools into a single one. When storing an +item in a cache chain, Symfony stores it in all pools sequentially. When +retrieving an item, Symfony tries to get it from the first pool. If it's not +found, it tries the next pools until the item is found or an exception is thrown. +Because of this behavior, it's recommended to define the adapters in the chain +in order from fastest to slowest. + +If an error happens when storing an item in a pool, Symfony stores it in the +other pools and no exception is thrown. Later, when the item is retrieved, +Symfony stores the item automatically in all the missing pools. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/cache.yaml + framework: + cache: + pools: + my_cache_pool: + default_lifetime: 31536000 # One year + adapters: + - cache.adapter.array + - cache.adapter.apcu + - {name: cache.adapter.redis, provider: 'redis://user:password@example.com'} + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/cache.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->cache() + ->pool('my_cache_pool') + ->defaultLifetime(31536000) // One year + ->adapters([ + 'cache.adapter.array', + 'cache.adapter.apcu', + ['name' => 'cache.adapter.redis', 'provider' => 'redis://user:password@example.com'], + ]) + ; + }; + +Using Cache Tags +---------------- + +In applications with many cache keys it could be useful to organize the data stored +to be able to invalidate the cache more efficiently. One way to achieve that is to +use cache tags. One or more tags could be added to the cache item. All items with +the same tag could be invalidated with one function call:: + + use Symfony\Contracts\Cache\ItemInterface; + use Symfony\Contracts\Cache\TagAwareCacheInterface; + + class SomeClass + { + // using autowiring to inject the cache pool + public function __construct( + private TagAwareCacheInterface $myCachePool, + ) { + } + + public function someMethod(): void + { + $value0 = $this->myCachePool->get('item_0', function (ItemInterface $item): string { + $item->tag(['foo', 'bar']); + + return 'debug'; + }); + + $value1 = $this->myCachePool->get('item_1', function (ItemInterface $item): string { + $item->tag('foo'); + + return 'debug'; + }); + + // Remove all cache keys tagged with "bar" + $this->myCachePool->invalidateTags(['bar']); + } + } + +The cache adapter needs to implement :class:`Symfony\\Contracts\\Cache\\TagAwareCacheInterface` +to enable this feature. This could be added by using the following configuration. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/cache.yaml + framework: + cache: + pools: + my_cache_pool: + adapter: cache.adapter.redis_tag_aware + tags: true + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // config/packages/cache.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->cache() + ->pool('my_cache_pool') + ->tags(true) + ->adapters(['cache.adapter.redis_tag_aware']) + ; + }; + +Tags are stored in the same pool by default. This is good in most scenarios. But +sometimes it might be better to store the tags in a different pool. That could be +achieved by specifying the adapter. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/cache.yaml + framework: + cache: + pools: + my_cache_pool: + adapter: cache.adapter.redis + tags: tag_pool + tag_pool: + adapter: cache.adapter.apcu + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/cache.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->cache() + ->pool('my_cache_pool') + ->tags('tag_pool') + ->adapters(['cache.adapter.redis']) + ; + + $framework->cache() + ->pool('tag_pool') + ->adapters(['cache.adapter.apcu']) + ; + }; + +.. note:: + + The interface :class:`Symfony\\Contracts\\Cache\\TagAwareCacheInterface` is + autowired to the ``cache.app`` service. + +Clearing the Cache +------------------ + +To clear the cache you can use the ``bin/console cache:pool:clear [pool]`` command. +That will remove all the entries from your storage and you will have to recalculate +all the values. You can also group your pools into "cache clearers". There are 3 cache +clearers by default: + +* ``cache.global_clearer`` +* ``cache.system_clearer`` +* ``cache.app_clearer`` + +The global clearer clears all the cache items 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 + +Clear one pool: + +.. code-block:: terminal + + $ php bin/console cache:pool:clear my_cache_pool + +Clear all custom pools: + +.. code-block:: terminal + + $ php bin/console cache:pool:clear cache.app_clearer + +Clear all cache pools: + +.. code-block:: terminal + + $ php bin/console cache:pool:clear --all + +Clear all cache pools except some: + +.. code-block:: terminal + + $ php bin/console cache:pool:clear --all --exclude=my_cache_pool --exclude=another_cache_pool + +Clear all caches everywhere: + +.. code-block:: terminal + + $ php bin/console cache:pool:clear cache.global_clearer + +Clear cache by tag(s): + +.. code-block:: terminal + + # invalidate tag1 from all taggable pools + $ php bin/console cache:pool:invalidate-tags tag1 + + # invalidate tag1 & tag2 from all taggable pools + $ php bin/console cache:pool:invalidate-tags tag1 tag2 + + # invalidate tag1 & tag2 from cache.app pool + $ php bin/console cache:pool:invalidate-tags tag1 tag2 --pool=cache.app + + # invalidate tag1 & tag2 from cache1 & cache2 pools + $ php bin/console cache:pool:invalidate-tags tag1 tag2 -p cache1 -p cache2 + +Encrypting the Cache +-------------------- + +To encrypt the cache using ``libsodium``, you can use the +:class:`Symfony\\Component\\Cache\\Marshaller\\SodiumMarshaller`. + +First, you need to generate a secure key and add it to your :doc:`secret +store ` as ``CACHE_DECRYPTION_KEY``: + +.. code-block:: terminal + + $ php -r 'echo base64_encode(sodium_crypto_box_keypair());' + +Then, register the ``SodiumMarshaller`` service using this key: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/cache.yaml + + # ... + services: + Symfony\Component\Cache\Marshaller\SodiumMarshaller: + decorates: cache.default_marshaller + arguments: + - ['%env(base64:CACHE_DECRYPTION_KEY)%'] + # use multiple keys in order to rotate them + #- ['%env(base64:CACHE_DECRYPTION_KEY)%', '%env(base64:OLD_CACHE_DECRYPTION_KEY)%'] + - '@.inner' + + .. code-block:: xml + + + + + + + + + + + env(base64:CACHE_DECRYPTION_KEY) + + + + + + + + + .. code-block:: php + + // config/packages/cache.php + use Symfony\Component\Cache\Marshaller\SodiumMarshaller; + use Symfony\Component\DependencyInjection\ChildDefinition; + use Symfony\Component\DependencyInjection\Reference; + + // ... + $container->setDefinition(SodiumMarshaller::class, new ChildDefinition('cache.default_marshaller')) + ->addArgument(['env(base64:CACHE_DECRYPTION_KEY)']) + // use multiple keys in order to rotate them + //->addArgument(['env(base64:CACHE_DECRYPTION_KEY)', 'env(base64:OLD_CACHE_DECRYPTION_KEY)']) + ->addArgument(new Reference('.inner')); + +.. danger:: + + This will encrypt the values of the cache items, but not the cache keys. Be + careful not to leak sensitive data in the keys. + +When configuring multiple keys, the first key will be used for reading and +writing, and the additional key(s) will only be used for reading. Once all +cache items encrypted with the old key have expired, you can completely remove +``OLD_CACHE_DECRYPTION_KEY``. + +Computing Cache Values Asynchronously +------------------------------------- + +The Cache component uses the `probabilistic early expiration`_ algorithm to +protect against the :ref:`cache stampede ` problem. +This means that some cache items are elected for early-expiration while they are +still fresh. + +By default, expired cache items are computed synchronously. However, you can +compute them asynchronously by delegating the value computation to a background +worker using the :doc:`Messenger component `. In this case, +when an item is queried, its cached value is immediately returned and a +:class:`Symfony\\Component\\Cache\\Messenger\\EarlyExpirationMessage` is +dispatched through a Messenger bus. + +When this message is handled by a message consumer, the refreshed cache value is +computed asynchronously. The next time the item is queried, the refreshed value +will be fresh and returned. + +First, create a service that will compute the item's value:: + + // src/Cache/CacheComputation.php + namespace App\Cache; + + use Symfony\Contracts\Cache\ItemInterface; + + class CacheComputation + { + public function compute(ItemInterface $item): string + { + $item->expiresAfter(5); + + // this is just a random example; here you must do your own calculation + return sprintf('#%06X', mt_rand(0, 0xFFFFFF)); + } + } + +This cache value will be requested from a controller, another service, etc. +In the following example, the value is requested from a controller:: + + // src/Controller/CacheController.php + namespace App\Controller; + + use App\Cache\CacheComputation; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Attribute\Route; + use Symfony\Contracts\Cache\CacheInterface; + use Symfony\Contracts\Cache\ItemInterface; + + class CacheController extends AbstractController + { + #[Route('/cache', name: 'cache')] + public function index(CacheInterface $asyncCache): Response + { + // pass to the cache the service method that refreshes the item + $cachedValue = $asyncCache->get('my_value', [CacheComputation::class, 'compute']) + + // ... + } + } + +Finally, configure a new cache pool (e.g. called ``async.cache``) that will use +a message bus to compute values in a worker: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + cache: + pools: + async.cache: + early_expiration_message_bus: messenger.default_bus + + messenger: + transports: + async_bus: '%env(MESSENGER_TRANSPORT_DSN)%' + routing: + 'Symfony\Component\Cache\Messenger\EarlyExpirationMessage': async_bus + + .. code-block:: xml + + + + + + + + + + + %env(MESSENGER_TRANSPORT_DSN)% + + + + + + + + .. code-block:: php + + // config/framework/framework.php + use function Symfony\Component\DependencyInjection\Loader\Configurator\env; + use Symfony\Component\Cache\Messenger\EarlyExpirationMessage; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->cache() + ->pool('async.cache') + ->earlyExpirationMessageBus('messenger.default_bus'); + + $framework->messenger() + ->transport('async_bus') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + ->routing(EarlyExpirationMessage::class) + ->senders(['async_bus']); + }; + +You can now start the consumer: + +.. code-block:: terminal + + $ php bin/console messenger:consume async_bus + +That's it! Now, whenever an item is queried from this cache pool, its cached +value will be returned immediately. If it is elected for early-expiration, a +message will be sent through to bus to schedule a background computation to refresh +the value. + +.. _`probabilistic early expiration`: https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration diff --git a/components/asset.rst b/components/asset.rst new file mode 100644 index 00000000000..d6d3f485859 --- /dev/null +++ b/components/asset.rst @@ -0,0 +1,432 @@ +The Asset Component +=================== + + The Asset component manages URL generation and versioning of web assets such + as CSS stylesheets, JavaScript files and image files. + +In the past, it was common for web applications to hard-code the URLs of web assets. +For example: + +.. code-block:: html + + + + + + logo + +This practice is no longer recommended unless the web application is extremely +simple. Hardcoding URLs can be a disadvantage because: + +* **Templates get verbose**: you have to write the full path for each + asset. When using the Asset component, you can group assets in packages to + avoid repeating the common part of their path; +* **Versioning is difficult**: it has to be custom managed for each + application. Adding a version (e.g. ``main.css?v=5``) to the asset URLs + is essential for some applications because it allows you to control how + the assets are cached. The Asset component allows you to define different + versioning strategies for each package; +* **Moving assets' location** is cumbersome and error-prone: it requires you to + carefully update the URLs of all assets included in all templates. The Asset + component allows to move assets effortlessly just by changing the base path + value associated with the package of assets; +* **It's nearly impossible to use multiple CDNs**: this technique requires + you to change the URL of the asset randomly for each request. The Asset component + provides out-of-the-box support for any number of multiple CDNs, both regular + (``http://``) and secure (``https://``). + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/asset + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +.. _asset-packages: + +Asset Packages +~~~~~~~~~~~~~~ + +The Asset component manages assets through packages. A package groups all the +assets which share the same properties: versioning strategy, base path, CDN hosts, +etc. In the following basic example, a package is created to manage assets without +any versioning:: + + use Symfony\Component\Asset\Package; + use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy; + + $package = new Package(new EmptyVersionStrategy()); + + // Absolute path + echo $package->getUrl('/image.png'); + // result: /image.png + + // Relative path + echo $package->getUrl('image.png'); + // result: image.png + +Packages implement :class:`Symfony\\Component\\Asset\\PackageInterface`, +which defines the following two methods: + +:method:`Symfony\\Component\\Asset\\PackageInterface::getVersion` + Returns the asset version for an asset. + +:method:`Symfony\\Component\\Asset\\PackageInterface::getUrl` + Returns an absolute or root-relative public path. + +With a package, you can: + +A) :ref:`version the assets `; +B) set a :ref:`common base path ` (e.g. ``/css``) + for the assets; +C) :ref:`configure a CDN ` for the assets + +.. _component-assets-versioning: + +Versioned Assets +~~~~~~~~~~~~~~~~ + +One of the main features of the Asset component is the ability to manage +the versioning of the application's assets. Asset versions are commonly used +to control how these assets are cached. + +Instead of relying on a simple version mechanism, the Asset component allows +you to define advanced versioning strategies via PHP classes. The two built-in +strategies are the :class:`Symfony\\Component\\Asset\\VersionStrategy\\EmptyVersionStrategy`, +which doesn't add any version to the asset and :class:`Symfony\\Component\\Asset\\VersionStrategy\\StaticVersionStrategy`, +which allows you to set the version with a format string. + +In this example, the ``StaticVersionStrategy`` is used to append the ``v1`` +suffix to any asset path:: + + use Symfony\Component\Asset\Package; + use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; + + $package = new Package(new StaticVersionStrategy('v1')); + + // Absolute path + echo $package->getUrl('/image.png'); + // result: /image.png?v1 + + // Relative path + echo $package->getUrl('image.png'); + // result: image.png?v1 + +In case you want to modify the version format, pass a ``sprintf``-compatible +format string as the second argument of the ``StaticVersionStrategy`` +constructor:: + + // puts the 'version' word before the version value + $package = new Package(new StaticVersionStrategy('v1', '%s?version=%s')); + + echo $package->getUrl('/image.png'); + // result: /image.png?version=v1 + + // puts the asset version before its path + $package = new Package(new StaticVersionStrategy('v1', '%2$s/%1$s')); + + echo $package->getUrl('/image.png'); + // result: /v1/image.png + + echo $package->getUrl('image.png'); + // result: v1/image.png + +JSON File Manifest +.................. + +A popular strategy to manage asset versioning, which is used by tools such as +`Webpack`_, is to generate a JSON file mapping all source file names to their +corresponding output file: + +.. code-block:: json + + { + "css/app.css": "build/css/app.b916426ea1d10021f3f17ce8031f93c2.css", + "js/app.js": "build/js/app.13630905267b809161e71d0f8a0c017b.js", + "...": "..." + } + +In those cases, use the +:class:`Symfony\\Component\\Asset\\VersionStrategy\\JsonManifestVersionStrategy`:: + + use Symfony\Component\Asset\Package; + use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy; + + // assumes the JSON file above is called "rev-manifest.json" + $package = new Package(new JsonManifestVersionStrategy(__DIR__.'/rev-manifest.json')); + + echo $package->getUrl('css/app.css'); + // result: build/css/app.b916426ea1d10021f3f17ce8031f93c2.css + +If you request an asset that is *not found* in the ``rev-manifest.json`` file, +the original - *unmodified* - asset path will be returned. The ``$strictMode`` +argument helps debug issues because it throws an exception when the asset is not +listed in the manifest:: + + use Symfony\Component\Asset\Package; + use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy; + + // The value of $strictMode can be specific per environment "true" for debugging and "false" for stability. + $strictMode = true; + // assumes the JSON file above is called "rev-manifest.json" + $package = new Package(new JsonManifestVersionStrategy(__DIR__.'/rev-manifest.json', null, $strictMode)); + + echo $package->getUrl('not-found.css'); + // error: + +If your JSON file is not on your local filesystem but is accessible over HTTP, +use the :class:`Symfony\\Component\\Asset\\VersionStrategy\\JsonManifestVersionStrategy` +with the :doc:`HttpClient component `:: + + use Symfony\Component\Asset\Package; + use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy; + use Symfony\Component\HttpClient\HttpClient; + + $httpClient = HttpClient::create(); + $manifestUrl = 'https://cdn.example.com/rev-manifest.json'; + $package = new Package(new JsonManifestVersionStrategy($manifestUrl, $httpClient)); + +Custom Version Strategies +......................... + +Use the :class:`Symfony\\Component\\Asset\\VersionStrategy\\VersionStrategyInterface` +to define your own versioning strategy. For example, your application may need +to append the current date to all its web assets in order to bust the cache +every day:: + + use Symfony\Component\Asset\VersionStrategy\VersionStrategyInterface; + + class DateVersionStrategy implements VersionStrategyInterface + { + private string $version; + + public function __construct() + { + $this->version = date('Ymd'); + } + + public function getVersion(string $path): string + { + return $this->version; + } + + public function applyVersion(string $path): string + { + return sprintf('%s?v=%s', $path, $this->getVersion($path)); + } + } + +.. _component-assets-path-package: + +Grouped Assets +~~~~~~~~~~~~~~ + +Often, many assets live under a common path (e.g. ``/static/images``). If +that's your case, replace the default :class:`Symfony\\Component\\Asset\\Package` +class with :class:`Symfony\\Component\\Asset\\PathPackage` to avoid repeating +that path over and over again:: + + use Symfony\Component\Asset\PathPackage; + // ... + + $pathPackage = new PathPackage('/static/images', new StaticVersionStrategy('v1')); + + echo $pathPackage->getUrl('logo.png'); + // result: /static/images/logo.png?v1 + + // Base path is ignored when using absolute paths + echo $pathPackage->getUrl('/logo.png'); + // result: /logo.png?v1 + +Request Context Aware Assets +............................ + +If you are also using the :doc:`HttpFoundation ` +component in your project (for instance, in a Symfony application), the ``PathPackage`` +class can take into account the context of the current request:: + + use Symfony\Component\Asset\Context\RequestStackContext; + use Symfony\Component\Asset\PathPackage; + // ... + + $pathPackage = new PathPackage( + '/static/images', + new StaticVersionStrategy('v1'), + new RequestStackContext($requestStack) + ); + + echo $pathPackage->getUrl('logo.png'); + // result: /somewhere/static/images/logo.png?v1 + + // Both "base path" and "base url" are ignored when using absolute path for asset + echo $pathPackage->getUrl('/logo.png'); + // result: /logo.png?v1 + +Now that the request context is set, the ``PathPackage`` will prepend the +current request base URL. So, for example, if your entire site is hosted under +the ``/somewhere`` directory of your web server root directory and the configured +base path is ``/static/images``, all paths will be prefixed with +``/somewhere/static/images``. + +.. _component-assets-cdn: + +Absolute Assets and CDNs +~~~~~~~~~~~~~~~~~~~~~~~~ + +Applications that host their assets on different domains and CDNs (*Content +Delivery Networks*) should use the :class:`Symfony\\Component\\Asset\\UrlPackage` +class to generate absolute URLs for their assets:: + + use Symfony\Component\Asset\UrlPackage; + // ... + + $urlPackage = new UrlPackage( + 'https://static.example.com/images/', + new StaticVersionStrategy('v1') + ); + + echo $urlPackage->getUrl('/logo.png'); + // result: https://static.example.com/images/logo.png?v1 + +You can also pass a schema-agnostic URL:: + + use Symfony\Component\Asset\UrlPackage; + // ... + + $urlPackage = new UrlPackage( + '//static.example.com/images/', + new StaticVersionStrategy('v1') + ); + + echo $urlPackage->getUrl('/logo.png'); + // result: //static.example.com/images/logo.png?v1 + +This is useful because assets will automatically be requested via HTTPS if +a visitor is viewing your site in https. If you want to use this, make sure +that your CDN host supports HTTPS. + +In case you serve assets from more than one domain to improve application +performance, pass an array of URLs as the first argument to the ``UrlPackage`` +constructor:: + + use Symfony\Component\Asset\UrlPackage; + // ... + + $urls = [ + 'https://static1.example.com/images/', + 'https://static2.example.com/images/', + ]; + $urlPackage = new UrlPackage($urls, new StaticVersionStrategy('v1')); + + echo $urlPackage->getUrl('/logo.png'); + // result: https://static1.example.com/images/logo.png?v1 + echo $urlPackage->getUrl('/icon.png'); + // result: https://static2.example.com/images/icon.png?v1 + +For each asset, one of the URLs will be randomly used. But, the selection +is deterministic, meaning that each asset will always be served by the same +domain. This behavior simplifies the management of HTTP cache. + +Request Context Aware Assets +............................ + +Similarly to application-relative assets, absolute assets can also take into +account the context of the current request. In this case, only the request +scheme is considered, in order to select the appropriate base URL (HTTPs or +protocol-relative URLs for HTTPs requests, any base URL for HTTP requests):: + + use Symfony\Component\Asset\Context\RequestStackContext; + use Symfony\Component\Asset\UrlPackage; + // ... + + $urlPackage = new UrlPackage( + ['http://example.com/', 'https://example.com/'], + new StaticVersionStrategy('v1'), + new RequestStackContext($requestStack) + ); + + echo $urlPackage->getUrl('/logo.png'); + // assuming the RequestStackContext says that we are on a secure host + // result: https://example.com/logo.png?v1 + +Named Packages +~~~~~~~~~~~~~~ + +Applications that manage lots of different assets may need to group them in +packages with the same versioning strategy and base path. The Asset component +includes a :class:`Symfony\\Component\\Asset\\Packages` class to simplify +management of several packages. + +In the following example, all packages use the same versioning strategy, but +they all have different base paths:: + + use Symfony\Component\Asset\Package; + use Symfony\Component\Asset\Packages; + use Symfony\Component\Asset\PathPackage; + use Symfony\Component\Asset\UrlPackage; + // ... + + $versionStrategy = new StaticVersionStrategy('v1'); + + $defaultPackage = new Package($versionStrategy); + + $namedPackages = [ + 'img' => new UrlPackage('https://img.example.com/', $versionStrategy), + 'doc' => new PathPackage('/somewhere/deep/for/documents', $versionStrategy), + ]; + + $packages = new Packages($defaultPackage, $namedPackages); + +The ``Packages`` class allows to define a default package, which will be applied +to assets that don't define the name of the package to use. In addition, this +application defines a package named ``img`` to serve images from an external +domain and a ``doc`` package to avoid repeating long paths when linking to a +document inside a template:: + + echo $packages->getUrl('/main.css'); + // result: /main.css?v1 + + echo $packages->getUrl('/logo.png', 'img'); + // result: https://img.example.com/logo.png?v1 + + echo $packages->getUrl('resume.pdf', 'doc'); + // result: /somewhere/deep/for/documents/resume.pdf?v1 + +Local Files and Other Protocols +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to HTTP this component supports other protocols (such as ``file://`` +and ``ftp://``). This allows for example to serve local files in order to +improve performance:: + + use Symfony\Component\Asset\UrlPackage; + // ... + + $localPackage = new UrlPackage( + 'file:///path/to/images/', + new EmptyVersionStrategy() + ); + + $ftpPackage = new UrlPackage( + 'ftp://example.com/images/', + new EmptyVersionStrategy() + ); + + echo $localPackage->getUrl('/logo.png'); + // result: file:///path/to/images/logo.png + + echo $ftpPackage->getUrl('/logo.png'); + // result: ftp://example.com/images/logo.png + +Learn more +---------- + +* :doc:`How to manage CSS and JavaScript assets in Symfony applications ` +* :doc:`WebLink component ` to preload assets using HTTP/2. + +.. _`Webpack`: https://webpack.js.org/ diff --git a/components/browser_kit.rst b/components/browser_kit.rst new file mode 100644 index 00000000000..bcb8f7b3c8e --- /dev/null +++ b/components/browser_kit.rst @@ -0,0 +1,409 @@ +The BrowserKit Component +======================== + + The BrowserKit component simulates the behavior of a web browser, allowing + you to make requests, click on links and submit forms programmatically. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/browser-kit + +.. include:: /components/require_autoload.rst.inc + +Basic Usage +----------- + +.. seealso:: + + This article explains how to use the BrowserKit features as an independent + component in any PHP application. Read the :ref:`Symfony Functional Tests ` + article to learn about how to use it in Symfony applications. + +Creating a Client +~~~~~~~~~~~~~~~~~ + +The component only provides an abstract client and does not provide any backend +ready to use for the HTTP layer. To create your own client, you must extend the +``AbstractBrowser`` class and implement the +:method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::doRequest` method. +This method accepts a request and should return a response:: + + namespace Acme; + + use Symfony\Component\BrowserKit\AbstractBrowser; + use Symfony\Component\BrowserKit\Response; + + class Client extends AbstractBrowser + { + protected function doRequest($request): Response + { + // ... convert request into a response + + return new Response($content, $status, $headers); + } + } + +For a simple implementation of a browser based on the HTTP layer, have a look +at the :class:`Symfony\\Component\\BrowserKit\\HttpBrowser` provided by +:ref:`this component `. For an implementation based +on ``HttpKernelInterface``, have a look at the :class:`Symfony\\Component\\HttpKernel\\HttpClientKernel` +provided by the :doc:`HttpKernel component `. + +Making Requests +~~~~~~~~~~~~~~~ + +Use the :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::request` method to +make HTTP requests. The first two arguments are the HTTP method and the requested +URL:: + + use Acme\Client; + + $client = new Client(); + $crawler = $client->request('GET', '/'); + +The value returned by the ``request()`` method is an instance of the +:class:`Symfony\\Component\\DomCrawler\\Crawler` class, provided by the +:doc:`DomCrawler component `, which allows accessing +and traversing HTML elements programmatically. + +The :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::jsonRequest` method, +which defines the same arguments as the ``request()`` method, is a shortcut to +convert the request parameters into a JSON string and set the needed HTTP headers:: + + use Acme\Client; + + $client = new Client(); + // this encodes parameters as JSON and sets the required CONTENT_TYPE and HTTP_ACCEPT headers + $crawler = $client->jsonRequest('GET', '/', ['some_parameter' => 'some_value']); + +The :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::xmlHttpRequest` method, +which defines the same arguments as the ``request()`` method, is a shortcut to +make AJAX requests:: + + use Acme\Client; + + $client = new Client(); + // the required HTTP_X_REQUESTED_WITH header is added automatically + $crawler = $client->xmlHttpRequest('GET', '/'); + +Clicking Links +~~~~~~~~~~~~~~ + +The ``AbstractBrowser`` is capable of simulating link clicks. Pass the text +content of the link and the client will perform the needed HTTP GET request to +simulate the link click:: + + use Acme\Client; + + $client = new Client(); + $client->request('GET', '/product/123'); + + $crawler = $client->clickLink('Go elsewhere...'); + +If you need the :class:`Symfony\\Component\\DomCrawler\\Link` object that +provides access to the link properties (e.g. ``$link->getMethod()``, +``$link->getUri()``), use this other method:: + + // ... + $crawler = $client->request('GET', '/product/123'); + $link = $crawler->selectLink('Go elsewhere...')->link(); + $client->click($link); + +The :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::click` and +:method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::clickLink` methods +can take an optional ``serverParameters`` argument. This +parameter allows to send additional information like headers when clicking +on a link:: + + use Acme\Client; + + $client = new Client(); + $client->request('GET', '/product/123'); + + // works both with `click()`... + $link = $crawler->selectLink('Go elsewhere...')->link(); + $client->click($link, ['X-Custom-Header' => 'Some data']); + + // ... and `clickLink()` + $crawler = $client->clickLink('Go elsewhere...', ['X-Custom-Header' => 'Some data']); + +Submitting Forms +~~~~~~~~~~~~~~~~ + +The ``AbstractBrowser`` is also capable of submitting forms. First, select the +form using any of its buttons and then override any of its properties (method, +field values, etc.) before submitting it:: + + use Acme\Client; + + $client = new Client(); + $crawler = $client->request('GET', 'https://github.com/login'); + + // find the form with the 'Log in' button and submit it + // 'Log in' can be the text content, id, value or name of a + + // you can get button by its label + $form = $crawler->selectButton('My super button')->form(); + + // or by button id (#my-super-button) if the button doesn't have a label + $form = $crawler->selectButton('my-super-button')->form(); + + // or you can filter the whole form, for example a form has a class attribute:
+ $crawler->filter('.form-vertical')->form(); // or "fill" the form fields with data - $form = $crawler->selectButton('validate')->form(array( + $form = $crawler->selectButton('my-super-button')->form([ 'name' => 'Ryan', - )); + ]); The :class:`Symfony\\Component\\DomCrawler\\Form` object has lots of very useful methods for working with forms:: $uri = $form->getUri(); - $method = $form->getMethod(); + $name = $form->getName(); The :method:`Symfony\\Component\\DomCrawler\\Form::getUri` method does more than just return the ``action`` attribute of the form. If the form method is GET, then it mimics the browser's behavior and returns the ``action`` attribute followed by a query string of all of the form's values. +.. note:: + + The optional ``formaction`` and ``formmethod`` button attributes are + supported. The ``getUri()`` and ``getMethod()`` methods take into account + those attributes to always return the right action and method depending on + the button used to get the form. + You can virtually set and get values on the form:: - // set values on the form internally - $form->setValues(array( + // sets values on the form internally + $form->setValues([ 'registration[username]' => 'symfonyfan', 'registration[terms]' => 1, - )); + ]); - // get back an array of values - in the "flat" array like above + // gets back an array of values - in the "flat" array like above $values = $form->getValues(); // returns the values like PHP would see them, // where "registration" is its own array $values = $form->getPhpValues(); -To work with multi-dimensional fields:: +To work with multi-dimensional fields: + +.. code-block:: html - - - + + + + + +
Pass an array of values:: - // Set a single field - $form->setValues(array('multi' => array('value'))); + // sets a single field + $form->setValues(['multi' => ['value']]); - // Set multiple fields at once - $form->setValues(array('multi' => array( + // sets multiple fields at once + $form->setValues(['multi' => [ 1 => 'value', - 'dimensional' => 'an other value' - ))); + '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, @@ -295,19 +565,22 @@ and uploading files:: $form['registration[username]']->setValue('symfonyfan'); - // check or uncheck a checkbox + // checks or unchecks a checkbox $form['registration[terms]']->tick(); $form['registration[terms]']->untick(); - // select an option + // selects an option $form['registration[birthday][year]']->select(1984); - // select many options from a "multiple" select or checkboxes - $form['registration[interests]']->select(array('symfony', 'cookies')); + // selects many options from a "multiple" select + $form['registration[interests]']->select(['symfony', 'cookies']); - // even fake a file upload + // fakes a file upload $form['registration[photo]']->upload('/path/to/lucas.jpg'); +Using the Form Data +................... + What's the point of doing all of this? If you're testing internally, you can grab the information off of your form as if it had just been submitted by using the PHP values:: @@ -325,23 +598,74 @@ of the information you need to create a POST request for the form:: // now use some HTTP client and post using this information -One great example of an integrated system that uses all of this is `Goutte`_. -Goutte understands the Symfony Crawler object and can use it to submit forms +One great example of an integrated system that uses all of this is +the :class:`Symfony\\Component\\BrowserKit\\HttpBrowser` provided by +the :doc:`BrowserKit component `. +It understands the Symfony Crawler object and can use it to submit forms directly:: - use Goutte\Client; + use Symfony\Component\BrowserKit\HttpBrowser; + use Symfony\Component\HttpClient\HttpClient; - // make a real request to an external site - $client = new Client(); - $crawler = $client->request('GET', 'https://github.com/login'); + // makes a real request to an external site + $browser = new HttpBrowser(HttpClient::create()); + $crawler = $browser->request('GET', 'https://github.com/login'); // select the form and fill in some values - $form = $crawler->selectButton('Log in')->form(); + $form = $crawler->selectButton('Sign in')->form(); $form['login'] = 'symfonyfan'; $form['password'] = 'anypass'; - // submit that form - $crawler = $client->submit($form); + // submits the given form + $crawler = $browser->submit($form); + +.. _components-dom-crawler-invalid: + +Selecting Invalid Choice Values +............................... + +By default, choice fields (select, radio) have internal validation activated +to prevent you from setting invalid values. If you want to be able to set +invalid values, you can use the ``disableValidation()`` method on either +the whole form or specific field(s):: + + // disables validation for a specific field + $form['country']->disableValidation()->select('Invalid value'); + + // disables validation for the whole form + $form->disableValidation(); + $form['country']->select('Invalid value'); + +Resolving a URI +~~~~~~~~~~~~~~~ + +The :class:`Symfony\\Component\\DomCrawler\\UriResolver` class takes a URI +(relative, absolute, fragment, etc.) and turns it into an absolute URI against +another given base URI:: + + use Symfony\Component\DomCrawler\UriResolver; + + UriResolver::resolve('/foo', 'http://localhost/bar/foo/'); // http://localhost/foo + UriResolver::resolve('?a=b', 'http://localhost/bar#foo'); // http://localhost/bar?a=b + UriResolver::resolve('../../', 'http://localhost/'); // http://localhost/ + +Using a HTML5 Parser +~~~~~~~~~~~~~~~~~~~~ + +If you need the :class:`Symfony\\Component\\DomCrawler\\Crawler` to use an HTML5 +parser, set its ``useHtml5Parser`` constructor argument to ``true``:: + + use Symfony\Component\DomCrawler\Crawler; + + $crawler = new Crawler(null, $uri, useHtml5Parser: true); + +By doing so, the crawler will use the HTML5 parser provided by the `masterminds/html5`_ +library to parse the documents. + +Learn more +---------- + +* :doc:`/testing` +* :doc:`/components/css_selector` -.. _`Goutte`: https://github.com/fabpot/goutte -.. _Packagist: https://packagist.org/packages/symfony/dom-crawler +.. _`masterminds/html5`: https://packagist.org/packages/masterminds/html5 diff --git a/components/event_dispatcher.rst b/components/event_dispatcher.rst new file mode 100644 index 00000000000..8cd676dd5fe --- /dev/null +++ b/components/event_dispatcher.rst @@ -0,0 +1,486 @@ +The EventDispatcher Component +============================= + + The EventDispatcher component provides tools that allow your application + components to communicate with each other by dispatching events and + listening to them. + +Introduction +------------ + +Object-oriented code has gone a long way to ensuring code extensibility. +By creating classes that have well-defined responsibilities, your code becomes +more flexible and a developer can extend them with subclasses to modify +their behaviors. But if they want to share the changes with other developers +who have also made their own subclasses, code inheritance is no longer the +answer. + +Consider the real-world example where you want to provide a plugin system +for your project. A plugin should be able to add methods, or do something +before or after a method is executed, without interfering with other plugins. +This is not an easy problem to solve with single inheritance, and even if +multiple inheritance was possible with PHP, it comes with its own drawbacks. + +The Symfony EventDispatcher component implements the `Mediator`_ and `Observer`_ +design patterns to make all these things possible and to make your projects +truly extensible. + +Take an example from :doc:`the HttpKernel component `. +Once a ``Response`` object has been created, it may be useful to allow other +elements in the system to modify it (e.g. add some cache headers) before +it's actually used. To make this possible, the Symfony kernel dispatches an +event - ``kernel.response``. Here's how it works: + +* A *listener* (PHP object) tells a central *dispatcher* object that it + wants to listen to the ``kernel.response`` event; + +* At some point, the Symfony kernel tells the *dispatcher* object to dispatch + the ``kernel.response`` event, passing with it an ``Event`` object that + has access to the ``Response`` object; + +* The dispatcher notifies (i.e. calls a method on) all listeners of the + ``kernel.response`` event, allowing each of them to make modifications + to the ``Response`` object. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/event-dispatcher + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +.. seealso:: + + This article explains how to use the EventDispatcher features as an + independent component in any PHP application. Read the :doc:`/event_dispatcher` + article to learn about how to use it in Symfony applications. + +Events +~~~~~~ + +When an event is dispatched, it's identified by a unique name (e.g. +``kernel.response``), which any number of listeners might be listening to. +An :class:`Symfony\\Contracts\\EventDispatcher\\Event` instance is also +created and passed to all of the listeners. As you'll see later, the ``Event`` +object itself often contains data about the event being dispatched. + +Event Names and Event Objects +............................. + +When the dispatcher notifies listeners, it passes an actual ``Event`` object +to those listeners. The base ``Event`` class contains a method for stopping +:ref:`event propagation `, but not much +else. + +.. seealso:: + + Read ":doc:`/components/event_dispatcher/generic_event`" for more + information about this base event object. + +Often times, data about a specific event needs to be passed along with the +``Event`` object so that the listeners have the needed information. In such +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\\ResponseEvent`, which +contains methods to get and even replace the ``Response`` object. + +The Dispatcher +~~~~~~~~~~~~~~ + +The dispatcher is the central object of the event dispatcher system. In +general, a single dispatcher is created, which maintains a registry of +listeners. When an event is dispatched via the dispatcher, it notifies all +listeners registered with that event:: + + use Symfony\Component\EventDispatcher\EventDispatcher; + + $dispatcher = new EventDispatcher(); + +Connecting Listeners +~~~~~~~~~~~~~~~~~~~~ + +To take advantage of an existing event, you need to connect a listener to +the dispatcher so that it can be notified when the event is dispatched. +A call to the dispatcher's ``addListener()`` method associates any valid +PHP callable to an event:: + + $listener = new AcmeListener(); + $dispatcher->addListener('acme.foo.action', [$listener, 'onFooAction']); + +The ``addListener()`` method takes up to three arguments: + +#. The event name (string) that this listener wants to listen to; +#. A PHP callable that will be executed when the specified event is dispatched; +#. An optional priority, defined as a positive or negative integer (defaults to + ``0``). The higher the number, the earlier the listener is called. If two + listeners have the same priority, they are executed in the order that they + were added to the dispatcher. + +.. note:: + + A `PHP callable`_ is a PHP variable that can be used by the + ``call_user_func()`` function and returns ``true`` when passed to the + ``is_callable()`` function. It can be a ``\Closure`` instance, an object + implementing an ``__invoke()`` method (which is what closures are in fact), + a string representing a function or an array representing an object + method or a class method. + + So far, you've seen how PHP objects can be registered as listeners. + You can also register PHP `Closures`_ as event listeners:: + + use Symfony\Contracts\EventDispatcher\Event; + + $dispatcher->addListener('acme.foo.action', function (Event $event): void { + // will be executed when the acme.foo.action event is dispatched + }); + +Once a listener is registered with the dispatcher, it waits until the event +is notified. In the above example, when the ``acme.foo.action`` event is dispatched, +the dispatcher calls the ``AcmeListener::onFooAction()`` method and passes +the ``Event`` object as the single argument:: + + use Symfony\Contracts\EventDispatcher\Event; + + class AcmeListener + { + // ... + + public function onFooAction(Event $event): void + { + // ... do something + } + } + +The ``$event`` argument is the event object that was passed when dispatching the +event. In many cases, a special event subclass is passed with extra +information. You can check the documentation or implementation of each event to +determine which instance is passed. + +.. sidebar:: Registering Event Listeners and Subscribers in the Service Container + + Registering service definitions and tagging them with the + ``kernel.event_listener`` and ``kernel.event_subscriber`` tags is not enough + to enable the event listeners and event subscribers. You must also register + a compiler pass called ``RegisterListenersPass()`` in the container builder:: + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; + use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; + use Symfony\Component\EventDispatcher\EventDispatcher; + + $container = new ContainerBuilder(new ParameterBag()); + // register the compiler pass that handles the 'kernel.event_listener' + // and 'kernel.event_subscriber' service tags + $container->addCompilerPass(new RegisterListenersPass()); + + $container->register('event_dispatcher', EventDispatcher::class); + + // registers an event listener + $container->register('listener_service_id', \AcmeListener::class) + ->addTag('kernel.event_listener', [ + 'event' => 'acme.foo.action', + 'method' => 'onFooAction', + ]); + + // registers an event subscriber + $container->register('subscriber_service_id', \AcmeSubscriber::class) + ->addTag('kernel.event_subscriber'); + + ``RegisterListenersPass`` resolves aliased class names which for instance + allows to refer to an event via the fully qualified class name (FQCN) of the + event class. The pass will read the alias mapping from a dedicated container + parameter. This parameter can be extended by registering another compiler pass, + ``AddEventAliasesPass``:: + + use Symfony\Component\DependencyInjection\Compiler\PassConfig; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; + use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass; + use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; + use Symfony\Component\EventDispatcher\EventDispatcher; + + $container = new ContainerBuilder(new ParameterBag()); + $container->addCompilerPass(new AddEventAliasesPass([ + \AcmeFooActionEvent::class => 'acme.foo.action', + ])); + $container->addCompilerPass(new RegisterListenersPass(), PassConfig::TYPE_BEFORE_REMOVING); + + $container->register('event_dispatcher', EventDispatcher::class); + + // registers an event listener + $container->register('listener_service_id', \AcmeListener::class) + ->addTag('kernel.event_listener', [ + // will be translated to 'acme.foo.action' by RegisterListenersPass. + 'event' => \AcmeFooActionEvent::class, + 'method' => 'onFooAction', + ]); + + .. note:: + + Note that ``AddEventAliasesPass`` has to be processed before ``RegisterListenersPass``. + + The listeners pass assumes that the event dispatcher's service + id is ``event_dispatcher``, that event listeners are tagged with the + ``kernel.event_listener`` tag, that event subscribers are tagged + with the ``kernel.event_subscriber`` tag and that the alias mapping is + stored as parameter ``event_dispatcher.event_aliases``. + +.. _event_dispatcher-closures-as-listeners: + +Creating and Dispatching an Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to registering listeners with existing events, you can create +and dispatch your own events. This is useful when creating third-party +libraries and also when you want to keep different components of your own +system flexible and decoupled. + +.. _creating-an-event-object: + +Creating an Event Class +....................... + +Suppose you want to create a new event that is dispatched +each time a customer orders a product with your application. When dispatching +this event, you'll pass a custom event instance that has access to the placed +order. Start by creating this custom event class and documenting it:: + + namespace Acme\Store\Event; + + use Acme\Store\Order; + use Symfony\Contracts\EventDispatcher\Event; + + /** + * This event is dispatched each time an order + * is placed in the system. + */ + final class OrderPlacedEvent extends Event + { + public function __construct(private Order $order) {} + + public function getOrder(): Order + { + return $this->order; + } + } + +Each listener now has access to the order via the ``getOrder()`` method. + +Dispatch the Event +.................. + +The :method:`Symfony\\Component\\EventDispatcher\\EventDispatcher::dispatch` +method notifies all listeners of the given event. It takes two arguments: +the ``Event`` instance to pass to each listener of that event and the name +of the event to dispatch:: + + use Acme\Store\Event\OrderPlacedEvent; + use Acme\Store\Order; + + // the order is somehow created or retrieved + $order = new Order(); + // ... + + // creates the OrderPlacedEvent and dispatches it + $event = new OrderPlacedEvent($order); + $dispatcher->dispatch($event); + +Notice that the special ``OrderPlacedEvent`` object is created and passed to +the ``dispatch()`` method. Now, any listener to the ``OrderPlacedEvent::class`` +event will receive the ``OrderPlacedEvent``. + +.. note:: + + If you don't need to pass any additional data to the event listeners, you + can also use the default + :class:`Symfony\\Contracts\\EventDispatcher\\Event` class. In such case, + you can document the event and its name in a generic ``StoreEvents`` class, + similar to the :class:`Symfony\\Component\\HttpKernel\\KernelEvents` + class:: + + namespace App\Event; + + class StoreEvents { + + /** + * @Event("Symfony\Contracts\EventDispatcher\Event") + */ + public const ORDER_PLACED = 'order.placed'; + } + + And use the :class:`Symfony\\Contracts\\EventDispatcher\\Event` class to + dispatch the event:: + + use Symfony\Contracts\EventDispatcher\Event; + + $this->eventDispatcher->dispatch(new Event(), StoreEvents::ORDER_PLACED); + +.. _event_dispatcher-using-event-subscribers: + +Using Event Subscribers +~~~~~~~~~~~~~~~~~~~~~~~ + +The most common way to listen to an event is to register an *event listener* +with the dispatcher. This listener can listen to one or more events and +is notified each time those events are dispatched. + +Another way to listen to events is via an *event subscriber*. An event +subscriber is a PHP class that's able to tell the dispatcher exactly which +events it should subscribe to. It implements the +:class:`Symfony\\Component\\EventDispatcher\\EventSubscriberInterface` +interface, which requires a single static method called +:method:`Symfony\\Component\\EventDispatcher\\EventSubscriberInterface::getSubscribedEvents`. +Take the following example of a subscriber that subscribes to the +``kernel.response`` and ``OrderPlacedEvent::class`` events:: + + namespace Acme\Store\Event; + + use Acme\Store\Event\OrderPlacedEvent; + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpKernel\Event\ResponseEvent; + use Symfony\Component\HttpKernel\KernelEvents; + + class StoreSubscriber implements EventSubscriberInterface + { + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::RESPONSE => [ + ['onKernelResponsePre', 10], + ['onKernelResponsePost', -10], + ], + OrderPlacedEvent::class => 'onPlacedOrder', + ]; + } + + public function onKernelResponsePre(ResponseEvent $event): void + { + // ... + } + + public function onKernelResponsePost(ResponseEvent $event): void + { + // ... + } + + public function onPlacedOrder(OrderPlacedEvent $event): void + { + $order = $event->getOrder(); + // ... + } + } + +This is very similar to a listener class, except that the class itself can +tell the dispatcher which events it should listen to. To register a subscriber +with the dispatcher, use the +:method:`Symfony\\Component\\EventDispatcher\\EventDispatcher::addSubscriber` +method:: + + use Acme\Store\Event\StoreSubscriber; + // ... + + $subscriber = new StoreSubscriber(); + $dispatcher->addSubscriber($subscriber); + +The dispatcher will automatically register the subscriber for each event +returned by the ``getSubscribedEvents()`` method. This method returns an array +indexed by event names and whose values are either the method name to call +or an array composed of the method name to call and a priority (a positive or +negative integer that defaults to ``0``). + +The example above shows how to register several listener methods for the same +event in subscriber and also shows how to pass the priority of each listener +method. The higher the number, the earlier the method is called. In the above +example, when the ``kernel.response`` event is triggered, the methods +``onKernelResponsePre()`` and ``onKernelResponsePost()`` are called in that +order. + +.. _event_dispatcher-event-propagation: + +Stopping Event Flow/Propagation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In some cases, it may make sense for a listener to prevent any other listeners +from being called. In other words, the listener needs to be able to tell +the dispatcher to stop all propagation of the event to future listeners +(i.e. to not notify any more listeners). This can be accomplished from +inside a listener via the +:method:`Symfony\\Contracts\\EventDispatcher\\Event::stopPropagation` method:: + + use Acme\Store\Event\OrderPlacedEvent; + + public function onPlacedOrder(OrderPlacedEvent $event): void + { + // ... + + $event->stopPropagation(); + } + +Now, any listeners to ``OrderPlacedEvent::class`` that have not yet been called will +*not* be called. + +It is possible to detect if an event was stopped by using the +:method:`Symfony\\Contracts\\EventDispatcher\\Event::isPropagationStopped` +method which returns a boolean value:: + + // ... + $dispatcher->dispatch($event, 'foo.event'); + if ($event->isPropagationStopped()) { + // ... + } + +.. _event_dispatcher-dispatcher-aware-events: + +EventDispatcher Aware Events and Listeners +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``EventDispatcher`` always passes the dispatched event, the event's +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. + +.. _event_dispatcher-event-name-introspection: + +Event Name Introspection +~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``EventDispatcher`` instance, as well as the name of the event that +is dispatched, are passed as arguments to the listener:: + + use Symfony\Contracts\EventDispatcher\Event; + use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + + class MyListener + { + public function myEventListener(Event $event, string $eventName, EventDispatcherInterface $dispatcher): void + { + // ... do something with the event name + } + } + +Other Dispatchers +----------------- + +Besides the commonly used ``EventDispatcher``, the component comes +with some other dispatchers: + +* :doc:`/components/event_dispatcher/immutable_dispatcher` +* :doc:`/components/event_dispatcher/traceable_dispatcher` + +Learn More +---------- + +* :doc:`/components/event_dispatcher/generic_event` +* :ref:`The kernel.event_listener tag ` +* :ref:`The kernel.event_subscriber tag ` + +.. _Mediator: https://en.wikipedia.org/wiki/Mediator_pattern +.. _Observer: https://en.wikipedia.org/wiki/Observer_pattern +.. _Closures: https://www.php.net/manual/en/functions.anonymous.php +.. _PHP callable: https://www.php.net/manual/en/language.types.callable.php diff --git a/components/event_dispatcher/container_aware_dispatcher.rst b/components/event_dispatcher/container_aware_dispatcher.rst deleted file mode 100644 index a1bfe3e2904..00000000000 --- a/components/event_dispatcher/container_aware_dispatcher.rst +++ /dev/null @@ -1,102 +0,0 @@ -.. index:: - single: Event Dispatcher; Service container aware - -The Container Aware Event Dispatcher -==================================== - -.. versionadded:: 2.1 - This feature was moved into the EventDispatcher component in Symfony 2.1. - -Introduction ------------- - -The :class:`Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher` is -a special event dispatcher implementation which is coupled to the service container -that is part of :doc:`the Dependency Injection component`. -It allows services to be specified as event listeners making the event dispatcher -extremely powerful. - -Services are lazy loaded meaning the services attached as listeners will only be -created if an event is dispatched that requires those listeners. - -Setup ------ - -Setup is straightforward by injecting a :class:`Symfony\\Component\\DependencyInjection\\ContainerInterface` -into the :class:`Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher`:: - - use Symfony\Component\DependencyInjection\ContainerBuilder; - use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher; - - $container = new ContainerBuilder(); - $dispatcher = new ContainerAwareEventDispatcher($container); - -Adding Listeners ----------------- - -The *Container Aware Event Dispatcher* can either load specified services -directly, or services that implement :class:`Symfony\\Component\\EventDispatcher\\EventSubscriberInterface`. - -The following examples assume the service container has been loaded with any -services that are mentioned. - -.. note:: - - Services must be marked as public in the container. - -Adding Services -~~~~~~~~~~~~~~~ - -To connect existing service definitions, use the -:method:`Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher::addListenerService` -method where the ``$callback`` is an array of ``array($serviceId, $methodName)``:: - - $dispatcher->addListenerService($eventName, array('foo', 'logListener')); - -Adding Subscriber Services -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``EventSubscribers`` can be added using the -:method:`Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher::addSubscriberService` -method where the first argument is the service ID of the subscriber service, -and the second argument is the the service's class name (which must implement -:class:`Symfony\\Component\\EventDispatcher\\EventSubscriberInterface`) as follows:: - - $dispatcher->addSubscriberService( - 'kernel.store_subscriber', - 'StoreSubscriber' - ); - -The ``EventSubscriberInterface`` will be exactly as you would expect:: - - use Symfony\Component\EventDispatcher\EventSubscriberInterface; - // ... - - class StoreSubscriber implements EventSubscriberInterface - { - public static function getSubscribedEvents() - { - return array( - 'kernel.response' => array( - array('onKernelResponsePre', 10), - array('onKernelResponsePost', 0), - ), - 'store.order' => array('onStoreOrder', 0), - ); - } - - public function onKernelResponsePre(FilterResponseEvent $event) - { - // ... - } - - public function onKernelResponsePost(FilterResponseEvent $event) - { - // ... - } - - public function onStoreOrder(FilterOrderEvent $event) - { - // ... - } - } diff --git a/components/event_dispatcher/generic_event.rst b/components/event_dispatcher/generic_event.rst index e7a3dd5be36..41d0a9d66a4 100644 --- a/components/event_dispatcher/generic_event.rst +++ b/components/event_dispatcher/generic_event.rst @@ -1,26 +1,21 @@ -.. index:: - single: Event Dispatcher - The Generic Event Object ======================== -.. versionadded:: 2.1 - The ``GenericEvent`` event class was added in Symfony 2.1 - -The base :class:`Symfony\\Component\\EventDispatcher\\Event` class provided by the -``Event Dispatcher`` component is deliberately sparse to allow the creation of -API specific event objects by inheritance using OOP. This allow for elegant and -readable code in complex applications. +The base :class:`Symfony\\Contracts\\EventDispatcher\\Event` class provided +by the EventDispatcher component is deliberately sparse to allow the creation +of API specific event objects by inheritance using OOP. This allows for +elegant and readable code in complex applications. The :class:`Symfony\\Component\\EventDispatcher\\GenericEvent` is available -for convenience for those who wish to use just one event object throughout their -application. It is suitable for most purposes straight out of the box, because -it follows the standard observer pattern where the event object +for convenience for those who wish to use just one event object throughout +their application. It is suitable for most purposes straight out of the +box, because it follows the standard observer pattern where the event object encapsulates an event 'subject', but has the addition of optional extra arguments. -:class:`Symfony\\Component\\EventDispatcher\\GenericEvent` has a simple API in -addition to the base class :class:`Symfony\\Component\\EventDispatcher\\Event` +:class:`Symfony\\Component\\EventDispatcher\\GenericEvent` adds some more +methods in addition to the base class +:class:`Symfony\\Contracts\\EventDispatcher\\Event` * :method:`Symfony\\Component\\EventDispatcher\\GenericEvent::__construct`: Constructor takes the event subject and any arguments; @@ -44,22 +39,22 @@ addition to the base class :class:`Symfony\\Component\\EventDispatcher\\Event` Returns true if the argument key exists; The ``GenericEvent`` also implements :phpclass:`ArrayAccess` on the event -arguments which makes it very convenient to pass extra arguments regarding the -event subject. +arguments which makes it very convenient to pass extra arguments regarding +the event subject. The following examples show use-cases to give a general idea of the flexibility. The examples assume event listeners have been added to the dispatcher. -Simply passing a subject:: +Passing a subject:: use Symfony\Component\EventDispatcher\GenericEvent; - $event = GenericEvent($subject); - $dispatcher->dispatch('foo', $event); + $event = new GenericEvent($subject); + $dispatcher->dispatch($event, 'foo'); class FooListener { - public function handler(GenericEvent $event) + public function handler(GenericEvent $event): void { if ($event->getSubject() instanceof Foo) { // ... @@ -67,24 +62,22 @@ Simply passing a subject:: } } -Passing and processing arguments using the :phpclass:`ArrayAccess` API to access -the event arguments:: +Passing and processing arguments using the :phpclass:`ArrayAccess` API to +access the event arguments:: use Symfony\Component\EventDispatcher\GenericEvent; $event = new GenericEvent( $subject, - array('type' => 'foo', 'counter' => 0) + ['type' => 'foo', 'counter' => 0] ); - $dispatcher->dispatch('foo', $event); - - echo $event['counter']; + $dispatcher->dispatch($event, 'foo'); class FooListener { - public function handler(GenericEvent $event) + public function handler(GenericEvent $event): void { - if (isset($event['type']) && $event['type'] === 'foo') { + if (isset($event['type']) && 'foo' === $event['type']) { // ... do something } @@ -96,15 +89,13 @@ Filtering data:: use Symfony\Component\EventDispatcher\GenericEvent; - $event = new GenericEvent($subject, array('data' => 'foo')); - $dispatcher->dispatch('foo', $event); - - echo $event['data']; + $event = new GenericEvent($subject, ['data' => 'Foo']); + $dispatcher->dispatch($event, 'foo'); class FooListener { - public function filter(GenericEvent $event) + public function filter(GenericEvent $event): void { - strtolower($event['data']); + $event['data'] = strtolower($event['data']); } } diff --git a/components/event_dispatcher/immutable_dispatcher.rst b/components/event_dispatcher/immutable_dispatcher.rst new file mode 100644 index 00000000000..a6a98c47f37 --- /dev/null +++ b/components/event_dispatcher/immutable_dispatcher.rst @@ -0,0 +1,35 @@ +The Immutable Event Dispatcher +============================== + +The :class:`Symfony\\Component\\EventDispatcher\\ImmutableEventDispatcher` +is a locked or frozen event dispatcher. The dispatcher cannot register new +listeners or subscribers. + +The ``ImmutableEventDispatcher`` takes another event dispatcher with all +the listeners and subscribers. The immutable dispatcher is just a proxy +of this original dispatcher. + +To use it, first create a normal ``EventDispatcher`` dispatcher and register +some listeners or subscribers:: + + use Symfony\Component\EventDispatcher\EventDispatcher; + use Symfony\Contracts\EventDispatcher\Event; + + $dispatcher = new EventDispatcher(); + $dispatcher->addListener('foo.action', function (Event $event): void { + // ... + }); + + // ... + +Now, inject that into an ``ImmutableEventDispatcher``:: + + use Symfony\Component\EventDispatcher\ImmutableEventDispatcher; + // ... + + $immutableDispatcher = new ImmutableEventDispatcher($dispatcher); + +You'll need to use this new dispatcher in your project. + +If you are trying to execute one of the methods which modifies the dispatcher +(e.g. ``addListener()``), a ``BadMethodCallException`` is thrown. diff --git a/components/event_dispatcher/index.rst b/components/event_dispatcher/index.rst deleted file mode 100644 index 4800978d501..00000000000 --- a/components/event_dispatcher/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Event Dispatcher -================ - -.. toctree:: - :maxdepth: 2 - - introduction - generic_event - container_aware_dispatcher diff --git a/components/event_dispatcher/introduction.rst b/components/event_dispatcher/introduction.rst deleted file mode 100644 index 5cd4fe3af21..00000000000 --- a/components/event_dispatcher/introduction.rst +++ /dev/null @@ -1,603 +0,0 @@ -.. index:: - single: Event Dispatcher - single: Components; EventDispatcher - -The Event Dispatcher Component -============================== - -Introduction ------------- - -Objected Oriented code has gone a long way to ensuring code extensibility. By -creating classes that have well defined responsibilities, your code becomes -more flexible and a developer can extend them with subclasses to modify their -behaviors. But if he wants to share his changes with other developers who have -also made their own subclasses, code inheritance is no longer the answer. - -Consider the real-world example where you want to provide a plugin system for -your project. A plugin should be able to add methods, or do something before -or after a method is executed, without interfering with other plugins. This is -not an easy problem to solve with single inheritance, and multiple inheritance -(were it possible with PHP) has its own drawbacks. - -The Symfony2 Event Dispatcher component implements the `Observer`_ pattern in -a simple and effective way to make all these things possible and to make your -projects truly extensible. - -Take a simple example from the :doc:`/components/http_kernel/introduction`. Once a -``Response`` object has been created, it may be useful to allow other elements -in the system to modify it (e.g. add some cache headers) before it's actually -used. To make this possible, the Symfony2 kernel throws an event - -``kernel.response``. Here's how it works: - -* A *listener* (PHP object) tells a central *dispatcher* object that it wants - to listen to the ``kernel.response`` event; - -* At some point, the Symfony2 kernel tells the *dispatcher* object to dispatch - the ``kernel.response`` event, passing with it an ``Event`` object that has - access to the ``Response`` object; - -* The dispatcher notifies (i.e. calls a method on) all listeners of the - ``kernel.response`` event, allowing each of them to make modifications to - the ``Response`` object. - -.. index:: - single: Event Dispatcher; Events - -Installation ------------- - -You can install the component in many different ways: - -* Use the official Git repository (https://github.com/symfony/EventDispatcher); -* :doc:`Install it via Composer` (``symfony/event-dispatcher`` on `Packagist`_). - -Usage ------ - -Events -~~~~~~ - -When an event is dispatched, it's identified by a unique name (e.g. -``kernel.response``), which any number of listeners might be listening to. An -:class:`Symfony\\Component\\EventDispatcher\\Event` instance is also created -and passed to all of the listeners. As you'll see later, the ``Event`` object -itself often contains data about the event being dispatched. - -.. index:: - pair: Event Dispatcher; Naming conventions - -Naming Conventions -.................. - -The unique event name can be any string, but optionally follows a few simple -naming conventions: - -* use only lowercase letters, numbers, dots (``.``), and underscores (``_``); - -* prefix names with a namespace followed by a dot (e.g. ``kernel.``); - -* end names with a verb that indicates what action is being taken (e.g. - ``request``). - -Here are some examples of good event names: - -* ``kernel.response`` -* ``form.pre_set_data`` - -.. index:: - single: Event Dispatcher; Event subclasses - -Event Names and Event Objects -............................. - -When the dispatcher notifies listeners, it passes an actual ``Event`` object -to those listeners. The base ``Event`` class is very simple: it contains a -method for stopping :ref:`event -propagation`, but not much else. - -Often times, data about a specific event needs to be passed along with the -``Event`` object so that the listeners have needed information. In the case of -the ``kernel.response`` event, the ``Event`` object that's created and passed to -each listener is actually of type -:class:`Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent`, a -subclass of the base ``Event`` object. This class contains methods such as -``getResponse`` and ``setResponse``, allowing listeners to get or even replace -the ``Response`` object. - -The moral of the story is this: When creating a listener to an event, the -``Event`` object that's passed to the listener may be a special subclass that -has additional methods for retrieving information from and responding to the -event. - -The Dispatcher -~~~~~~~~~~~~~~ - -The dispatcher is the central object of the event dispatcher system. In -general, a single dispatcher is created, which maintains a registry of -listeners. When an event is dispatched via the dispatcher, it notifies all -listeners registered with that event:: - - use Symfony\Component\EventDispatcher\EventDispatcher; - - $dispatcher = new EventDispatcher(); - -.. index:: - single: Event Dispatcher; Listeners - -Connecting Listeners -~~~~~~~~~~~~~~~~~~~~ - -To take advantage of an existing event, you need to connect a listener to the -dispatcher so that it can be notified when the event is dispatched. A call to -the dispatcher ``addListener()`` method associates any valid PHP callable to -an event:: - - $listener = new AcmeListener(); - $dispatcher->addListener('foo.action', array($listener, 'onFooAction')); - -The ``addListener()`` method takes up to three arguments: - -* The event name (string) that this listener wants to listen to; - -* A PHP callable that will be notified when an event is thrown that it listens - to; - -* An optional priority integer (higher equals more important) that determines - when a listener is triggered versus other listeners (defaults to ``0``). If - two listeners have the same priority, they are executed in the order that - they were added to the dispatcher. - -.. note:: - - A `PHP callable`_ is a PHP variable that can be used by the - ``call_user_func()`` function and returns ``true`` when passed to the - ``is_callable()`` function. It can be a ``\Closure`` instance, an object - implementing an __invoke method (which is what closures are in fact), - a string representing a function, or an array representing an object - method or a class method. - - So far, you've seen how PHP objects can be registered as listeners. You - can also register PHP `Closures`_ as event listeners:: - - use Symfony\Component\EventDispatcher\Event; - - $dispatcher->addListener('foo.action', function (Event $event) { - // will be executed when the foo.action event is dispatched - }); - -Once a listener is registered with the dispatcher, it waits until the event is -notified. In the above example, when the ``foo.action`` event is dispatched, -the dispatcher calls the ``AcmeListener::onFooAction`` method and passes the -``Event`` object as the single argument:: - - use Symfony\Component\EventDispatcher\Event; - - class AcmeListener - { - // ... - - public function onFooAction(Event $event) - { - // ... do something - } - } - -In many cases, a special ``Event`` subclass that's specific to the given event -is passed to the listener. This gives the listener access to special -information about the event. Check the documentation or implementation of each -event to determine the exact ``Symfony\Component\EventDispatcher\Event`` -instance that's being passed. For example, the ``kernel.event`` event passes an -instance of ``Symfony\Component\HttpKernel\Event\FilterResponseEvent``:: - - use Symfony\Component\HttpKernel\Event\FilterResponseEvent; - - public function onKernelResponse(FilterResponseEvent $event) - { - $response = $event->getResponse(); - $request = $event->getRequest(); - - // ... - } - -.. _event_dispatcher-closures-as-listeners: - -.. index:: - single: Event Dispatcher; Creating and dispatching an event - -Creating and Dispatching an Event -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In addition to registering listeners with existing events, you can create and -dispatch your own events. This is useful when creating third-party libraries -and also when you want to keep different components of your own system -flexible and decoupled. - -The Static ``Events`` Class -........................... - -Suppose you want to create a new Event - ``store.order`` - that is dispatched -each time an order is created inside your application. To keep things -organized, start by creating a ``StoreEvents`` class inside your application -that serves to define and document your event:: - - namespace Acme\StoreBundle; - - final class StoreEvents - { - /** - * The store.order event is thrown each time an order is created - * in the system. - * - * The event listener receives an - * Acme\StoreBundle\Event\FilterOrderEvent instance. - * - * @var string - */ - const STORE_ORDER = 'store.order'; - } - -Notice that this class doesn't actually *do* anything. The purpose of the -``StoreEvents`` class is just to be a location where information about common -events can be centralized. Notice also that a special ``FilterOrderEvent`` -class will be passed to each listener of this event. - -Creating an Event object -........................ - -Later, when you dispatch this new event, you'll create an ``Event`` instance -and pass it to the dispatcher. The dispatcher then passes this same instance -to each of the listeners of the event. If you don't need to pass any -information to your listeners, you can use the default -``Symfony\Component\EventDispatcher\Event`` class. Most of the time, however, -you *will* need to pass information about the event to each listener. To -accomplish this, you'll create a new class that extends -``Symfony\Component\EventDispatcher\Event``. - -In this example, each listener will need access to some pretend ``Order`` -object. Create an ``Event`` class that makes this possible:: - - namespace Acme\StoreBundle\Event; - - use Symfony\Component\EventDispatcher\Event; - use Acme\StoreBundle\Order; - - class FilterOrderEvent extends Event - { - protected $order; - - public function __construct(Order $order) - { - $this->order = $order; - } - - public function getOrder() - { - return $this->order; - } - } - -Each listener now has access to the ``Order`` object via the ``getOrder`` -method. - -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:: - - use Acme\StoreBundle\StoreEvents; - use Acme\StoreBundle\Order; - use Acme\StoreBundle\Event\FilterOrderEvent; - - // the order is somehow created or retrieved - $order = new Order(); - // ... - - // create the FilterOrderEvent and dispatch it - $event = new FilterOrderEvent($order); - $dispatcher->dispatch(StoreEvents::STORE_ORDER, $event); - -Notice that the special ``FilterOrderEvent`` object is created and passed to -the ``dispatch`` method. Now, any listener to the ``store.order`` event will -receive the ``FilterOrderEvent`` and have access to the ``Order`` object via -the ``getOrder`` method:: - - // some listener class that's been registered for "STORE_ORDER" event - use Acme\StoreBundle\Event\FilterOrderEvent; - - public function onStoreOrder(FilterOrderEvent $event) - { - $order = $event->getOrder(); - // do something to or with the order - } - -.. index:: - single: Event Dispatcher; Event subscribers - -.. _event_dispatcher-using-event-subscribers: - -Using Event Subscribers -~~~~~~~~~~~~~~~~~~~~~~~ - -The most common way to listen to an event is to register an *event listener* -with the dispatcher. This listener can listen to one or more events and is -notified each time those events are dispatched. - -Another way to listen to events is via an *event subscriber*. An event -subscriber is a PHP class that's able to tell the dispatcher exactly which -events it should subscribe to. It implements the -:class:`Symfony\\Component\\EventDispatcher\\EventSubscriberInterface` -interface, which requires a single static method called -``getSubscribedEvents``. Take the following example of a subscriber that -subscribes to the ``kernel.response`` and ``store.order`` events:: - - namespace Acme\StoreBundle\Event; - - use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\FilterResponseEvent; - - class StoreSubscriber implements EventSubscriberInterface - { - public static function getSubscribedEvents() - { - return array( - 'kernel.response' => array( - array('onKernelResponsePre', 10), - array('onKernelResponseMid', 5), - array('onKernelResponsePost', 0), - ), - 'store.order' => array('onStoreOrder', 0), - ); - } - - public function onKernelResponsePre(FilterResponseEvent $event) - { - // ... - } - - public function onKernelResponseMid(FilterResponseEvent $event) - { - // ... - } - - public function onKernelResponsePost(FilterResponseEvent $event) - { - // ... - } - - public function onStoreOrder(FilterOrderEvent $event) - { - // ... - } - } - -This is very similar to a listener class, except that the class itself can -tell the dispatcher which events it should listen to. To register a subscriber -with the dispatcher, use the -:method:`Symfony\\Component\\EventDispatcher\\EventDispatcher::addSubscriber` -method:: - - use Acme\StoreBundle\Event\StoreSubscriber; - - $subscriber = new StoreSubscriber(); - $dispatcher->addSubscriber($subscriber); - -The dispatcher will automatically register the subscriber for each event -returned by the ``getSubscribedEvents`` method. This method returns an array -indexed by event names and whose values are either the method name to call or -an array composed of the method name to call and a priority. The example -above shows how to register several listener methods for the same event in -subscriber and also shows how to pass the priority of each listener method. -The higher the priority, the earlier the method is called. In the above -example, when the ``kernel.response`` event is triggered, the methods -``onKernelResponsePre``, ``onKernelResponseMid``, and ``onKernelResponsePost`` -are called in that order. - -.. index:: - single: Event Dispatcher; Stopping event flow - -.. _event_dispatcher-event-propagation: - -Stopping Event Flow/Propagation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In some cases, it may make sense for a listener to prevent any other listeners -from being called. In other words, the listener needs to be able to tell the -dispatcher to stop all propagation of the event to future listeners (i.e. to -not notify any more listeners). This can be accomplished from inside a -listener via the -:method:`Symfony\\Component\\EventDispatcher\\Event::stopPropagation` method:: - - use Acme\StoreBundle\Event\FilterOrderEvent; - - public function onStoreOrder(FilterOrderEvent $event) - { - // ... - - $event->stopPropagation(); - } - -Now, any listeners to ``store.order`` that have not yet been called will *not* -be called. - -It is possible to detect if an event was stopped by using the -:method:`Symfony\\Component\\EventDispatcher\\Event::isPropagationStopped` method -which returns a boolean value:: - - $dispatcher->dispatch('foo.event', $event); - if ($event->isPropagationStopped()) { - // ... - } - -.. index:: - single: Event Dispatcher; Event Dispatcher aware events and listeners - -.. _event_dispatcher-dispatcher-aware-events: - -EventDispatcher aware Events and Listeners -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.1 - The ``Event`` object contains a reference to the invoking dispatcher since Symfony 2.1 - -The ``EventDispatcher`` always injects a reference to itself in the passed event -object. This means that all listeners have direct access to the -``EventDispatcher`` object that notified the listener via the passed ``Event`` -object's :method:`Symfony\\Component\\EventDispatcher\\Event::getDispatcher` -method. - -This can lead to some advanced applications of the ``EventDispatcher`` including -letting listeners dispatch other events, event chaining or even lazy loading of -more listeners into the dispatcher object. Examples follow: - -Lazy loading listeners:: - - use Symfony\Component\EventDispatcher\Event; - use Acme\StoreBundle\Event\StoreSubscriber; - - class Foo - { - private $started = false; - - public function myLazyListener(Event $event) - { - if (false === $this->started) { - $subscriber = new StoreSubscriber(); - $event->getDispatcher()->addSubscriber($subscriber); - } - - $this->started = true; - - // ... more code - } - } - -Dispatching another event from within a listener:: - - use Symfony\Component\EventDispatcher\Event; - - class Foo - { - public function myFooListener(Event $event) - { - $event->getDispatcher()->dispatch('log', $event); - - // ... more code - } - } - -While this above is sufficient for most uses, if your application uses multiple -``EventDispatcher`` instances, you might need to specifically inject a known -instance of the ``EventDispatcher`` into your listeners. This could be done -using constructor or setter injection as follows: - -Constructor injection:: - - use Symfony\Component\EventDispatcher\EventDispatcherInterface; - - class Foo - { - protected $dispatcher = null; - - public function __construct(EventDispatcherInterface $dispatcher) - { - $this->dispatcher = $dispatcher; - } - } - -Or setter injection:: - - use Symfony\Component\EventDispatcher\EventDispatcherInterface; - - class Foo - { - protected $dispatcher = null; - - public function setEventDispatcher(EventDispatcherInterface $dispatcher) - { - $this->dispatcher = $dispatcher; - } - } - -Choosing between the two is really a matter of taste. Many tend to prefer the -constructor injection as the objects are fully initialized at construction -time. But when you have a long list of dependencies, using setter injection -can be the way to go, especially for optional dependencies. - -.. index:: - single: Event Dispatcher; Dispatcher shortcuts - -.. _event_dispatcher-shortcuts: - -Dispatcher Shortcuts -~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.1 - ``EventDispatcher::dispatch()`` method returns the event since Symfony 2.1. - -The :method:`EventDispatcher::dispatch` -method always returns an :class:`Symfony\\Component\\EventDispatcher\\Event` -object. This allows for various shortcuts. For example if one does not need -a custom event object, one can simply rely on a plain -:class:`Symfony\\Component\\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('foo.event'); - -Moreover, the EventDispatcher 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:: - - $barEvent = new BarEvent(); - $bar = $dispatcher->dispatch('bar.event', $barEvent)->getBar(); - -Or:: - - $response = $dispatcher->dispatch('bar.event', new BarEvent())->getBar(); - -and so on... - -.. index:: - single: Event Dispatcher; Event name introspection - -.. _event_dispatcher-event-name-introspection: - -Event Name Introspection -~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.1 - Added event name to the ``Event`` object since Symfony 2.1 - -Since the ``EventDispatcher`` already knows the name of the event when dispatching -it, the event name is also injected into the -:class:`Symfony\\Component\\EventDispatcher\\Event` objects, making it available -to event listeners via the :method:`Symfony\\Component\\EventDispatcher\\Event::getName` -method. - -The event name, (as with any other data in a custom event object) can be used as -part of the listener's processing logic:: - - use Symfony\Component\EventDispatcher\Event; - - class Foo - { - public function myEventListener(Event $event) - { - echo $event->getName(); - } - } - -.. _Observer: http://en.wikipedia.org/wiki/Observer_pattern -.. _Closures: http://php.net/manual/en/functions.anonymous.php -.. _PHP callable: http://www.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/traceable_dispatcher.rst b/components/event_dispatcher/traceable_dispatcher.rst new file mode 100644 index 00000000000..7b3819e3a48 --- /dev/null +++ b/components/event_dispatcher/traceable_dispatcher.rst @@ -0,0 +1,49 @@ +The Traceable Event Dispatcher +============================== + +The :class:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher` +is an event dispatcher that wraps any other event dispatcher and can then +be used to determine which event listeners have been called by the dispatcher. +Pass the event dispatcher to be wrapped and an instance of the +:class:`Symfony\\Component\\Stopwatch\\Stopwatch` to its constructor:: + + use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher; + use Symfony\Component\Stopwatch\Stopwatch; + + // the event dispatcher to debug + $dispatcher = ...; + + $traceableEventDispatcher = new TraceableEventDispatcher( + $dispatcher, + new Stopwatch() + ); + +Now, the ``TraceableEventDispatcher`` can be used like any other event dispatcher +to register event listeners and dispatch events:: + + // ... + + // registers an event listener + $eventListener = ...; + $priority = ...; + $traceableEventDispatcher->addListener( + 'event.the_name', + $eventListener, + $priority + ); + + // dispatches an event + $event = ...; + $traceableEventDispatcher->dispatch($event, 'event.the_name'); + +After your application has been processed, you can use the +:method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher::getCalledListeners` +method to retrieve an array of event listeners that have been called in +your application. Similarly, the +:method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher::getNotCalledListeners` +method returns an array of event listeners that have not been called:: + + // ... + + $calledListeners = $traceableEventDispatcher->getCalledListeners(); + $notCalledListeners = $traceableEventDispatcher->getNotCalledListeners(); diff --git a/components/expression_language.rst b/components/expression_language.rst new file mode 100644 index 00000000000..b0dd10b0f42 --- /dev/null +++ b/components/expression_language.rst @@ -0,0 +1,419 @@ +The ExpressionLanguage Component +================================ + + The ExpressionLanguage component provides an engine that can compile and + evaluate expressions. An expression is a one-liner that returns a value + (mostly, but not limited to, Booleans). + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/expression-language + +.. include:: /components/require_autoload.rst.inc + +.. _how-can-the-expression-engine-help-me: + +How can the Expression Language Help Me? +---------------------------------------- + +The purpose of the component is to allow users to use expressions inside +configuration for more complex logic. For example, the Symfony Framework uses +expressions in security, for validation rules and in route matching. + +Besides using the component in the framework itself, the ExpressionLanguage +component is a perfect candidate for the foundation of a *business rule engine*. +The idea is to let the webmaster of a website configure things in a dynamic +way without using PHP and without introducing security problems: + +.. _component-expression-language-examples: + +.. code-block:: text + + # Get the special price if + user.getGroup() in ['good_customers', 'collaborator'] + + # Promote article to the homepage when + article.commentCount > 100 and article.category not in ["misc"] + + # Send an alert when + product.stock < 15 + +Expressions can be seen as a very restricted PHP sandbox and are less vulnerable +to external injections because you must explicitly declare which variables are +available in an expression (but you should still sanitize any data given by end +users and passed to expressions). + +Usage +----- + +The ExpressionLanguage component can compile and evaluate expressions. +Expressions are one-liners that often return a Boolean, which can be used +by the code executing the expression in an ``if`` statement. A simple example +of an expression is ``1 + 2``. You can also use more complicated expressions, +such as ``someArray[3].someMethod('bar')``. + +The component provides 2 ways to work with expressions: + +* **evaluation**: the expression is evaluated without being compiled to PHP; +* **compile**: the expression is compiled to PHP, so it can be cached and + evaluated. + +The main class of the component is +:class:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage`:: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + + $expressionLanguage = new ExpressionLanguage(); + + var_dump($expressionLanguage->evaluate('1 + 2')); // displays 3 + + var_dump($expressionLanguage->compile('1 + 2')); // displays (1 + 2) + +.. tip:: + + See :doc:`/reference/formats/expression_language` to learn the syntax of + the ExpressionLanguage component. + +Null Coalescing Operator +........................ + +.. note:: + + This content has been moved to the :ref:`null coalescing operator ` + section of ExpressionLanguage syntax reference page. + +Parsing and Linting Expressions +............................... + +The ExpressionLanguage component provides a way to parse and lint expressions. +The :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::parse` +method returns a :class:`Symfony\\Component\\ExpressionLanguage\\ParsedExpression` +instance that can be used to inspect and manipulate the expression. The +:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::lint`, on the +other hand, throws a :class:`Symfony\\Component\\ExpressionLanguage\\SyntaxError` +if the expression is not valid:: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + + $expressionLanguage = new ExpressionLanguage(); + + var_dump($expressionLanguage->parse('1 + 2', [])); + // displays the AST nodes of the expression which can be + // inspected and manipulated + + $expressionLanguage->lint('1 + 2', []); // doesn't throw anything + + $expressionLanguage->lint('1 + a', []); + // throws a SyntaxError exception: + // "Variable "a" is not valid around position 5 for expression `1 + a`." + +The behavior of these methods can be configured with some flags defined in the +:class:`Symfony\\Component\\ExpressionLanguage\\Parser` class: + +* ``IGNORE_UNKNOWN_VARIABLES``: don't throw an exception if a variable is not + defined in the expression; +* ``IGNORE_UNKNOWN_FUNCTIONS``: don't throw an exception if a function is not + defined in the expression. + +This is how you can use these flags:: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + use Symfony\Component\ExpressionLanguage\Parser; + + $expressionLanguage = new ExpressionLanguage(); + + // does not throw a SyntaxError because the unknown variables and functions are ignored + $expressionLanguage->lint('unknown_var + unknown_function()', [], Parser::IGNORE_UNKNOWN_VARIABLES | Parser::IGNORE_UNKNOWN_FUNCTIONS); + +.. versionadded:: 7.1 + + The support for flags in the ``parse()`` and ``lint()`` methods + was introduced in Symfony 7.1. + +Passing in Variables +-------------------- + +You can also pass variables into the expression, which can be of any valid +PHP type (including objects):: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + + $expressionLanguage = new ExpressionLanguage(); + + class Apple + { + public string $variety; + } + + $apple = new Apple(); + $apple->variety = 'Honeycrisp'; + + var_dump($expressionLanguage->evaluate( + 'fruit.variety', + [ + 'fruit' => $apple, + ] + )); // displays "Honeycrisp" + +When using this component inside a Symfony application, certain objects and +variables are automatically injected by Symfony so you can use them in your +expressions (e.g. the request, the current user, etc.): + +* :doc:`Variables available in security expressions `; +* :doc:`Variables available in service container expressions `; +* :ref:`Variables available in routing expressions `. + +.. _expression-language-caching: + +Caching +------- + +The ExpressionLanguage component provides a +:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::compile` +method to be able to cache the expressions in plain PHP. But internally, the +component also caches the parsed expressions, so duplicated expressions can be +compiled/evaluated quicker. + +The Workflow +~~~~~~~~~~~~ + +Both :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::evaluate` +and ``compile()`` need to do some things before each can provide the return +values. For ``evaluate()``, this overhead is even bigger. + +Both methods need to tokenize and parse the expression. This is done by the +:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::parse` +method. It returns a :class:`Symfony\\Component\\ExpressionLanguage\\ParsedExpression`. +Now, the ``compile()`` method just returns the string conversion of this object. +The ``evaluate()`` method needs to loop through the "nodes" (pieces of an +expression saved in the ``ParsedExpression``) and evaluate them on the fly. + +To save time, the ``ExpressionLanguage`` caches the ``ParsedExpression`` so +it can skip the tokenization and parsing steps with duplicate expressions. The +caching is done by a PSR-6 `CacheItemPoolInterface`_ instance (by default, it +uses an :class:`Symfony\\Component\\Cache\\Adapter\\ArrayAdapter`). You can +customize this by creating a custom cache pool or using one of the available +ones and injecting this using the constructor:: + + use Symfony\Component\Cache\Adapter\RedisAdapter; + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + + $cache = new RedisAdapter(...); + $expressionLanguage = new ExpressionLanguage($cache); + +.. seealso:: + + See the :doc:`/components/cache` documentation for more information about + available cache adapters. + +Using Parsed and Serialized Expressions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Both ``evaluate()`` and ``compile()`` can handle ``ParsedExpression`` and +``SerializedParsedExpression``:: + + // ... + + // the parse() method returns a ParsedExpression + $expression = $expressionLanguage->parse('1 + 4', []); + + var_dump($expressionLanguage->evaluate($expression)); // prints 5 + +.. code-block:: php + + use Symfony\Component\ExpressionLanguage\SerializedParsedExpression; + // ... + + $expression = new SerializedParsedExpression( + '1 + 4', + serialize($expressionLanguage->parse('1 + 4', [])->getNodes()) + ); + + var_dump($expressionLanguage->evaluate($expression)); // prints 5 + +.. _expression-language-ast: + +AST Dumping and Editing +----------------------- + +It's difficult to manipulate or inspect the expressions created with the ExpressionLanguage +component, because the expressions are plain strings. A better approach is to +turn those expressions into an AST. In computer science, `AST`_ (*Abstract +Syntax Tree*) is *"a tree representation of the structure of source code written +in a programming language"*. In Symfony, an ExpressionLanguage AST is a set of +nodes that contain PHP classes representing the given expression. + +Dumping the AST +~~~~~~~~~~~~~~~ + +Call the :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::getNodes` +method after parsing any expression to get its AST:: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + + $ast = (new ExpressionLanguage()) + ->parse('1 + 2', []) + ->getNodes() + ; + + // dump the AST nodes for inspection + var_dump($ast); + + // dump the AST nodes as a string representation + $astAsString = $ast->dump(); + +Manipulating the AST +~~~~~~~~~~~~~~~~~~~~ + +The nodes of the AST can also be dumped into a PHP array of nodes to allow +manipulating them. Call the :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::toArray` +method to turn the AST into an array:: + + // ... + + $astAsArray = (new ExpressionLanguage()) + ->parse('1 + 2', []) + ->getNodes() + ->toArray() + ; + +.. _expression-language-extending: + +Extending the ExpressionLanguage +-------------------------------- + +The ExpressionLanguage can be extended by adding custom functions. For +instance, in the Symfony Framework, the security has custom functions to check +the user's role. + +.. note:: + + If you want to learn how to use functions in an expression, read + ":ref:`component-expression-functions`". + +Registering Functions +~~~~~~~~~~~~~~~~~~~~~ + +Functions are registered on each specific ``ExpressionLanguage`` instance. +That means the functions can be used in any expression executed by that +instance. + +To register a function, use +:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::register`. +This method has 3 arguments: + +* **name** - The name of the function in an expression; +* **compiler** - A function executed when compiling an expression using the + function; +* **evaluator** - A function executed when the expression is evaluated. + +Example:: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + + $expressionLanguage = new ExpressionLanguage(); + $expressionLanguage->register('lowercase', function ($str): string { + return sprintf('(is_string(%1$s) ? strtolower(%1$s) : %1$s)', $str); + }, function ($arguments, $str): string { + if (!is_string($str)) { + return $str; + } + + return strtolower($str); + }); + + var_dump($expressionLanguage->evaluate('lowercase("HELLO")')); + // this will print: hello + +In addition to the custom function arguments, the **evaluator** is passed an +``arguments`` variable as its first argument, which is equal to the second +argument of ``evaluate()`` (e.g. the "values" when evaluating an expression). + +.. _components-expression-language-provider: + +Using Expression Providers +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you use the ``ExpressionLanguage`` class in your library, you often want +to add custom functions. To do so, you can create a new expression provider by +creating a class that implements +:class:`Symfony\\Component\\ExpressionLanguage\\ExpressionFunctionProviderInterface`. + +This interface requires one method: +:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionFunctionProviderInterface::getFunctions`, +which returns an array of expression functions (instances of +:class:`Symfony\\Component\\ExpressionLanguage\\ExpressionFunction`) to +register:: + + use Symfony\Component\ExpressionLanguage\ExpressionFunction; + use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; + + class StringExpressionLanguageProvider implements ExpressionFunctionProviderInterface + { + public function getFunctions(): array + { + return [ + new ExpressionFunction('lowercase', function ($str): string { + return sprintf('(is_string(%1$s) ? strtolower(%1$s) : %1$s)', $str); + }, function ($arguments, $str): string { + if (!is_string($str)) { + return $str; + } + + return strtolower($str); + }), + ]; + } + } + +.. tip:: + + To create an expression function from a PHP function with the + :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionFunction::fromPhp` static method:: + + ExpressionFunction::fromPhp('strtoupper'); + + Namespaced functions are supported, but they require a second argument to + define the name of the expression:: + + ExpressionFunction::fromPhp('My\strtoupper', 'my_strtoupper'); + +You can register providers using +:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::registerProvider` +or by using the second argument of the constructor:: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + + // using the constructor + $expressionLanguage = new ExpressionLanguage(null, [ + new StringExpressionLanguageProvider(), + // ... + ]); + + // using registerProvider() + $expressionLanguage->registerProvider(new StringExpressionLanguageProvider()); + +.. tip:: + + It is recommended to create your own ``ExpressionLanguage`` class in your + library. Now you can add the extension by overriding the constructor:: + + use Psr\Cache\CacheItemPoolInterface; + use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage; + + class ExpressionLanguage extends BaseExpressionLanguage + { + public function __construct(?CacheItemPoolInterface $cache = null, array $providers = []) + { + // prepends the default provider to let users override it + array_unshift($providers, new StringExpressionLanguageProvider()); + + parent::__construct($cache, $providers); + } + } + +.. _`AST`: https://en.wikipedia.org/wiki/Abstract_syntax_tree +.. _`CacheItemPoolInterface`: https://github.com/php-fig/cache/blob/master/src/CacheItemPoolInterface.php diff --git a/components/filesystem.rst b/components/filesystem.rst index 6e5ca5140fa..dabf3f81872 100644 --- a/components/filesystem.rst +++ b/components/filesystem.rst @@ -1,244 +1,519 @@ -.. index:: - single: Filesystem - The Filesystem Component ======================== - The Filesystem components provides basic utilities for the filesystem. - -.. versionadded:: 2.1 - The Filesystem Component is new to Symfony 2.1. Previously, the ``Filesystem`` - class was located in the ``HttpKernel`` component. + The Filesystem component provides platform-independent utilities for + filesystem operations and for file/directory paths manipulation. Installation ------------ -You can install the component in many different ways: +.. code-block:: terminal -* Use the official Git repository (https://github.com/symfony/Filesystem); -* Install it via Composer (``symfony/filesystem`` on `Packagist`_). + $ composer require symfony/filesystem + +.. include:: /components/require_autoload.rst.inc Usage ----- -The :class:`Symfony\\Component\\Filesystem\\Filesystem` class is the unique -endpoint for filesystem operations:: +The component contains two main classes called :class:`Symfony\\Component\\Filesystem\\Filesystem` +and :class:`Symfony\\Component\\Filesystem\\Path`:: + use Symfony\Component\Filesystem\Exception\IOExceptionInterface; use Symfony\Component\Filesystem\Filesystem; - use Symfony\Component\Filesystem\Exception\IOException; + use Symfony\Component\Filesystem\Path; - $fs = new Filesystem(); + $filesystem = new Filesystem(); try { - $fs->mkdir('/tmp/random/dir/' . mt_rand()); - } catch (IOException $e) { - echo "An error occurred while creating your directory"; + $filesystem->mkdir( + Path::normalize(sys_get_temp_dir().'/'.random_int(0, 1000)), + ); + } catch (IOExceptionInterface $exception) { + echo "An error occurred while creating your directory at ".$exception->getPath(); } -.. note:: +Filesystem Utilities +-------------------- - Methods :method:`Symfony\\Component\\Filesystem\\Filesystem::mkdir`, - :method:`Symfony\\Component\\Filesystem\\Filesystem::exists`, - :method:`Symfony\\Component\\Filesystem\\Filesystem::touch`, - :method:`Symfony\\Component\\Filesystem\\Filesystem::remove`, - :method:`Symfony\\Component\\Filesystem\\Filesystem::chmod`, - :method:`Symfony\\Component\\Filesystem\\Filesystem::chown` and - :method:`Symfony\\Component\\Filesystem\\Filesystem::chgrp` can receive a - string, an array or any object implementing :phpclass:`Traversable` as - the target argument. +``mkdir`` +~~~~~~~~~ +:method:`Symfony\\Component\\Filesystem\\Filesystem::mkdir` creates a directory recursively. +On POSIX filesystems, directories are created with a default mode value +``0777``. You can use the second argument to set your own mode:: -Mkdir -~~~~~ - -Mkdir creates directory. On posix filesystems, directories are created with a -default mode value `0777`. You can use the second argument to set your own mode:: - - $fs->mkdir('/tmp/photos', 0700); + $filesystem->mkdir('/tmp/photos', 0700); .. note:: You can pass an array or any :phpclass:`Traversable` object as the first argument. -Exists -~~~~~~ +.. note:: -Exists checks for the presence of all files or directories and returns false if a -file is missing:: + This function ignores already existing directories. - // this directory exists, return true - $fs->exists('/tmp/photos'); +.. note:: + + The directory permissions are affected by the current `umask`_. + Set the ``umask`` for your webserver, use PHP's :phpfunction:`umask` + function or use the :phpfunction:`chmod` function after the + directory has been created. + +``exists`` +~~~~~~~~~~ - // rabbit.jpg exists, bottle.png does not exists, return false - $fs->exists(array('rabbit.jpg', 'bottle.png')); +:method:`Symfony\\Component\\Filesystem\\Filesystem::exists` checks for the +presence of one or more files or directories and returns ``false`` if any of +them is missing:: + + // if this absolute directory exists, returns true + $filesystem->exists('/tmp/photos'); + + // if rabbit.jpg exists and bottle.png does not exist, returns false + // non-absolute paths are relative to the directory where the running PHP script is stored + $filesystem->exists(['rabbit.jpg', 'bottle.png']); .. note:: You can pass an array or any :phpclass:`Traversable` object as the first argument. -Copy -~~~~ +``copy`` +~~~~~~~~ -This method is used to copy files. If the target already exists, the file is -copied only if the source modification date is later than the target. This -behavior can be overridden by the third boolean argument:: +:method:`Symfony\\Component\\Filesystem\\Filesystem::copy` makes a copy of a +single file (use :method:`Symfony\\Component\\Filesystem\\Filesystem::mirror` to +copy directories). If the target already exists, the file is copied only if the +source modification date is later than the target. This behavior can be overridden +by the third boolean argument:: // works only if image-ICC has been modified after image.jpg - $fs->copy('image-ICC.jpg', 'image.jpg'); + $filesystem->copy('image-ICC.jpg', 'image.jpg'); // image.jpg will be overridden - $fs->copy('image-ICC.jpg', 'image.jpg', true); + $filesystem->copy('image-ICC.jpg', 'image.jpg', true); -Touch -~~~~~ +``touch`` +~~~~~~~~~ -Touch sets access and modification time for a file. The current time is used by -default. You can set your own with the second argument. The third argument is -the access time:: +:method:`Symfony\\Component\\Filesystem\\Filesystem::touch` sets access and +modification time for a file. The current time is used by default. You can set +your own with the second argument. The third argument is the access time:: - // set modification time to the current timestamp - $fs->touch('file.txt'); - // set modification time 10 seconds in the future - $fs->touch('file.txt', time() + 10); - // set access time 10 seconds in the past - $fs->touch('file.txt', time(), time() - 10); + // sets modification time to the current timestamp + $filesystem->touch('file.txt'); + // sets modification time 10 seconds in the future + $filesystem->touch('file.txt', time() + 10); + // sets access time 10 seconds in the past + $filesystem->touch('file.txt', time(), time() - 10); .. note:: You can pass an array or any :phpclass:`Traversable` object as the first argument. -Chown -~~~~~ +``chown`` +~~~~~~~~~ -Chown is used to change the owner of a file. The third argument is a boolean -recursive option:: +:method:`Symfony\\Component\\Filesystem\\Filesystem::chown` changes the owner of +a file. The third argument is a boolean recursive option:: - // set the owner of the lolcat video to www-data - $fs->chown('lolcat.mp4', 'www-data'); - // change the owner of the video directory recursively - $fs->chown('/video', 'www-data', true); + // sets the owner of the lolcat video to www-data + $filesystem->chown('lolcat.mp4', 'www-data'); + // changes the owner of the video directory recursively + $filesystem->chown('/video', 'www-data', true); .. note:: You can pass an array or any :phpclass:`Traversable` object as the first argument. -Chgrp -~~~~~ - -Chgrp is used to change the group of a file. The third argument is a boolean -recursive option:: +``chgrp`` +~~~~~~~~~ - // set the group of the lolcat video to nginx - $fs->chgrp('lolcat.mp4', 'nginx'); - // change the group of the video directory recursively - $fs->chgrp('/video', 'nginx', true); +:method:`Symfony\\Component\\Filesystem\\Filesystem::chgrp` changes the group of +a file. The third argument is a boolean recursive option:: + // sets the group of the lolcat video to nginx + $filesystem->chgrp('lolcat.mp4', 'nginx'); + // changes the group of the video directory recursively + $filesystem->chgrp('/video', 'nginx', true); .. note:: You can pass an array or any :phpclass:`Traversable` object as the first argument. -Chmod -~~~~~ +``chmod`` +~~~~~~~~~ -Chmod is used to change the mode of a file. The fourth argument is a boolean -recursive option:: +:method:`Symfony\\Component\\Filesystem\\Filesystem::chmod` changes the mode or +permissions of a file. The fourth argument is a boolean recursive option:: - // set the mode of the video to 0600 - $fs->chmod('video.ogg', 0600); - // change the mod of the src directory recursively - $fs->chmod('src', 0700, 0000, true); + // sets the mode of the video to 0600 + $filesystem->chmod('video.ogg', 0600); + // changes the mode of the src directory recursively + $filesystem->chmod('src', 0700, 0000, true); .. note:: You can pass an array or any :phpclass:`Traversable` object as the first argument. -Remove -~~~~~~ +``remove`` +~~~~~~~~~~ -Remove let's you remove files, symlink, directories easily:: +:method:`Symfony\\Component\\Filesystem\\Filesystem::remove` deletes files, +directories and symlinks:: - $fs->remove(array('symlink', '/path/to/directory', 'activity.log')); + $filesystem->remove(['symlink', '/path/to/directory', 'activity.log']); .. note:: You can pass an array or any :phpclass:`Traversable` object as the first argument. -Rename -~~~~~~ +``rename`` +~~~~~~~~~~ -Rename is used to rename files and directories:: +:method:`Symfony\\Component\\Filesystem\\Filesystem::rename` changes the name +of a single file or directory:: - //rename a file - $fs->rename('/tmp/processed_video.ogg', '/path/to/store/video_647.ogg'); - //rename a directory - $fs->rename('/tmp/files', '/path/to/store/files'); + // renames a file + $filesystem->rename('/tmp/processed_video.ogg', '/path/to/store/video_647.ogg'); + // renames a directory + $filesystem->rename('/tmp/files', '/path/to/store/files'); + // if the target already exists, a third boolean argument is available to overwrite. + $filesystem->rename('/tmp/processed_video2.ogg', '/path/to/store/video_647.ogg', true); -symlink -~~~~~~~ +``symlink`` +~~~~~~~~~~~ -Creates a symbolic link from the target to the destination. If the filesystem -does not support symbolic links, a third boolean argument is available:: +:method:`Symfony\\Component\\Filesystem\\Filesystem::symlink` creates a +symbolic link from the target to the destination. If the filesystem does not +support symbolic links, a third boolean argument is available:: - // create a symbolic link - $fs->symlink('/path/to/source', '/path/to/destination'); - // duplicate the source directory if the filesystem + // creates a symbolic link + $filesystem->symlink('/path/to/source', '/path/to/destination'); + // duplicates the source directory if the filesystem // does not support symbolic links - $fs->symlink('/path/to/source', '/path/to/destination', true); + $filesystem->symlink('/path/to/source', '/path/to/destination', true); -makePathRelative -~~~~~~~~~~~~~~~~ +``readlink`` +~~~~~~~~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::readlink` read links targets. + +The :method:`Symfony\\Component\\Filesystem\\Filesystem::readlink` method +provided by the Filesystem component behaves in the same way on all operating +systems (unlike PHP's :phpfunction:`readlink` function):: + + // returns the next direct target of the link without considering the existence of the target + $filesystem->readlink('/path/to/link'); + + // returns its absolute fully resolved final version of the target (if there are nested links, they are resolved) + $filesystem->readlink('/path/to/link', true); + +Its behavior is the following: + +* When ``$canonicalize`` is ``false``: + + * if ``$path`` does not exist or is not a link, it returns ``null``. + * if ``$path`` is a link, it returns the next direct target of the link without considering the existence of the target. + +* When ``$canonicalize`` is ``true``: + + * if ``$path`` does not exist, it returns null. + * if ``$path`` exists, it returns its absolute fully resolved final version. + +.. note:: -Return the relative path of a directory given another one:: + If you wish to canonicalize the path without checking its existence, you can + use :method:`Symfony\\Component\\Filesystem\\Path::canonicalize` method instead. + +``makePathRelative`` +~~~~~~~~~~~~~~~~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::makePathRelative` takes two +absolute paths and returns the relative path from the second path to the first one:: // returns '../' - $fs->makePathRelative( + $filesystem->makePathRelative( '/var/lib/symfony/src/Symfony/', '/var/lib/symfony/src/Symfony/Component' ); - // returns 'videos' - $fs->makePathRelative('/tmp', '/tmp/videos'); + // returns 'videos/' + $filesystem->makePathRelative('/tmp/videos', '/tmp'); + +``mirror`` +~~~~~~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::mirror` copies all the +contents of the source directory into the target one (use the +:method:`Symfony\\Component\\Filesystem\\Filesystem::copy` method to copy single +files):: + + $filesystem->mirror('/path/to/source', '/path/to/target'); + +``isAbsolutePath`` +~~~~~~~~~~~~~~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::isAbsolutePath` returns +``true`` if the given path is absolute, ``false`` otherwise:: + + // returns true + $filesystem->isAbsolutePath('/tmp'); + // returns true + $filesystem->isAbsolutePath('c:\\Windows'); + // returns false + $filesystem->isAbsolutePath('tmp'); + // returns false + $filesystem->isAbsolutePath('../dir'); + +``tempnam`` +~~~~~~~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::tempnam` creates a +temporary file with a unique filename, and returns its path, or throw an +exception on failure:: + + // returns a path like : /tmp/prefix_wyjgtF + $filesystem->tempnam('/tmp', 'prefix_'); + // returns a path like : /tmp/prefix_wyjgtF.png + $filesystem->tempnam('/tmp', 'prefix_', '.png'); + +.. _filesystem-dumpfile: + +``dumpFile`` +~~~~~~~~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::dumpFile` saves the given +contents into a file (creating the file and its directory if they don't exist). +It does this in an atomic manner: it writes a temporary file first and then moves +it to the new file location when it's finished. This means that the user will +always see either the complete old file or complete new file (but never a +partially-written file):: + + $filesystem->dumpFile('file.txt', 'Hello World'); + +The ``file.txt`` file contains ``Hello World`` now. + +``appendToFile`` +~~~~~~~~~~~~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::appendToFile` adds new +contents at the end of some file:: + + $filesystem->appendToFile('logs.txt', 'Email sent to user@example.com'); + // the third argument tells whether the file should be locked when writing to it + $filesystem->appendToFile('logs.txt', 'Email sent to user@example.com', true); + +If either the file or its containing directory doesn't exist, this method +creates them before appending the contents. + +``readFile`` +~~~~~~~~~~~~ + +.. versionadded:: 7.1 + + The ``readFile()`` method was introduced in Symfony 7.1. + +:method:`Symfony\\Component\\Filesystem\\Filesystem::readFile` returns all the +contents of a file as a string. Unlike the :phpfunction:`file_get_contents` function +from PHP, it throws an exception when the given file path is not readable and +when passing the path to a directory instead of a file:: + + $contents = $filesystem->readFile('/some/path/to/file.txt'); + +The ``$contents`` variable now stores all the contents of the ``file.txt`` file. + +Path Manipulation Utilities +--------------------------- + +Dealing with file paths usually involves some difficulties: + +- Platform differences: file paths look different on different platforms. UNIX + file paths start with a slash ("/"), while Windows file paths start with a + system drive ("C:"). UNIX uses forward slashes, while Windows uses backslashes + by default. +- Absolute/relative paths: web applications frequently need to deal with absolute + and relative paths. Converting one to the other properly is tricky and repetitive. + +:class:`Symfony\\Component\\Filesystem\\Path` provides utility methods to tackle +those issues. + +Canonicalization +~~~~~~~~~~~~~~~~ + +Returns the shortest path name equivalent to the given path. It applies the +following rules iteratively until no further processing can be done: + +- "." segments are removed; +- ".." segments are resolved; +- backslashes ("\\") are converted into forward slashes ("/"); +- root paths ("/" and "C:/") always terminate with a slash; +- non-root paths never terminate with a slash; +- schemes (such as "phar://") are kept; +- replace ``~`` with the user's home directory. + +You can canonicalize a path with :method:`Symfony\\Component\\Filesystem\\Path::canonicalize`:: + + echo Path::canonicalize('/var/www/vhost/webmozart/../config.ini'); + // => /var/www/vhost/config.ini + +You can pass absolute paths and relative paths to the +:method:`Symfony\\Component\\Filesystem\\Path::canonicalize` method. When a +relative path is passed, ".." segments at the beginning of the path are kept:: + + echo Path::canonicalize('../uploads/../config/config.yaml'); + // => ../config/config.yaml + +Malformed paths are returned unchanged:: + + echo Path::canonicalize('C:Programs/PHP/php.ini'); + // => C:Programs/PHP/php.ini + +Converting Absolute/Relative Paths +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Absolute/relative paths can be converted with the methods +:method:`Symfony\\Component\\Filesystem\\Path::makeAbsolute` +and :method:`Symfony\\Component\\Filesystem\\Path::makeRelative`. + +:method:`Symfony\\Component\\Filesystem\\Path::makeAbsolute` method expects a +relative path and a base path to base that relative path upon:: + + echo Path::makeAbsolute('config/config.yaml', '/var/www/project'); + // => /var/www/project/config/config.yaml + +If an absolute path is passed in the first argument, the absolute path is +returned unchanged:: + + echo Path::makeAbsolute('/usr/share/lib/config.ini', '/var/www/project'); + // => /usr/share/lib/config.ini + +The method resolves ".." segments, if there are any:: + + echo Path::makeAbsolute('../config/config.yaml', '/var/www/project/uploads'); + // => /var/www/project/config/config.yaml + +This method is very useful if you want to be able to accept relative paths (for +example, relative to the root directory of your project) and absolute paths at +the same time. + +:method:`Symfony\\Component\\Filesystem\\Path::makeRelative` is the inverse +operation to :method:`Symfony\\Component\\Filesystem\\Path::makeAbsolute`:: + + echo Path::makeRelative('/var/www/project/config/config.yaml', '/var/www/project'); + // => config/config.yaml + +If the path is not within the base path, the method will prepend ".." segments +as necessary:: + + echo Path::makeRelative('/var/www/project/config/config.yaml', '/var/www/project/uploads'); + // => ../config/config.yaml + +Use :method:`Symfony\\Component\\Filesystem\\Path::isAbsolute` and +:method:`Symfony\\Component\\Filesystem\\Path::isRelative` to check whether a +path is absolute or relative:: + + Path::isAbsolute('C:\Programs\PHP\php.ini') + // => true + +All four methods internally canonicalize the passed path. + +Finding Longest Common Base Paths +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you store absolute file paths on the file system, this leads to a lot of +duplicated information:: + + return [ + '/var/www/vhosts/project/httpdocs/config/config.yaml', + '/var/www/vhosts/project/httpdocs/config/routing.yaml', + '/var/www/vhosts/project/httpdocs/config/services.yaml', + '/var/www/vhosts/project/httpdocs/images/banana.gif', + '/var/www/vhosts/project/httpdocs/uploads/images/nicer-banana.gif', + ]; + +Especially when storing many paths, the amount of duplicated information is +noticeable. You can use :method:`Symfony\\Component\\Filesystem\\Path::getLongestCommonBasePath` +to check a list of paths for a common base path:: + + $basePath = Path::getLongestCommonBasePath( + '/var/www/vhosts/project/httpdocs/config/config.yaml', + '/var/www/vhosts/project/httpdocs/config/routing.yaml', + '/var/www/vhosts/project/httpdocs/config/services.yaml', + '/var/www/vhosts/project/httpdocs/images/banana.gif', + '/var/www/vhosts/project/httpdocs/uploads/images/nicer-banana.gif' + ); + // => /var/www/vhosts/project/httpdocs + +Use this common base path to shorten the stored paths:: + + return [ + $basePath.'/config/config.yaml', + $basePath.'/config/routing.yaml', + $basePath.'/config/services.yaml', + $basePath.'/images/banana.gif', + $basePath.'/uploads/images/nicer-banana.gif', + ]; + +:method:`Symfony\\Component\\Filesystem\\Path::getLongestCommonBasePath` always +returns canonical paths. + +Use :method:`Symfony\\Component\\Filesystem\\Path::isBasePath` to test whether a +path is a base path of another path:: + + Path::isBasePath("/var/www", "/var/www/project"); + // => true + + Path::isBasePath("/var/www", "/var/www/project/.."); + // => true + + Path::isBasePath("/var/www", "/var/www/project/../.."); + // => false + +Finding Directories/Root Directories +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PHP offers the function :phpfunction:`dirname` to obtain the directory path of a +file path. This method has a few quirks:: -mirror -~~~~~~ +- ``dirname()`` does not accept backslashes on UNIX +- ``dirname("C:/Programs")`` returns "C:", not "C:/" +- ``dirname("C:/")`` returns ".", not "C:/" +- ``dirname("C:")`` returns ".", not "C:/" +- ``dirname("Programs")`` returns ".", not "" +- ``dirname()`` does not canonicalize the result -Mirrors a directory:: +:method:`Symfony\\Component\\Filesystem\\Path::getDirectory` fixes these +shortcomings:: - $fs->mirror('/path/to/source', '/path/to/target'); + echo Path::getDirectory("C:\Programs"); + // => C:/ -isAbsolutePath -~~~~~~~~~~~~~~ +Additionally, you can use :method:`Symfony\\Component\\Filesystem\\Path::getRoot` +to obtain the root of a path:: -isAbsolutePath returns true if the given path is absolute, false otherwise:: + echo Path::getRoot("/etc/apache2/sites-available"); + // => / - // return true - $fs->isAbsolutePath('/tmp'); - // return true - $fs->isAbsolutePath('c:\\Windows'); - // return false - $fs->isAbsolutePath('tmp'); - // return false - $fs->isAbsolutePath('../dir'); + echo Path::getRoot("C:\Programs\Apache\Config"); + // => C:/ Error Handling -------------- Whenever something wrong happens, an exception implementing -:class:`Symfony\\Component\\Filesystem\\Exception\\ExceptionInterface` is -thrown. +:class:`Symfony\\Component\\Filesystem\\Exception\\ExceptionInterface` or +:class:`Symfony\\Component\\Filesystem\\Exception\\IOExceptionInterface` is thrown. .. note:: - Prior to version 2.1, :method:`Symfony\\Component\\Filesystem\\Filesystem::mkdir` - returned a boolean and did not throw exceptions. As of 2.1, a - :class:`Symfony\\Component\\Filesystem\\Exception\\IOException` is - thrown if a directory creation fails. + 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 ab1cda43e82..cecc597ac64 100644 --- a/components/finder.rst +++ b/components/finder.rst @@ -1,20 +1,17 @@ -.. index:: - single: Finder - single: Components; Finder - The Finder Component ==================== - The Finder Component finds files and directories via an intuitive fluent - interface. + The Finder component finds files and directories based on different criteria + (name, file size, modification time, etc.) via an intuitive fluent interface. Installation ------------ -You can install the component in many different ways: +.. code-block:: terminal + + $ composer require symfony/finder -* Use the official Git repository (https://github.com/symfony/Finder); -* :doc:`Install it via Composer` (``symfony/finder`` on `Packagist`_). +.. include:: /components/require_autoload.rst.inc Usage ----- @@ -25,49 +22,36 @@ directories:: use Symfony\Component\Finder\Finder; $finder = new Finder(); + // find all files in the current directory $finder->files()->in(__DIR__); - foreach ($finder as $file) { - // Print the absolute path - print $file->getRealpath()."\n"; - - // Print the relative path to the file, omitting the filename - print $file->getRelativePath()."\n"; - - // Print the relative path to the file - print $file->getRelativePathname()."\n"; + // check if there are any search results + if ($finder->hasResults()) { + // ... } -The ``$file`` is an instance of :class:`Symfony\\Component\\Finder\\SplFileInfo` -which extends :phpclass:`SplFileInfo` to provide methods to work with relative -paths. - -The above code prints the names of all the files in the current directory -recursively. The Finder class uses a fluent interface, so all methods return -the Finder instance. + foreach ($finder as $file) { + $absoluteFilePath = $file->getRealPath(); + $fileNameWithExtension = $file->getRelativePathname(); -.. tip:: + // ... + } - A Finder instance is a PHP :phpclass:`Iterator`. So, instead of iterating over the - Finder with ``foreach``, you can also convert it to an array with the - :phpfunction:`iterator_to_array` method, or get the number of items with - :phpfunction:`iterator_count`. +The ``$file`` variable is an instance of +:class:`Symfony\\Component\\Finder\\SplFileInfo` which extends PHP's own +:phpclass:`SplFileInfo` to provide methods to work with relative paths. -.. caution:: +.. warning:: - When searching through multiple locations passed to the - :method:`Symfony\\Component\\Finder\\Finder::in` method, a separate iterator - is created internally for every location. This means we have multiple result - sets aggregated into one. - Since :phpfunction:`iterator_to_array` uses keys of result sets by default, - when converting to an array, some keys might be duplicated and their values - overwritten. This can be avoided by passing ``false`` as a second parameter - to :phpfunction:`iterator_to_array`. + The ``Finder`` object doesn't reset its internal state automatically. + This means that you need to create a new instance if you do not want + to get mixed results. -Criteria --------- +Searching for Files and Directories +----------------------------------- -There are lots of ways to filter and sort your results. +The component provides lots of methods to define the search criteria. They all +can be chained because they implement a `fluent interface`_. Location ~~~~~~~~ @@ -80,112 +64,154 @@ directory to use for the search:: Search in several locations by chaining calls to :method:`Symfony\\Component\\Finder\\Finder::in`:: - $finder->files()->in(__DIR__)->in('/elsewhere'); + // search inside *both* directories + $finder->in([__DIR__, '/elsewhere']); -.. versionadded:: 2.2 - Wildcard support was added in version 2.2. + // same as above + $finder->in(__DIR__)->in('/elsewhere'); -Use wildcard characters to search in the directories matching a pattern:: +Use ``*`` as a wildcard character to search in the directories matching a +pattern (each pattern has to resolve to at least one directory path):: $finder->in('src/Symfony/*/*/Resources'); -Each pattern has to resolve to at least one directory path. - Exclude directories from matching with the :method:`Symfony\\Component\\Finder\\Finder::exclude` method:: + // directories passed as argument must be relative to the ones defined with the in() method $finder->in(__DIR__)->exclude('ruby'); +It's also possible to ignore directories that you don't have permission to read:: + + $finder->ignoreUnreadableDirs()->in(__DIR__); + As the Finder uses PHP iterators, you can pass any URL with a supported -`protocol`_:: +`PHP wrapper for URL-style protocols`_ (``ftp://``, ``zlib://``, etc.):: + + // always add a trailing slash when looking for in the FTP root dir + $finder->in('ftp://example.com/'); + // you can also look for in a FTP directory $finder->in('ftp://example.com/pub/'); And it also works with user-defined streams:: use Symfony\Component\Finder\Finder; - $s3 = new \Zend_Service_Amazon_S3($key, $secret); - $s3->registerStreamWrapper("s3"); + // register a 's3://' wrapper with the official AWS SDK + $s3Client = new Aws\S3\S3Client([/* config options */]); + $s3Client->registerStreamWrapper(); $finder = new Finder(); $finder->name('photos*')->size('< 100K')->date('since 1 hour ago'); foreach ($finder->in('s3://bucket-name') as $file) { - // ... do something - - print $file->getFilename()."\n"; + // ... do something with the file } -.. note:: +.. seealso:: - Read the `Streams`_ documentation to learn how to create your own streams. + Read the `PHP streams`_ documentation to learn how to create your own streams. Files or Directories ~~~~~~~~~~~~~~~~~~~~ -By default, the Finder returns files and directories; but the -:method:`Symfony\\Component\\Finder\\Finder::files` and -:method:`Symfony\\Component\\Finder\\Finder::directories` methods control that:: +By default, the Finder returns both files and directories. If you need to find either files or directories only, use the :method:`Symfony\\Component\\Finder\\Finder::files` and :method:`Symfony\\Component\\Finder\\Finder::directories` methods:: + // look for files only; ignore directories $finder->files(); + // look for directories only; ignore files $finder->directories(); -If you want to follow links, use the ``followLinks()`` method:: +If you want to follow `symbolic links`_, use the ``followLinks()`` method:: $finder->files()->followLinks(); -By default, the iterator ignores popular VCS files. This can be changed with -the ``ignoreVCS()`` method:: +Note that this method follows links but it doesn't resolve them. Consider +the following structure of files of directories: - $finder->ignoreVCS(false); - -Sorting -~~~~~~~ - -Sort the result by name or by type (directories first, then files):: +.. code-block:: text - $finder->sortByName(); + ├── folder1/ + │ ├──file1.txt + │ ├── file2link (symbolic link to folder2/file2.txt file) + │ └── folder3link (symbolic link to folder3/ directory) + ├── folder2/ + │ └── file2.txt + └── folder3/ + └── file3.txt + +If you try to find all files in ``folder1/`` via ``$finder->files()->in('/path/to/folder1/')`` +you'll get the following results: + +* When **not** using the ``followLinks()`` method: ``file1.txt`` and ``file2link`` + (this link is not resolved). The ``folder3link`` doesn't appear in the results + because it's not followed or resolved; +* When using the ``followLinks()`` method: ``file1.txt``, ``file2link`` (this link + is still not resolved) and ``folder3/file3.txt`` (this file appears in the results + because the ``folder1/folder3link`` link was followed). + +Version Control Files +~~~~~~~~~~~~~~~~~~~~~ + +`Version Control Systems`_ (or "VCS" for short), such as Git and Mercurial, +create some special files to store their metadata. Those files are ignored by +default when looking for files and directories, but you can change this with the +``ignoreVCS()`` method:: - $finder->sortByType(); + $finder->ignoreVCS(false); -.. note:: +If the search directory and its subdirectories contain ``.gitignore`` files, you +can reuse those rules to exclude files and directories from the results with the +:method:`Symfony\\Component\\Finder\\Finder::ignoreVCSIgnored` method:: - Notice that the ``sort*`` methods need to get all matching elements to do - their jobs. For large iterators, it is slow. + // excludes files/directories matching the .gitignore patterns + $finder->ignoreVCSIgnored(true); -You can also define your own sorting algorithm with ``sort()`` method:: +The rules of a directory always override the rules of its parent directories. - $sort = function (\SplFileInfo $a, \SplFileInfo $b) - { - return strcmp($a->getRealpath(), $b->getRealpath()); - }; +.. note:: - $finder->sort($sort); + Git looks for ``.gitignore`` files starting from the repository root directory. + Symfony's Finder behavior is different and it looks for ``.gitignore`` files + starting from the directory used to search files/directories. To be consistent + with Git behavior, you should explicitly search from the Git repository root. File Name ~~~~~~~~~ -Restrict files by name with the +Find files by name with the :method:`Symfony\\Component\\Finder\\Finder::name` method:: $finder->files()->name('*.php'); -The ``name()`` method accepts globs, strings, or regexes:: +The ``name()`` method accepts globs, strings, regexes or an array of globs, +strings or regexes:: $finder->files()->name('/\.php$/'); +Multiple filenames can be defined by chaining calls or passing an array:: + + $finder->files()->name('*.php')->name('*.twig'); + + // same as above + $finder->files()->name(['*.php', '*.twig']); + The ``notName()`` method excludes files matching a pattern:: $finder->files()->notName('*.rb'); +Multiple filenames can be excluded by chaining calls or passing an array:: + + $finder->files()->notName('*.rb')->notName('*.py'); + + // same as above + $finder->files()->notName(['*.rb', '*.py']); + File Contents ~~~~~~~~~~~~~ -.. versionadded:: 2.1 - The ``contains()`` and ``notContains()`` methods were added in version 2.1 - -Restrict files by contents with the +Find files by content with the :method:`Symfony\\Component\\Finder\\Finder::contains` method:: $finder->files()->contains('lorem ipsum'); @@ -201,50 +227,69 @@ The ``notContains()`` method excludes files containing given pattern:: Path ~~~~ -.. versionadded:: 2.2 - The ``path()`` and ``notPath()`` methods were added in version 2.2. - -Restrict files and directories by path with the +Find files and directories by path with the :method:`Symfony\\Component\\Finder\\Finder::path` method:: - $finder->path('some/special/dir'); + // matches files that contain "data" anywhere in their paths (files or directories) + $finder->path('data'); + // for example this will match data/*.xml and data.xml if they exist + $finder->path('data')->name('*.xml'); -On all platforms slash (i.e. ``/``) should be used as the directory separator. +Use the forward slash (i.e. ``/``) as the directory separator on all platforms, +including Windows. The component makes the necessary conversion internally. -The ``path()`` method accepts a string or a regular expression:: +The ``path()`` method accepts a string, a regular expression or an array of +strings or regular expressions:: $finder->path('foo/bar'); $finder->path('/^foo\/bar/'); +Multiple paths can be defined by chaining calls or passing an array:: + + $finder->path('data')->path('foo/bar'); + + // same as above + $finder->path(['data', 'foo/bar']); + Internally, strings are converted into regular expressions by escaping slashes and adding delimiters: -.. code-block:: text - - dirname ===> /dirname/ - a/b/c ===> /a\/b\/c/ +===================== ======================= +Original Given String Regular Expression Used +===================== ======================= +``dirname`` ``/dirname/`` +``a/b/c`` ``/a\/b\/c/`` +===================== ======================= -The :method:`Symfony\\Component\\Finder\\Finder::notPath` method excludes files by path:: +The :method:`Symfony\\Component\\Finder\\Finder::notPath` method excludes files +by path:: $finder->notPath('other/dir'); +Multiple paths can be excluded by chaining calls or passing an array:: + + $finder->notPath('first/dir')->notPath('other/dir'); + + // same as above + $finder->notPath(['first/dir', 'other/dir']); + File Size ~~~~~~~~~ -Restrict files by size with the +Find files by size with the :method:`Symfony\\Component\\Finder\\Finder::size` method:: $finder->files()->size('< 1.5K'); -Restrict by a size range by chaining calls:: +Restrict by a size range by chaining calls or passing an array:: $finder->files()->size('>= 1K')->size('<= 2K'); -The comparison operator can be any of the following: ``>``, ``>=``, ``<``, ``<=``, -``==``, ``!=``. + // same as above + $finder->files()->size(['>= 1K', '<= 2K']); -.. versionadded:: 2.1 - The operator ``!=`` was added in version 2.1. +The comparison operator can be any of the following: ``>``, ``>=``, ``<``, +``<=``, ``==``, ``!=``. The target value may use magnitudes of kilobytes (``k``, ``ki``), megabytes (``m``, ``mi``), or gigabytes (``g``, ``gi``). Those suffixed with an ``i`` use @@ -253,30 +298,45 @@ the appropriate ``2**n`` version in accordance with the `IEC standard`_. File Date ~~~~~~~~~ -Restrict files by last modified dates with the +Find files by last modified dates with the :method:`Symfony\\Component\\Finder\\Finder::date` method:: $finder->date('since yesterday'); -The comparison operator can be any of the following: ``>``, ``>=``, ``<``, '<=', -'=='. You can also use ``since`` or ``after`` as an alias for ``>``, and -``until`` or ``before`` as an alias for ``<``. +Restrict by a date range by chaining calls or passing an array:: -The target value can be any date supported by the `strtotime`_ function. + $finder->date('>= 2018-01-01')->date('<= 2018-12-31'); + + // same as above + $finder->date(['>= 2018-01-01', '<= 2018-12-31']); + +The comparison operator can be any of the following: ``>``, ``>=``, ``<``, +``<=``, ``==``. You can also use ``since`` or ``after`` as an alias for ``>``, +and ``until`` or ``before`` as an alias for ``<``. + +The target value can be any date supported by :phpfunction:`strtotime`. Directory Depth ~~~~~~~~~~~~~~~ -By default, the Finder recursively traverse directories. Restrict the depth of +By default, the Finder recursively traverses directories. Restrict the depth of traversing with :method:`Symfony\\Component\\Finder\\Finder::depth`:: + // this will only consider files/directories which are direct children $finder->depth('== 0'); $finder->depth('< 3'); +Restrict by a depth range by chaining calls or passing an array:: + + $finder->depth('> 2')->depth('< 5'); + + // same as above + $finder->depth(['> 2', '< 5']); + Custom Filtering ~~~~~~~~~~~~~~~~ -To restrict the matching file with your own strategy, use +To filter results with your own strategy, use :method:`Symfony\\Component\\Finder\\Finder::filter`:: $filter = function (\SplFileInfo $file) @@ -293,11 +353,78 @@ it is called with the file as a :class:`Symfony\\Component\\Finder\\SplFileInfo` instance. The file is excluded from the result set if the Closure returns ``false``. -Reading contents of returned files -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The ``filter()`` method includes a second optional argument to prune directories. +If set to ``true``, this method completely skips the excluded directories instead +of traversing the entire file/directory structure and excluding them later. When +using a closure, return ``false`` for the directories which you want to prune. + +Pruning directories early can improve performance significantly depending on the +file/directory hierarchy complexity and the number of excluded directories. + +Sorting Results +--------------- + +Sort the results by name, extension, size or type (directories first, then files):: + + $finder->sortByName(); + $finder->sortByCaseInsensitiveName(); + $finder->sortByExtension(); + $finder->sortBySize(); + $finder->sortByType(); + +.. tip:: + + By default, the ``sortByName()`` method uses the :phpfunction:`strcmp` PHP + function (e.g. ``file1.txt``, ``file10.txt``, ``file2.txt``). Pass ``true`` + as its argument to use PHP's `natural sort order`_ algorithm instead (e.g. + ``file1.txt``, ``file2.txt``, ``file10.txt``). + + The ``sortByCaseInsensitiveName()`` method uses the case insensitive + :phpfunction:`strcasecmp` PHP function. Pass ``true`` as its argument to use + PHP's case insensitive `natural sort order`_ algorithm instead (i.e. the + :phpfunction:`strnatcasecmp` PHP function) + +Sort the files and directories by the last accessed, changed or modified time:: + + $finder->sortByAccessedTime(); + + $finder->sortByChangedTime(); -.. versionadded:: 2.1 - Method ``getContents()`` have been introduced in version 2.1. + $finder->sortByModifiedTime(); + +You can also define your own sorting algorithm with the ``sort()`` method:: + + $finder->sort(function (\SplFileInfo $a, \SplFileInfo $b): int { + return strcmp($a->getRealPath(), $b->getRealPath()); + }); + +You can reverse any sorting by using the ``reverseSorting()`` method:: + + // results will be sorted "Z to A" instead of the default "A to Z" + $finder->sortByName()->reverseSorting(); + +.. note:: + + Notice that the ``sort*`` methods need to get all matching elements to do + their jobs. For large iterators, it is slow. + +Transforming Results into Arrays +-------------------------------- + +A Finder instance is an :phpclass:`IteratorAggregate` PHP class. So, in addition +to iterating over the Finder results with ``foreach``, you can also convert it +to an array with the :phpfunction:`iterator_to_array` function, or get the +number of items with :phpfunction:`iterator_count`. + +If you call to the :method:`Symfony\\Component\\Finder\\Finder::in` method more +than once to search through multiple locations, pass ``false`` as a second +parameter to :phpfunction:`iterator_to_array` to avoid issues (a separate +iterator is created for each location and, if you don't pass ``false`` to +:phpfunction:`iterator_to_array`, keys of result sets are used and some of them +might be duplicated and their values overwritten). + +Reading Contents of Returned Files +---------------------------------- The contents of returned files can be read with :method:`Symfony\\Component\\Finder\\SplFileInfo::getContents`:: @@ -309,11 +436,14 @@ The contents of returned files can be read with foreach ($finder as $file) { $contents = $file->getContents(); - ... + + // ... } -.. _strtotime: http://www.php.net/manual/en/datetime.formats.php -.. _protocol: http://www.php.net/manual/en/wrappers.php -.. _Streams: http://www.php.net/streams -.. _IEC standard: http://physics.nist.gov/cuu/Units/binary.html -.. _Packagist: https://packagist.org/packages/symfony/finder +.. _`fluent interface`: https://en.wikipedia.org/wiki/Fluent_interface +.. _`symbolic links`: https://en.wikipedia.org/wiki/Symbolic_link +.. _`Version Control Systems`: https://en.wikipedia.org/wiki/Version_control +.. _`PHP wrapper for URL-style protocols`: https://www.php.net/manual/en/wrappers.php +.. _`PHP streams`: https://www.php.net/streams +.. _`IEC standard`: https://physics.nist.gov/cuu/Units/binary.html +.. _`natural sort order`: https://en.wikipedia.org/wiki/Natural_sort_order diff --git a/components/form.rst b/components/form.rst new file mode 100644 index 00000000000..44f407e4c8e --- /dev/null +++ b/components/form.rst @@ -0,0 +1,786 @@ +The Form Component +================== + + The Form component allows you to create, process and reuse forms. + +The Form component is a tool to help you solve the problem of allowing end-users +to interact with the data and modify the data in your application. And though +traditionally this has been through HTML forms, the component focuses on +processing data to and from your client and application, whether that data +be from a normal form post or from an API. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/form + +.. include:: /components/require_autoload.rst.inc + +Configuration +------------- + +.. seealso:: + + This article explains how to use the Form features as an independent + component in any PHP application. Read the :doc:`/forms` article to learn + about how to use it in Symfony applications. + +In Symfony, forms are represented by objects and these objects are built +by using a *form factory*. Building a form factory is done with the factory +method ``Forms::createFormFactory``:: + + use Symfony\Component\Form\Forms; + + $formFactory = Forms::createFormFactory(); + +This factory can already be used to create basic forms, but it is lacking +support for very important features: + +* **Request Handling:** Support for request handling and file uploads; +* **CSRF Protection:** Support for protection against Cross-Site-Request-Forgery + (CSRF) attacks; +* **Templating:** Integration with a templating layer that allows you to reuse + HTML fragments when rendering a form; +* **Translation:** Support for translating error messages, field labels and + other strings; +* **Validation:** Integration with a validation library to generate error + messages for submitted data. + +The Symfony Form component relies on other libraries to solve these problems. +Most of the time you will use Twig and the Symfony +:doc:`HttpFoundation `, +:doc:`Translation ` and :doc:`Validator ` +components, but you can replace any of these with a different library of your choice. + +The following sections explain how to plug these libraries into the form +factory. + +.. tip:: + + For a working example, see https://github.com/webmozart/standalone-forms + +Request Handling +~~~~~~~~~~~~~~~~ + +To process form data, you'll need to call the :method:`Symfony\\Component\\Form\\Form::handleRequest` +method:: + + $form->handleRequest(); + +Behind the scenes, this uses a :class:`Symfony\\Component\\Form\\NativeRequestHandler` +object to read data off of the correct PHP superglobals (i.e. ``$_POST`` or +``$_GET``) based on the HTTP method configured on the form (POST is default). + +.. seealso:: + + If you need more control over exactly when your form is submitted or which + data is passed to it, + :doc:`use the submit() method to handle form submissions `. + +.. sidebar:: Integration with the HttpFoundation Component + + If you use the HttpFoundation component, then you should add the + :class:`Symfony\\Component\\Form\\Extension\\HttpFoundation\\HttpFoundationExtension` + to your form factory:: + + use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension; + use Symfony\Component\Form\Forms; + + $formFactory = Forms::createFormFactoryBuilder() + ->addExtension(new HttpFoundationExtension()) + ->getFormFactory(); + + Now, when you process a form, you can pass the :class:`Symfony\\Component\\HttpFoundation\\Request` + object to :method:`Symfony\\Component\\Form\\Form::handleRequest`:: + + $form->handleRequest($request); + + .. note:: + + For more information about the HttpFoundation component or how to + install it, see :doc:`/components/http_foundation`. + +CSRF Protection +~~~~~~~~~~~~~~~ + +Protection against CSRF attacks is built into the Form component, but you need +to explicitly enable it or replace it with a custom solution. If you want to +use the built-in support, first install the Security CSRF component: + +.. code-block:: terminal + + $ composer require symfony/security-csrf + +The following snippet adds CSRF protection to the form factory:: + + use Symfony\Component\Form\Extension\Csrf\CsrfExtension; + use Symfony\Component\Form\Forms; + use Symfony\Component\HttpFoundation\RequestStack; + use Symfony\Component\Security\Csrf\CsrfTokenManager; + use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator; + use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage; + + // creates a RequestStack object using the current request + $requestStack = new RequestStack([$request]); + + $csrfGenerator = new UriSafeTokenGenerator(); + $csrfStorage = new SessionTokenStorage($requestStack); + $csrfManager = new CsrfTokenManager($csrfGenerator, $csrfStorage); + + $formFactory = Forms::createFormFactoryBuilder() + // ... + ->addExtension(new CsrfExtension($csrfManager)) + ->getFormFactory(); + +.. versionadded:: 7.2 + + Support for passing requests to the constructor of the ``RequestStack`` + class was introduced in Symfony 7.2. + +Internally, this extension will automatically add a hidden field to every +form (called ``_token`` by default) whose value is automatically generated by +the CSRF generator and validated when binding the form. + +.. tip:: + + If you're not using the HttpFoundation component, you can use + :class:`Symfony\\Component\\Security\\Csrf\\TokenStorage\\NativeSessionTokenStorage` + instead, which relies on PHP's native session handling:: + + use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage; + + $csrfStorage = new NativeSessionTokenStorage(); + // ... + +You can disable CSRF protection per form using the ``csrf_protection`` option:: + + use Symfony\Component\Form\Extension\Core\Type\FormType; + + $form = $formFactory->createBuilder(FormType::class, null, ['csrf_protection' => false]) + ->getForm(); + +Twig Templating +~~~~~~~~~~~~~~~ + +If you're using the Form component to process HTML forms, you'll need a way to +render your form as HTML form fields (complete with field values, errors, and +labels). If you use `Twig`_ as your template engine, the Form component offers a +rich integration. + +To use the integration, you'll need the twig bridge, which provides integration +between Twig and several Symfony components: + +.. code-block:: terminal + + $ composer require symfony/twig-bridge + +The TwigBridge integration provides you with several +:ref:`Twig Functions ` +that help you render the HTML widget, label, help and errors for each field +(as well as a few other things). To configure the integration, you'll need +to bootstrap or access Twig and add the :class:`Symfony\\Bridge\\Twig\\Extension\\FormExtension`:: + + use Symfony\Bridge\Twig\Extension\FormExtension; + use Symfony\Bridge\Twig\Form\TwigRendererEngine; + use Symfony\Component\Form\FormRenderer; + use Symfony\Component\Form\Forms; + use Twig\Environment; + use Twig\Loader\FilesystemLoader; + use Twig\RuntimeLoader\FactoryRuntimeLoader; + + // the Twig file that holds all the default markup for rendering forms + // this file comes with TwigBridge + $defaultFormTheme = 'form_div_layout.html.twig'; + + $vendorDirectory = realpath(__DIR__.'/../vendor'); + // the path to TwigBridge library so Twig can locate the + // form_div_layout.html.twig file + $appVariableReflection = new \ReflectionClass('\Symfony\Bridge\Twig\AppVariable'); + $vendorTwigBridgeDirectory = dirname($appVariableReflection->getFileName()); + // the path to your other templates + $viewsDirectory = realpath(__DIR__.'/../views'); + + $twig = new Environment(new FilesystemLoader([ + $viewsDirectory, + $vendorTwigBridgeDirectory.'/Resources/views/Form', + ])); + $formEngine = new TwigRendererEngine([$defaultFormTheme], $twig); + $twig->addRuntimeLoader(new FactoryRuntimeLoader([ + FormRenderer::class => function () use ($formEngine, $csrfManager): FormRenderer { + return new FormRenderer($formEngine, $csrfManager); + }, + ])); + + // ... (see the previous CSRF Protection section for more information) + + // adds the FormExtension to Twig + $twig->addExtension(new FormExtension()); + + // creates a form factory + $formFactory = Forms::createFormFactoryBuilder() + // ... + ->getFormFactory(); + +The exact details of your `Twig Configuration`_ will vary, but the goal is +always to add the :class:`Symfony\\Bridge\\Twig\\Extension\\FormExtension` +to Twig, which gives you access to the Twig functions for rendering forms. +To do this, you first need to create a :class:`Symfony\\Bridge\\Twig\\Form\\TwigRendererEngine`, +where you define your :doc:`form themes ` +(i.e. resources/files that define form HTML markup). + +For general details on rendering forms, see :doc:`/form/form_customization`. + +.. note:: + + If you use the Twig integration, read ":ref:`component-form-intro-install-translation`" + below for details on the needed translation filters. + +.. _component-form-intro-install-translation: + +Translation +~~~~~~~~~~~ + +If you're using the Twig integration with one of the default form theme files +(e.g. ``form_div_layout.html.twig``), there is a Twig filter (``trans``) +that is used for translating form labels, errors, option +text and other strings. + +To add the ``trans`` Twig filter, you can either use the built-in +:class:`Symfony\\Bridge\\Twig\\Extension\\TranslationExtension` that integrates +with Symfony's Translation component, or add the Twig filter yourself, +via your own Twig extension. + +To use the built-in integration, be sure that your project has Symfony's +Translation and :doc:`Config ` components +installed: + +.. code-block:: terminal + + $ composer require symfony/translation symfony/config + +Next, add the :class:`Symfony\\Bridge\\Twig\\Extension\\TranslationExtension` +to your ``Twig\Environment`` instance:: + + use Symfony\Bridge\Twig\Extension\TranslationExtension; + use Symfony\Component\Form\Forms; + use Symfony\Component\Translation\Loader\XliffFileLoader; + use Symfony\Component\Translation\Translator; + + // creates the Translator + $translator = new Translator('en'); + // somehow load some translations into it + $translator->addLoader('xlf', new XliffFileLoader()); + $translator->addResource( + 'xlf', + __DIR__.'/path/to/translations/messages.en.xlf', + 'en' + ); + + // adds the TranslationExtension (it gives us trans filter) + $twig->addExtension(new TranslationExtension($translator)); + + $formFactory = Forms::createFormFactoryBuilder() + // ... + ->getFormFactory(); + +Depending on how your translations are being loaded, you can now add string +keys, such as field labels, and their translations to your translation files. + +For more details on translations, see :doc:`/translation`. + +Validation +~~~~~~~~~~ + +The Form component comes with tight (but optional) integration with Symfony's +Validator component. If you're using a different solution for validation, +no problem! Take the submitted/bound data of your form (which is an +array or object) and pass it through your own validation system. + +To use the integration with Symfony's Validator component, first make sure +it's installed in your application: + +.. code-block:: terminal + + $ composer require symfony/validator + +If you're not familiar with Symfony's Validator component, read more about +it: :doc:`/validation`. The Form component comes with a +:class:`Symfony\\Component\\Form\\Extension\\Validator\\ValidatorExtension` +class, which automatically applies validation to your data on bind. These +errors are then mapped to the correct field and rendered. + +Your integration with the Validation component will look something like this:: + + use Symfony\Component\Form\Extension\Validator\ValidatorExtension; + use Symfony\Component\Form\Forms; + use Symfony\Component\Validator\Validation; + + $vendorDirectory = realpath(__DIR__.'/../vendor'); + $vendorFormDirectory = $vendorDirectory.'/symfony/form'; + $vendorValidatorDirectory = $vendorDirectory.'/symfony/validator'; + + // creates the validator - details will vary + $validator = Validation::createValidator(); + + // there are built-in translations for the core error messages + $translator->addResource( + 'xlf', + $vendorFormDirectory.'/Resources/translations/validators.en.xlf', + 'en', + 'validators' + ); + $translator->addResource( + 'xlf', + $vendorValidatorDirectory.'/Resources/translations/validators.en.xlf', + 'en', + 'validators' + ); + + $formFactory = Forms::createFormFactoryBuilder() + // ... + ->addExtension(new ValidatorExtension($validator)) + ->getFormFactory(); + +To learn more, skip down to the :ref:`component-form-intro-validation` section. + +Accessing the Form Factory +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Your application only needs one form factory, and that one factory object +should be used to create any and all form objects in your application. This +means that you should create it in some central, bootstrap part of your application +and then access it whenever you need to build a form. + +.. note:: + + In this document, the form factory is always a local variable called + ``$formFactory``. The point here is that you will probably need to create + this object in some more "global" way so you can access it from anywhere. + +Exactly how you gain access to your one form factory is up to you. If you're +using a service container (like provided with the +:doc:`DependencyInjection component `), +then you should add the form factory to your container and grab it out whenever +you need to. If your application uses global or static variables (not usually a +good idea), then you can store the object on some static class or do something +similar. + +.. _component-form-intro-create-simple-form: + +Creating a simple Form +---------------------- + +.. tip:: + + If you're using the Symfony Framework, then the form factory is available + automatically as a service called ``form.factory``, you can inject it as + ``Symfony\Component\Form\FormFactoryInterface``. Also, the default + base controller class has a :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::createFormBuilder` + method, which is a shortcut to fetch the form factory and call ``createBuilder()`` + on it. + +Creating a form is done via a :class:`Symfony\\Component\\Form\\FormBuilder` +object, where you build and configure different fields. The form builder +is created from the form factory. + +.. configuration-block:: + + .. code-block:: php-symfony + + // src/Controller/TaskController.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\TextType; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + class TaskController extends AbstractController + { + public function new(Request $request): Response + { + // createFormBuilder is a shortcut to get the "form factory" + // and then call "createBuilder()" on it + + $form = $this->createFormBuilder() + ->add('task', TextType::class) + ->add('dueDate', DateType::class) + ->getForm(); + + return $this->render('task/new.html.twig', [ + 'form' => $form->createView(), + ]); + } + } + + .. code-block:: php-standalone + + use Symfony\Component\Form\Extension\Core\Type\DateType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + + // ... + + $form = $formFactory->createBuilder() + ->add('task', TextType::class) + ->add('dueDate', DateType::class) + ->getForm(); + + var_dump($twig->render('new.html.twig', [ + 'form' => $form->createView(), + ])); + +As you can see, creating a form is like writing a recipe: you call ``add()`` +for each new field you want to create. The first argument to ``add()`` is the +name of your field, and the second is the fully qualified class name. The Form +component comes with a lot of :doc:`built-in types `. + +Now that you've built your form, learn how to :ref:`render ` +it and :ref:`process the form submission `. + +Setting default Values +~~~~~~~~~~~~~~~~~~~~~~ + +If you need your form to load with some default values (or you're building +an "edit" form), pass in the default data when creating your form builder: + +.. 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\TextType; + use Symfony\Component\HttpFoundation\Response; + + class DefaultController extends AbstractController + { + public function new(Request $request): Response + { + $defaults = [ + 'dueDate' => new \DateTime('tomorrow'), + ]; + + $form = $this->createFormBuilder($defaults) + ->add('task', TextType::class) + ->add('dueDate', DateType::class) + ->getForm(); + + // ... + } + } + + .. code-block:: php-standalone + + use Symfony\Component\Form\Extension\Core\Type\DateType; + use Symfony\Component\Form\Extension\Core\Type\FormType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + + // ... + + $defaults = [ + 'dueDate' => new \DateTime('tomorrow'), + ]; + + $form = $formFactory->createBuilder(FormType::class, $defaults) + ->add('task', TextType::class) + ->add('dueDate', DateType::class) + ->getForm(); + +.. tip:: + + In this example, the default data is an array. Later, when you use the + :ref:`data_class ` option to bind data directly to + objects, your default data will be an instance of that object. + +.. _component-form-intro-rendering-form: + +Rendering the Form +~~~~~~~~~~~~~~~~~~ + +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 `: + +.. code-block:: html+twig + + {{ form_start(form) }} + {{ form_widget(form) }} + + + {{ form_end(form) }} + +.. image:: /_images/form/simple-form.png + :alt: An HTML form showing a text box labelled "Task", three select boxes for a year, month and day labelled "Due date" and a button labelled "Create Task". + +That's it! By printing ``form_widget(form)``, each field in the form is +rendered, along with a label and error message (if there is one). While this is +convenient, it's not very flexible (yet). Usually, you'll want to render each +form field individually so you can control how the form looks. You'll learn how +to do that in the :doc:`form customization ` article. + +Changing a Form's Method and Action +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, a form is submitted to the same URI that rendered the form with +an HTTP POST request. This behavior can be changed using the :ref:`form-option-action` +and :ref:`form-option-method` options (the ``method`` option is also used +by :method:`Symfony\\Component\\Form\\Form::handleRequest` to determine whether a form has been submitted): + +.. 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\FormType; + use Symfony\Component\HttpFoundation\Response; + + class DefaultController extends AbstractController + { + public function search(): Response + { + $formBuilder = $this->createFormBuilder(null, [ + 'action' => '/search', + 'method' => 'GET', + ]); + + // ... + } + } + + .. code-block:: php-standalone + + use Symfony\Component\Form\Extension\Core\Type\FormType; + + // ... + + $formBuilder = $formFactory->createBuilder(FormType::class, null, [ + 'action' => '/search', + 'method' => 'GET', + ]); + + // ... + +.. _component-form-intro-handling-submission: + +Handling Form Submissions +~~~~~~~~~~~~~~~~~~~~~~~~~ + +To handle form submissions, use the :method:`Symfony\\Component\\Form\\Form::handleRequest` +method: + +.. configuration-block:: + + .. code-block:: php-symfony + + // src/Controller/TaskController.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\TextType; + use Symfony\Component\HttpFoundation\Response; + + class TaskController extends AbstractController + { + public function new(Request $request): Response + { + $form = $this->createFormBuilder() + ->add('task', TextType::class) + ->add('dueDate', DateType::class) + ->getForm(); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $data = $form->getData(); + + // ... perform some action, such as saving the data to the database + + return $this->redirectToRoute('task_success'); + } + + // ... + } + } + + .. code-block:: php-standalone + + use Symfony\Component\Form\Extension\Core\Type\DateType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\HttpFoundation\RedirectResponse; + use Symfony\Component\HttpFoundation\Request; + + // ... + + $form = $formFactory->createBuilder() + ->add('task', TextType::class) + ->add('dueDate', DateType::class) + ->getForm(); + + $request = Request::createFromGlobals(); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $data = $form->getData(); + + // ... perform some action, such as saving the data to the database + + $response = new RedirectResponse('/task/success'); + $response->prepare($request); + + return $response->send(); + } + + // ... + +.. warning:: + + The form's ``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). + +This defines a common form "workflow", which contains 3 different possibilities: + +#. On the initial GET request (i.e. when the user "surfs" to your page), + build your form and render it; + + If the request is a POST, process the submitted data (via :method:`Symfony\\Component\\Form\\Form::handleRequest`). + + Then: + +#. if the form is invalid, re-render the form (which will now contain errors); +#. if the form is valid, perform some action and redirect. + +Luckily, you don't need to decide whether or not a form has been submitted. +Just pass the current request to the :method:`Symfony\\Component\\Form\\Form::handleRequest` +method. Then, the Form component will do all the necessary work for you. + +.. _component-form-intro-validation: + +Form Validation +~~~~~~~~~~~~~~~ + +The easiest way to add validation to your form is via the ``constraints`` +option when building each field: + +.. 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\TextType; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Validator\Constraints\NotBlank; + use Symfony\Component\Validator\Constraints\Type; + + class DefaultController extends AbstractController + { + public function new(Request $request): Response + { + $form = $this->createFormBuilder() + ->add('task', TextType::class, [ + 'constraints' => new NotBlank(), + ]) + ->add('dueDate', DateType::class, [ + 'constraints' => [ + new NotBlank(), + new Type(\DateTime::class), + ], + ]) + ->getForm(); + // ... + } + } + + .. code-block:: php-standalone + + use Symfony\Component\Form\Extension\Core\Type\DateType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\Validator\Constraints\NotBlank; + use Symfony\Component\Validator\Constraints\Type; + + $form = $formFactory->createBuilder() + ->add('task', TextType::class, [ + 'constraints' => new NotBlank(), + ]) + ->add('dueDate', DateType::class, [ + 'constraints' => [ + new NotBlank(), + new Type(\DateTime::class), + ], + ]) + ->getForm(); + +When the form is bound, these validation constraints will be applied automatically +and the errors will display next to the fields on error. + +.. note:: + + For a list of all of the built-in validation constraints, see + :doc:`/reference/constraints`. + +Accessing Form Errors +~~~~~~~~~~~~~~~~~~~~~ + +You can use the :method:`Symfony\\Component\\Form\\FormInterface::getErrors` +method to access the list of errors. It returns a +:class:`Symfony\\Component\\Form\\FormErrorIterator` instance:: + + $form = ...; + + // ... + + // a FormErrorIterator instance, but only errors attached to this + // form level (e.g. global errors) + $errors = $form->getErrors(); + + // a FormErrorIterator instance, but only errors attached to the + // "firstName" field + $errors = $form['firstName']->getErrors(); + + // a FormErrorIterator instance including child forms in a flattened structure + // use getOrigin() to determine the form causing the error + $errors = $form->getErrors(true); + + // a FormErrorIterator instance including child forms without flattening the output structure + $errors = $form->getErrors(true, false); + +Clearing Form Errors +~~~~~~~~~~~~~~~~~~~~ + +Any errors can be manually cleared using the +:method:`Symfony\\Component\\Form\\ClearableErrorsInterface::clearErrors` +method. This is useful when you'd like to validate the form without showing +validation errors to the user (i.e. during a partial AJAX submission or +:doc:`dynamic form modification `). + +Because clearing the errors makes the form valid, +:method:`Symfony\\Component\\Form\\ClearableErrorsInterface::clearErrors` +should only be called after testing whether the form is valid. + +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /form/* + +.. _Twig: https://twig.symfony.com +.. _`Twig Configuration`: https://twig.symfony.com/doc/3.x/intro.html diff --git a/components/http_foundation.rst b/components/http_foundation.rst new file mode 100644 index 00000000000..1cb87aafb24 --- /dev/null +++ b/components/http_foundation.rst @@ -0,0 +1,1088 @@ +The HttpFoundation Component +============================ + + The HttpFoundation component defines an object-oriented layer for the HTTP + specification. + +In PHP, the request is represented by some global variables (``$_GET``, +``$_POST``, ``$_FILES``, ``$_COOKIE``, ``$_SESSION``, ...) and the response is +generated by some functions (``echo``, ``header()``, ``setcookie()``, ...). + +The Symfony HttpFoundation component replaces these default PHP global +variables and functions by an object-oriented layer. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/http-foundation + +.. include:: /components/require_autoload.rst.inc + +.. seealso:: + + This article explains how to use the HttpFoundation features as an + independent component in any PHP application. In Symfony applications + everything is already configured and ready to use. Read the :doc:`/controller` + article to learn about how to use these features when creating controllers. + +.. _component-http-foundation-request: + +Request +------- + +The most common way to create a request is to base it on the current PHP global +variables with +:method:`Symfony\\Component\\HttpFoundation\\Request::createFromGlobals`:: + + use Symfony\Component\HttpFoundation\Request; + + $request = Request::createFromGlobals(); + +which is almost equivalent to the more verbose, but also more flexible, +:method:`Symfony\\Component\\HttpFoundation\\Request::__construct` call:: + + $request = new Request( + $_GET, + $_POST, + [], + $_COOKIE, + $_FILES, + $_SERVER + ); + +.. _accessing-request-data: + +Accessing Request Data +~~~~~~~~~~~~~~~~~~~~~~ + +A Request object holds information about the client request. This information +can be accessed via several public properties: + +* ``request``: equivalent of ``$_POST``; + +* ``query``: equivalent of ``$_GET`` (``$request->query->get('name')``); + +* ``cookies``: equivalent of ``$_COOKIE``; + +* ``attributes``: no equivalent - used by your app to store other data (see :ref:`below `); + +* ``files``: equivalent of ``$_FILES``; + +* ``server``: equivalent of ``$_SERVER``; + +* ``headers``: mostly equivalent to a subset of ``$_SERVER`` + (``$request->headers->get('User-Agent')``). + +Each property is a :class:`Symfony\\Component\\HttpFoundation\\ParameterBag` +instance (or a subclass of), which is a data holder class: + +* ``request``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag` or + :class:`Symfony\\Component\\HttpFoundation\\InputBag` if the data is + coming from ``$_POST`` parameters; + +* ``query``: :class:`Symfony\\Component\\HttpFoundation\\InputBag`; + +* ``cookies``: :class:`Symfony\\Component\\HttpFoundation\\InputBag`; + +* ``attributes``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag`; + +* ``files``: :class:`Symfony\\Component\\HttpFoundation\\FileBag`; + +* ``server``: :class:`Symfony\\Component\\HttpFoundation\\ServerBag`; + +* ``headers``: :class:`Symfony\\Component\\HttpFoundation\\HeaderBag`. + +All :class:`Symfony\\Component\\HttpFoundation\\ParameterBag` instances have +methods to retrieve and update their data: + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::all` + Returns the parameters. + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::keys` + Returns the parameter keys. + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::replace` + Replaces the current parameters by a new set. + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::add` + Adds parameters. + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::get` + Returns a parameter by name. + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::set` + Sets a parameter by name. + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::has` + Returns ``true`` if the parameter is defined. + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::remove` + Removes a parameter. + +The :class:`Symfony\\Component\\HttpFoundation\\ParameterBag` instance also +has some methods to filter the input values: + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getAlpha` + Returns the alphabetic characters of the parameter value; + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getAlnum` + Returns the alphabetic characters and digits of the parameter value; + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getBoolean` + Returns the parameter value converted to boolean; + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getDigits` + Returns the digits of the parameter value; + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getInt` + Returns the parameter value converted to integer; + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getEnum` + Returns the parameter value converted to a PHP enum; + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getString` + Returns the parameter value as a string; + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::filter` + Filters the parameter by using the PHP :phpfunction:`filter_var` function. + If invalid values are found, a + :class:`Symfony\\Component\\HttpKernel\\Exception\\BadRequestHttpException` + is thrown. The ``FILTER_NULL_ON_FAILURE`` flag can be used to ignore invalid + values. + +All getters take up to two arguments: the first one is the parameter name +and the second one is the default value to return if the parameter does not +exist:: + + // the query string is '?foo=bar' + + $request->query->get('foo'); + // returns 'bar' + + $request->query->get('bar'); + // returns null + + $request->query->get('bar', 'baz'); + // returns 'baz' + +When PHP imports the request query, it handles request parameters like +``foo[bar]=baz`` in a special way as it creates an array. The ``get()`` method +doesn't support returning arrays, so you need to use the following code:: + + // the query string is '?foo[bar]=baz' + + // don't use $request->query->get('foo'); use the following instead: + $request->query->all('foo'); + // returns ['bar' => 'baz'] + + // if the requested parameter does not exist, an empty array is returned: + $request->query->all('qux'); + // returns [] + + $request->query->get('foo[bar]'); + // returns null + + $request->query->all()['foo']['bar']; + // returns 'baz' + +.. _component-foundation-attributes: + +Thanks to the public ``attributes`` property, you can store additional data +in the request, which is also an instance of +:class:`Symfony\\Component\\HttpFoundation\\ParameterBag`. This is mostly used +to attach information that belongs to the Request and that needs to be +accessed from many different points in your application. + +Finally, the raw data sent with the request body can be accessed using +:method:`Symfony\\Component\\HttpFoundation\\Request::getContent`:: + + $content = $request->getContent(); + +For instance, this may be useful to process an XML string sent to the +application by a remote service using the HTTP POST method. + +If the request body is a JSON string, it can be accessed using +:method:`Symfony\\Component\\HttpFoundation\\Request::toArray`:: + + $data = $request->toArray(); + +If the request data could be ``$_POST`` data *or* a JSON string, you can use +the :method:`Symfony\\Component\\HttpFoundation\\Request::getPayload` method +which returns an instance of :class:`Symfony\\Component\\HttpFoundation\\InputBag` +wrapping this data:: + + $data = $request->getPayload(); + +Identifying a Request +~~~~~~~~~~~~~~~~~~~~~ + +In your application, you need a way to identify a request; most of the time, +this is done via the "path info" of the request, which can be accessed via the +:method:`Symfony\\Component\\HttpFoundation\\Request::getPathInfo` method:: + + // for a request to http://example.com/blog/index.php/post/hello-world + // the path info is "/post/hello-world" + $request->getPathInfo(); + +Simulating a Request +~~~~~~~~~~~~~~~~~~~~ + +Instead of creating a request based on the PHP globals, you can also simulate +a request:: + + $request = Request::create( + '/hello-world', + 'GET', + ['name' => 'Fabien'] + ); + +The :method:`Symfony\\Component\\HttpFoundation\\Request::create` method +creates a request based on a URI, a method and some parameters (the +query parameters or the request ones depending on the HTTP method); and of +course, you can also override all other variables as well (by default, Symfony +creates sensible defaults for all the PHP global variables). + +Based on such a request, you can override the PHP global variables via +:method:`Symfony\\Component\\HttpFoundation\\Request::overrideGlobals`:: + + $request->overrideGlobals(); + +.. tip:: + + You can also duplicate an existing request via + :method:`Symfony\\Component\\HttpFoundation\\Request::duplicate` or + change a bunch of parameters with a single call to + :method:`Symfony\\Component\\HttpFoundation\\Request::initialize`. + +Accessing the Session +~~~~~~~~~~~~~~~~~~~~~ + +If you have a session attached to the request, you can access it via the +``getSession()`` method of the :class:`Symfony\\Component\\HttpFoundation\\Request` +or :class:`Symfony\\Component\\HttpFoundation\\RequestStack` class; +the :method:`Symfony\\Component\\HttpFoundation\\Request::hasPreviousSession` +method tells you if the request contains a session which was started in one of +the previous requests. + +Processing HTTP Headers +~~~~~~~~~~~~~~~~~~~~~~~ + +Processing HTTP headers is not a trivial task because of the escaping and white +space handling of their contents. Symfony provides a +:class:`Symfony\\Component\\HttpFoundation\\HeaderUtils` class that abstracts +this complexity and defines some methods for the most common tasks:: + + use Symfony\Component\HttpFoundation\HeaderUtils; + + // Splits an HTTP header by one or more separators + HeaderUtils::split('da, en-gb;q=0.8', ',;'); + // => [['da'], ['en-gb','q=0.8']] + + // Combines an array of arrays into one associative array + HeaderUtils::combine([['foo', 'abc'], ['bar']]); + // => ['foo' => 'abc', 'bar' => true] + + // Joins an associative array into a string for use in an HTTP header + HeaderUtils::toString(['foo' => 'abc', 'bar' => true, 'baz' => 'a b c'], ','); + // => 'foo=abc, bar, baz="a b c"' + + // Encodes a string as a quoted string, if necessary + HeaderUtils::quote('foo "bar"'); + // => '"foo \"bar\""' + + // Decodes a quoted string + HeaderUtils::unquote('"foo \"bar\""'); + // => 'foo "bar"' + + // Parses a query string but maintains dots (PHP parse_str() replaces '.' by '_') + HeaderUtils::parseQuery('foo[bar.baz]=qux'); + // => ['foo' => ['bar.baz' => 'qux']] + +Accessing ``Accept-*`` Headers Data +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can access basic data extracted from ``Accept-*`` headers +by using the following methods: + +:method:`Symfony\\Component\\HttpFoundation\\Request::getAcceptableContentTypes` + Returns the list of accepted content types ordered by descending quality. + +:method:`Symfony\\Component\\HttpFoundation\\Request::getLanguages` + Returns the list of accepted languages ordered by descending quality. + +:method:`Symfony\\Component\\HttpFoundation\\Request::getCharsets` + Returns the list of accepted charsets ordered by descending quality. + +:method:`Symfony\\Component\\HttpFoundation\\Request::getEncodings` + Returns the list of accepted encodings ordered by descending quality. + +If you need to get full access to parsed data from ``Accept``, ``Accept-Language``, +``Accept-Charset`` or ``Accept-Encoding``, you can use +:class:`Symfony\\Component\\HttpFoundation\\AcceptHeader` utility class:: + + use Symfony\Component\HttpFoundation\AcceptHeader; + + $acceptHeader = AcceptHeader::fromString($request->headers->get('Accept')); + if ($acceptHeader->has('text/html')) { + $item = $acceptHeader->get('text/html'); + $charset = $item->getAttribute('charset', 'utf-8'); + $quality = $item->getQuality(); + } + + // Accept header items are sorted by descending quality + $acceptHeaders = AcceptHeader::fromString($request->headers->get('Accept')) + ->all(); + +The default values that can be optionally included in the ``Accept-*`` headers +are also supported:: + + $acceptHeader = 'text/plain;q=0.5, text/html, text/*;q=0.8, */*;q=0.3'; + $accept = AcceptHeader::fromString($acceptHeader); + + $quality = $accept->get('text/xml')->getQuality(); // $quality = 0.8 + $quality = $accept->get('application/xml')->getQuality(); // $quality = 0.3 + +Anonymizing IP Addresses +~~~~~~~~~~~~~~~~~~~~~~~~ + +An increasingly common need for applications to comply with user protection +regulations is to anonymize IP addresses before logging and storing them for +analysis purposes. Use the ``anonymize()`` method from the +:class:`Symfony\\Component\\HttpFoundation\\IpUtils` to do that:: + + use Symfony\Component\HttpFoundation\IpUtils; + + $ipv4 = '123.234.235.236'; + $anonymousIpv4 = IpUtils::anonymize($ipv4); + // $anonymousIpv4 = '123.234.235.0' + + $ipv6 = '2a01:198:603:10:396e:4789:8e99:890f'; + $anonymousIpv6 = IpUtils::anonymize($ipv6); + // $anonymousIpv6 = '2a01:198:603:10::' + +If you need even more anonymization, you can use the second and third parameters +of the ``anonymize()`` method to specify the number of bytes that should be +anonymized depending on the IP address format:: + + $ipv4 = '123.234.235.236'; + $anonymousIpv4 = IpUtils::anonymize($ipv4, 3); + // $anonymousIpv4 = '123.0.0.0' + + $ipv6 = '2a01:198:603:10:396e:4789:8e99:890f'; + // (you must define the second argument (bytes to anonymize in IPv4 addresses) + // even when you are only anonymizing IPv6 addresses) + $anonymousIpv6 = IpUtils::anonymize($ipv6, 3, 10); + // $anonymousIpv6 = '2a01:198:603::' + +.. versionadded:: 7.2 + + The ``v4Bytes`` and ``v6Bytes`` parameters of the ``anonymize()`` method + were introduced in Symfony 7.2. + +Check If an IP Belongs to a CIDR Subnet +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to know if an IP address is included in a CIDR subnet, you can use +the ``checkIp()`` method from :class:`Symfony\\Component\\HttpFoundation\\IpUtils`:: + + use Symfony\Component\HttpFoundation\IpUtils; + + $ipv4 = '192.168.1.56'; + $CIDRv4 = '192.168.1.0/16'; + $isIpInCIDRv4 = IpUtils::checkIp($ipv4, $CIDRv4); + // $isIpInCIDRv4 = true + + $ipv6 = '2001:db8:abcd:1234::1'; + $CIDRv6 = '2001:db8:abcd::/48'; + $isIpInCIDRv6 = IpUtils::checkIp($ipv6, $CIDRv6); + // $isIpInCIDRv6 = true + +Check if an IP Belongs to a Private Subnet +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to know if an IP address belongs to a private subnet, you can +use the ``isPrivateIp()`` method from the +:class:`Symfony\\Component\\HttpFoundation\\IpUtils` to do that:: + + use Symfony\Component\HttpFoundation\IpUtils; + + $ipv4 = '192.168.1.1'; + $isPrivate = IpUtils::isPrivateIp($ipv4); + // $isPrivate = true + + $ipv6 = '2a01:198:603:10:396e:4789:8e99:890f'; + $isPrivate = IpUtils::isPrivateIp($ipv6); + // $isPrivate = false + +Matching a Request Against a Set of Rules +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The HttpFoundation component provides some matcher classes that allow you to +check if a given request meets certain conditions (e.g. it comes from some IP +address, it uses a certain HTTP method, etc.): + +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\AttributesRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\ExpressionRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\HeaderRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\HostRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\IpsRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\IsJsonRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\MethodRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\PathRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\PortRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\QueryParameterRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\SchemeRequestMatcher` + +You can use them individually or combine them using the +:class:`Symfony\\Component\\HttpFoundation\\ChainRequestMatcher` class:: + + use Symfony\Component\HttpFoundation\ChainRequestMatcher; + use Symfony\Component\HttpFoundation\RequestMatcher\HostRequestMatcher; + use Symfony\Component\HttpFoundation\RequestMatcher\PathRequestMatcher; + use Symfony\Component\HttpFoundation\RequestMatcher\SchemeRequestMatcher; + + // use only one criteria to match the request + $schemeMatcher = new SchemeRequestMatcher('https'); + if ($schemeMatcher->matches($request)) { + // ... + } + + // use a set of criteria to match the request + $matcher = new ChainRequestMatcher([ + new HostRequestMatcher('example.com'), + new PathRequestMatcher('/admin'), + ]); + + if ($matcher->matches($request)) { + // ... + } + +.. versionadded:: 7.1 + + The ``HeaderRequestMatcher`` and ``QueryParameterRequestMatcher`` were + introduced in Symfony 7.1. + +Accessing other Data +~~~~~~~~~~~~~~~~~~~~ + +The ``Request`` class has many other methods that you can use to access the +request information. Have a look at +:class:`the Request API ` +for more information about them. + +Overriding the Request +~~~~~~~~~~~~~~~~~~~~~~ + +The ``Request`` class should not be overridden as it is a data object that +represents an HTTP message. But when moving from a legacy system, adding +methods or changing some default behavior might help. In that case, register a +PHP callable that is able to create an instance of your ``Request`` class:: + + use App\Http\SpecialRequest; + use Symfony\Component\HttpFoundation\Request; + + Request::setFactory(function ( + array $query = [], + array $request = [], + array $attributes = [], + array $cookies = [], + array $files = [], + array $server = [], + $content = null + ) { + return new SpecialRequest( + $query, + $request, + $attributes, + $cookies, + $files, + $server, + $content + ); + }); + + $request = Request::createFromGlobals(); + +.. _component-http-foundation-response: + +Response +-------- + +A :class:`Symfony\\Component\\HttpFoundation\\Response` object holds all the +information that needs to be sent back to the client from a given request. The +constructor takes up to three arguments: the response content, the status +code, and an array of HTTP headers:: + + use Symfony\Component\HttpFoundation\Response; + + $response = new Response( + 'Content', + Response::HTTP_OK, + ['content-type' => 'text/html'] + ); + +This information can also be manipulated after the Response object creation:: + + $response->setContent('Hello World'); + + // the headers public attribute is a ResponseHeaderBag + $response->headers->set('Content-Type', 'text/plain'); + + $response->setStatusCode(Response::HTTP_NOT_FOUND); + +When setting the ``Content-Type`` of the Response, you can set the charset, +but it is better to set it via the +:method:`Symfony\\Component\\HttpFoundation\\Response::setCharset` method:: + + $response->setCharset('ISO-8859-1'); + +Note that by default, Symfony assumes that your Responses are encoded in +UTF-8. + +Sending the Response +~~~~~~~~~~~~~~~~~~~~ + +Before sending the Response, you can optionally call the +:method:`Symfony\\Component\\HttpFoundation\\Response::prepare` method to fix any +incompatibility with the HTTP specification (e.g. a wrong ``Content-Type`` header):: + + $response->prepare($request); + +Sending the response to the client is done by calling the method +:method:`Symfony\\Component\\HttpFoundation\\Response::send`:: + + $response->send(); + +The ``send()`` method takes an optional ``flush`` argument. If set to +``false``, functions like ``fastcgi_finish_request()`` or +``litespeed_finish_request()`` are not called. This is useful when debugging +your application to see which exceptions are thrown in listeners of the +:class:`Symfony\\Component\\HttpKernel\\Event\\TerminateEvent`. You can learn +more about it in +:ref:`the dedicated section about Kernel events `. + +Setting Cookies +~~~~~~~~~~~~~~~ + +The response cookies can be manipulated through the ``headers`` public +attribute:: + + use Symfony\Component\HttpFoundation\Cookie; + + $response->headers->setCookie(Cookie::create('foo', 'bar')); + +The +:method:`Symfony\\Component\\HttpFoundation\\ResponseHeaderBag::setCookie` +method takes an instance of +:class:`Symfony\\Component\\HttpFoundation\\Cookie` as an argument. + +You can clear a cookie via the +:method:`Symfony\\Component\\HttpFoundation\\ResponseHeaderBag::clearCookie` method. + +In addition to the ``Cookie::create()`` method, you can create a ``Cookie`` +object from a raw header value using :method:`Symfony\\Component\\HttpFoundation\\Cookie::fromString` +method. You can also use the ``with*()`` methods to change some Cookie property (or +to build the entire Cookie using a fluent interface). Each ``with*()`` method returns +a new object with the modified property:: + + $cookie = Cookie::create('foo') + ->withValue('bar') + ->withExpires(strtotime('Fri, 20-May-2011 15:25:52 GMT')) + ->withDomain('.example.com') + ->withSecure(true); + +It is possible to define partitioned cookies, also known as `CHIPS`_, by using the +:method:`Symfony\\Component\\HttpFoundation\\Cookie::withPartitioned` method:: + + $cookie = Cookie::create('foo') + ->withValue('bar') + ->withPartitioned(); + + // you can also set the partitioned argument to true when using the `create()` factory method + $cookie = Cookie::create('name', 'value', partitioned: true); + +Managing the HTTP Cache +~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`Symfony\\Component\\HttpFoundation\\Response` class has a rich set +of methods to manipulate the HTTP headers related to the cache: + +* :method:`Symfony\\Component\\HttpFoundation\\Response::setPublic` +* :method:`Symfony\\Component\\HttpFoundation\\Response::setPrivate` +* :method:`Symfony\\Component\\HttpFoundation\\Response::expire` +* :method:`Symfony\\Component\\HttpFoundation\\Response::setExpires` +* :method:`Symfony\\Component\\HttpFoundation\\Response::setMaxAge` +* :method:`Symfony\\Component\\HttpFoundation\\Response::setSharedMaxAge` +* :method:`Symfony\\Component\\HttpFoundation\\Response::setStaleIfError` +* :method:`Symfony\\Component\\HttpFoundation\\Response::setStaleWhileRevalidate` +* :method:`Symfony\\Component\\HttpFoundation\\Response::setTtl` +* :method:`Symfony\\Component\\HttpFoundation\\Response::setClientTtl` +* :method:`Symfony\\Component\\HttpFoundation\\Response::setLastModified` +* :method:`Symfony\\Component\\HttpFoundation\\Response::setEtag` +* :method:`Symfony\\Component\\HttpFoundation\\Response::setVary` + +.. note:: + + The methods :method:`Symfony\\Component\\HttpFoundation\\Response::setExpires`, + :method:`Symfony\\Component\\HttpFoundation\\Response::setLastModified` and + :method:`Symfony\\Component\\HttpFoundation\\Response::setDate` accept any + object that implements ``\DateTimeInterface``, including immutable date objects. + +The :method:`Symfony\\Component\\HttpFoundation\\Response::setCache` method +can be used to set the most commonly used cache information in one method +call:: + + $response->setCache([ + 'must_revalidate' => false, + 'no_cache' => false, + 'no_store' => false, + 'no_transform' => false, + 'public' => true, + 'private' => false, + 'proxy_revalidate' => false, + 'max_age' => 600, + 's_maxage' => 600, + 'stale_if_error' => 86400, + 'stale_while_revalidate' => 60, + 'immutable' => true, + 'last_modified' => new \DateTime(), + 'etag' => 'abcdef', + ]); + +To check if the Response validators (``ETag``, ``Last-Modified``) match a +conditional value specified in the client Request, use the +:method:`Symfony\\Component\\HttpFoundation\\Response::isNotModified` +method:: + + if ($response->isNotModified($request)) { + $response->send(); + } + +If the Response is not modified, it sets the status code to 304 and removes the +actual response content. + +.. _redirect-response: + +Redirecting the User +~~~~~~~~~~~~~~~~~~~~ + +To redirect the client to another URL, you can use the +:class:`Symfony\\Component\\HttpFoundation\\RedirectResponse` class:: + + use Symfony\Component\HttpFoundation\RedirectResponse; + + $response = new RedirectResponse('http://example.com/'); + +.. _streaming-response: + +Streaming a Response +~~~~~~~~~~~~~~~~~~~~ + +The :class:`Symfony\\Component\\HttpFoundation\\StreamedResponse` class allows +you to stream the Response back to the client. The response content can be +represented by a string iterable:: + + use Symfony\Component\HttpFoundation\StreamedResponse; + + $chunks = ['Hello', ' World']; + + $response = new StreamedResponse(); + $response->setChunks($chunks); + $response->send(); + +For most complex use cases, the response content can be instead represented by +a PHP callable:: + + use Symfony\Component\HttpFoundation\StreamedResponse; + + $response = new StreamedResponse(); + $response->setCallback(function (): void { + var_dump('Hello World'); + flush(); + sleep(2); + var_dump('Hello World'); + flush(); + }); + $response->send(); + +.. note:: + + The ``flush()`` function does not flush buffering. If ``ob_start()`` has + been called before or the ``output_buffering`` ``php.ini`` option is enabled, + you must call ``ob_flush()`` before ``flush()``. + + Additionally, PHP isn't the only layer that can buffer output. Your web + server might also buffer based on its configuration. Some servers, such as + nginx, let you disable buffering at the config level or by adding a special HTTP + header in the response:: + + // disables FastCGI buffering in nginx only for this response + $response->headers->set('X-Accel-Buffering', 'no'); + +.. versionadded:: 7.3 + + Support for using string iterables was introduced in Symfony 7.3. + +Streaming a JSON Response +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`Symfony\\Component\\HttpFoundation\\StreamedJsonResponse` allows to +stream large JSON responses using PHP generators to keep the used resources low. + +The class constructor expects an array which represents the JSON structure and +includes the list of contents to stream. In addition to PHP generators, which are +recommended to minimize memory usage, it also supports any kind of PHP Traversable +containing JSON serializable data:: + + use Symfony\Component\HttpFoundation\StreamedJsonResponse; + + // any method or function returning a PHP Generator + function loadArticles(): \Generator { + yield ['title' => 'Article 1']; + yield ['title' => 'Article 2']; + yield ['title' => 'Article 3']; + }; + + $response = new StreamedJsonResponse( + // JSON structure with generators in which will be streamed as a list + [ + '_embedded' => [ + 'articles' => loadArticles(), + ], + ], + ); + +When loading data via Doctrine, you can use the ``toIterable()`` method to +fetch results row by row and minimize resources consumption. +See the `Doctrine Batch processing`_ documentation for more:: + + public function __invoke(): Response + { + return new StreamedJsonResponse( + [ + '_embedded' => [ + 'articles' => $this->loadArticles(), + ], + ], + ); + } + + public function loadArticles(): \Generator + { + // get the $entityManager somehow (e.g. via constructor injection) + $entityManager = ... + + $queryBuilder = $entityManager->createQueryBuilder(); + $queryBuilder->from(Article::class, 'article'); + $queryBuilder->select('article.id') + ->addSelect('article.title') + ->addSelect('article.description'); + + return $queryBuilder->getQuery()->toIterable(); + } + +If you return a lot of data, consider calling the :phpfunction:`flush` function +after some specific item count to send the contents to the browser:: + + public function loadArticles(): \Generator + { + // ... + + $count = 0; + foreach ($queryBuilder->getQuery()->toIterable() as $article) { + yield $article; + + if (0 === ++$count % 100) { + flush(); + } + } + } + +Alternatively, you can also pass any iterable to ``StreamedJsonResponse``, +including generators:: + + public function loadArticles(): \Generator + { + yield ['title' => 'Article 1']; + yield ['title' => 'Article 2']; + yield ['title' => 'Article 3']; + } + + public function __invoke(): Response + { + // ... + + return new StreamedJsonResponse(loadArticles()); + } + +.. _component-http-foundation-serving-files: + +Serving Files +~~~~~~~~~~~~~ + +When sending a file, you must add a ``Content-Disposition`` header to your +response. While creating this header for basic file downloads is straightforward, +using non-ASCII filenames is more involved. The +:method:`Symfony\\Component\\HttpFoundation\\HeaderUtils::makeDisposition` +abstracts the hard work behind a simple API:: + + use Symfony\Component\HttpFoundation\HeaderUtils; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpFoundation\ResponseHeaderBag; + + $fileContent = ...; // the generated file content + $response = new Response($fileContent); + + $disposition = HeaderUtils::makeDisposition( + HeaderUtils::DISPOSITION_ATTACHMENT, + 'foo.pdf' + ); + + $response->headers->set('Content-Disposition', $disposition); + +Alternatively, if you are serving a static file, you can use a +:class:`Symfony\\Component\\HttpFoundation\\BinaryFileResponse`:: + + use Symfony\Component\HttpFoundation\BinaryFileResponse; + + $file = 'path/to/file.txt'; + $response = new BinaryFileResponse($file); + +The ``BinaryFileResponse`` will automatically handle ``Range`` and +``If-Range`` headers from the request. It also supports ``X-Sendfile`` +(see `FrankenPHP X-Sendfile and X-Accel-Redirect headers`_, +`nginx X-Accel-Redirect header`_ and `Apache mod_xsendfile module`_). To make use +of it, you need to determine whether or not the ``X-Sendfile-Type`` header should +be trusted and call :method:`Symfony\\Component\\HttpFoundation\\BinaryFileResponse::trustXSendfileTypeHeader` +if it should:: + + BinaryFileResponse::trustXSendfileTypeHeader(); + +.. note:: + + The ``BinaryFileResponse`` will only handle ``X-Sendfile`` if the particular header is present. + For Apache, this is not the default case. + + To add the header use the ``mod_headers`` Apache module and add the following to the Apache configuration: + + .. code-block:: apache + + + # This is already present somewhere... + XSendFile on + XSendFilePath ...some path... + + # This needs to be added: + + RequestHeader set X-Sendfile-Type X-Sendfile + + + +With the ``BinaryFileResponse``, you can still set the ``Content-Type`` of the sent file, +or change its ``Content-Disposition``:: + + // ... + $response->headers->set('Content-Type', 'text/plain'); + $response->setContentDisposition( + ResponseHeaderBag::DISPOSITION_ATTACHMENT, + 'filename.txt' + ); + +It is possible to delete the file after the response is sent with the +:method:`Symfony\\Component\\HttpFoundation\\BinaryFileResponse::deleteFileAfterSend` method. +Please note that this will not work when the ``X-Sendfile`` header is set. + +Alternatively, ``BinaryFileResponse`` supports instances of ``\SplTempFileObject``. +This is useful when you want to serve a file that has been created in memory +and that will be automatically deleted after the response is sent:: + + use Symfony\Component\HttpFoundation\BinaryFileResponse; + + $file = new \SplTempFileObject(); + $file->fwrite('Hello World'); + $file->rewind(); + + $response = new BinaryFileResponse($file); + +.. versionadded:: 7.1 + + The support for ``\SplTempFileObject`` in ``BinaryFileResponse`` + was introduced in Symfony 7.1. + +If the size of the served file is unknown (e.g. because it's being generated on the fly, +or because a PHP stream filter is registered on it, etc.), you can pass a ``Stream`` +instance to ``BinaryFileResponse``. This will disable ``Range`` and ``Content-Length`` +handling, switching to chunked encoding instead:: + + use Symfony\Component\HttpFoundation\BinaryFileResponse; + use Symfony\Component\HttpFoundation\File\Stream; + + $stream = new Stream('path/to/stream'); + $response = new BinaryFileResponse($stream); + +.. note:: + + If you *just* created the file during this same request, the file *may* be sent + without any content. This may be due to cached file stats that return zero for + the size of the file. To fix this issue, call ``clearstatcache(true, $file)`` + with the path to the binary file. + +.. _component-http-foundation-json-response: + +Creating a JSON Response +~~~~~~~~~~~~~~~~~~~~~~~~ + +Any type of response can be created via the +:class:`Symfony\\Component\\HttpFoundation\\Response` class by setting the +right content and headers. A JSON response might look like this:: + + use Symfony\Component\HttpFoundation\Response; + + $response = new Response(); + $response->setContent(json_encode([ + 'data' => 123, + ])); + $response->headers->set('Content-Type', 'application/json'); + +There is also a helpful :class:`Symfony\\Component\\HttpFoundation\\JsonResponse` +class, which can make this even easier:: + + use Symfony\Component\HttpFoundation\JsonResponse; + + // if you know the data to send when creating the response + $response = new JsonResponse(['data' => 123]); + + // if you don't know the data to send or if you want to customize the encoding options + $response = new JsonResponse(); + // ... + // configure any custom encoding options (if needed, it must be called before "setData()") + //$response->setEncodingOptions(JsonResponse::DEFAULT_ENCODING_OPTIONS | \JSON_PRESERVE_ZERO_FRACTION); + $response->setData(['data' => 123]); + + // if the data to send is already encoded in JSON + $response = JsonResponse::fromJsonString('{ "data": 123 }'); + +The ``JsonResponse`` class sets the ``Content-Type`` header to +``application/json`` and encodes your data to JSON when needed. + +.. danger:: + + To avoid XSSI `JSON Hijacking`_, you should pass an associative array + as the outermost array to ``JsonResponse`` and not an indexed array so + that the final result is an object (e.g. ``{"object": "not inside an array"}``) + instead of an array (e.g. ``[{"object": "inside an array"}]``). Read + the `OWASP guidelines`_ for more information. + + Only methods that respond to GET requests are vulnerable to XSSI 'JSON Hijacking'. + Methods responding to POST requests only remain unaffected. + +.. warning:: + + The ``JsonResponse`` constructor exhibits non-standard JSON encoding behavior + and will treat ``null`` as an empty object if passed as a constructor argument, + despite null being a `valid JSON top-level value`_. + + This behavior cannot be changed without backwards-compatibility concerns, but + it's possible to call ``setData`` and pass the value there to opt-out of the + behavior. + +JSONP Callback +~~~~~~~~~~~~~~ + +If you're using JSONP, you can set the callback function that the data should +be passed to:: + + $response->setCallback('handleResponse'); + +In this case, the ``Content-Type`` header will be ``text/javascript`` and +the response content will look like this: + +.. code-block:: javascript + + handleResponse({'data': 123}); + +Session +------- + +The session information is in its own document: :doc:`/session`. + +Safe Content Preference +----------------------- + +Some web sites have a "safe" mode to assist those who don't want to be exposed +to content to which they might object. The `RFC 8674`_ specification defines a +way for user agents to ask for safe content to a server. + +The specification does not define what content might be considered objectionable, +so the concept of "safe" is not precisely defined. Rather, the term is interpreted +by the server and within the scope of each web site that chooses to act upon this information. + +Symfony offers two methods to interact with this preference: + +* :method:`Symfony\\Component\\HttpFoundation\\Request::preferSafeContent`; +* :method:`Symfony\\Component\\HttpFoundation\\Response::setContentSafe`; + +The following example shows how to detect if the user agent prefers "safe" content:: + + if ($request->preferSafeContent()) { + $response = new Response($alternativeContent); + // this informs the user we respected their preferences + $response->setContentSafe(); + + return $response; + +Generating Relative and Absolute URLs +------------------------------------- + +Generating absolute and relative URLs for a given path is a common need +in some applications. In Twig templates you can use the +:ref:`absolute_url() ` and +:ref:`relative_path() ` functions to do that. + +The :class:`Symfony\\Component\\HttpFoundation\\UrlHelper` class provides the +same functionality for PHP code via the ``getAbsoluteUrl()`` and ``getRelativePath()`` +methods. You can inject this as a service anywhere in your application:: + + // src/Normalizer/UserApiNormalizer.php + namespace App\Normalizer; + + use Symfony\Component\HttpFoundation\UrlHelper; + + class UserApiNormalizer + { + public function __construct( + private UrlHelper $urlHelper, + ) { + } + + public function normalize($user): array + { + return [ + 'avatar' => $this->urlHelper->getAbsoluteUrl($user->avatar()->path()), + ]; + } + } + +Learn More +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /controller + /controller/* + /session + /http_cache/* + +.. _`FrankenPHP X-Sendfile and X-Accel-Redirect headers`: https://frankenphp.dev/docs/x-sendfile/ +.. _`nginx X-Accel-Redirect header`: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers +.. _`Apache mod_xsendfile module`: https://github.com/nmaier/mod_xsendfile +.. _`JSON Hijacking`: https://haacked.com/archive/2009/06/25/json-hijacking.aspx/ +.. _`valid JSON top-level value`: https://www.json.org/json-en.html +.. _OWASP guidelines: https://cheatsheetseries.owasp.org/cheatsheets/AJAX_Security_Cheat_Sheet.html#always-return-json-with-an-object-on-the-outside +.. _RFC 8674: https://tools.ietf.org/html/rfc8674 +.. _Doctrine Batch processing: https://www.doctrine-project.org/projects/doctrine-orm/en/2.14/reference/batch-processing.html#iterating-results +.. _`CHIPS`: https://developer.mozilla.org/en-US/docs/Web/Privacy/Partitioned_cookies diff --git a/components/http_foundation/index.rst b/components/http_foundation/index.rst deleted file mode 100644 index 348f8e50ca4..00000000000 --- a/components/http_foundation/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -HTTP Foundation -=============== - -.. toctree:: - :maxdepth: 2 - - introduction - sessions - session_configuration - session_testing - session_php_bridge - trusting_proxies diff --git a/components/http_foundation/introduction.rst b/components/http_foundation/introduction.rst deleted file mode 100644 index 1c6ba3b2188..00000000000 --- a/components/http_foundation/introduction.rst +++ /dev/null @@ -1,530 +0,0 @@ -.. index:: - single: HTTP - single: HttpFoundation - single: Components; HttpFoundation - -The HttpFoundation Component -============================ - - The HttpFoundation Component defines an object-oriented layer for the HTTP - specification. - -In PHP, the request is represented by some global variables (``$_GET``, -``$_POST``, ``$_FILE``, ``$_COOKIE``, ``$_SESSION``, ...) and the response is -generated by some functions (``echo``, ``header``, ``setcookie``, ...). - -The Symfony2 HttpFoundation component replaces these default PHP global -variables and functions by an Object-Oriented layer. - -Installation ------------- - -You can install the component in many different ways: - -* Use the official Git repository (https://github.com/symfony/HttpFoundation); -* :doc:`Install it via Composer` (``symfony/http-foundation`` on `Packagist`_). - -Request -------- - -The most common way to create request is to base it on the current PHP global -variables with -:method:`Symfony\\Component\\HttpFoundation\\Request::createFromGlobals`:: - - use Symfony\Component\HttpFoundation\Request; - - $request = Request::createFromGlobals(); - -which is almost equivalent to the more verbose, but also more flexible, -:method:`Symfony\\Component\\HttpFoundation\\Request::__construct` call:: - - $request = new Request( - $_GET, - $_POST, - array(), - $_COOKIE, - $_FILES, - $_SERVER - ); - -Accessing Request Data -~~~~~~~~~~~~~~~~~~~~~~ - -A Request object holds information about the client request. This information -can be accessed via several public properties: - -* ``request``: equivalent of ``$_POST``; - -* ``query``: equivalent of ``$_GET`` (``$request->query->get('name')``); - -* ``cookies``: equivalent of ``$_COOKIE``; - -* ``attributes``: no equivalent - used by your app to store other data (see :ref:`below`) - -* ``files``: equivalent of ``$_FILE``; - -* ``server``: equivalent of ``$_SERVER``; - -* ``headers``: mostly equivalent to a sub-set of ``$_SERVER`` - (``$request->headers->get('Content-Type')``). - -Each property is a :class:`Symfony\\Component\\HttpFoundation\\ParameterBag` -instance (or a sub-class of), which is a data holder class: - -* ``request``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag`; - -* ``query``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag`; - -* ``cookies``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag`; - -* ``attributes``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag`; - -* ``files``: :class:`Symfony\\Component\\HttpFoundation\\FileBag`; - -* ``server``: :class:`Symfony\\Component\\HttpFoundation\\ServerBag`; - -* ``headers``: :class:`Symfony\\Component\\HttpFoundation\\HeaderBag`. - -All :class:`Symfony\\Component\\HttpFoundation\\ParameterBag` instances have -methods to retrieve and update its data: - -* :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::all`: Returns - the parameters; - -* :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::keys`: Returns - the parameter keys; - -* :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::replace`: - Replaces the current parameters by a new set; - -* :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::add`: Adds - parameters; - -* :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::get`: Returns a - parameter by name; - -* :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::set`: Sets a - parameter by name; - -* :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::has`: Returns - true if the parameter is defined; - -* :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::remove`: Removes - a parameter. - -The :class:`Symfony\\Component\\HttpFoundation\\ParameterBag` instance also -has some methods to filter the input values: - -* :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getAlpha`: Returns - the alphabetic characters of the parameter value; - -* :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getAlnum`: Returns - the alphabetic characters and digits of the parameter value; - -* :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getDigits`: Returns - the digits of the parameter value; - -* :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getInt`: Returns the - parameter value converted to integer; - -* :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::filter`: Filters the - parameter by using the PHP ``filter_var()`` function. - -All getters takes up to three arguments: the first one is the parameter name -and the second one is the default value to return if the parameter does not -exist:: - - // the query string is '?foo=bar' - - $request->query->get('foo'); - // returns bar - - $request->query->get('bar'); - // returns null - - $request->query->get('bar', 'bar'); - // returns 'bar' - - -When PHP imports the request query, it handles request parameters like -``foo[bar]=bar`` in a special way as it creates an array. So you can get the -``foo`` parameter and you will get back an array with a ``bar`` element. But -sometimes, you might want to get the value for the "original" parameter name: -``foo[bar]``. This is possible with all the `ParameterBag` getters like -:method:`Symfony\\Component\\HttpFoundation\\Request::get` via the third -argument:: - - // the query string is '?foo[bar]=bar' - - $request->query->get('foo'); - // returns array('bar' => 'bar') - - $request->query->get('foo[bar]'); - // returns null - - $request->query->get('foo[bar]', null, true); - // returns 'bar' - -.. _component-foundation-attributes: - -Finally, you can also store additional data in the request, -thanks to the public ``attributes`` property, which is also an instance of -:class:`Symfony\\Component\\HttpFoundation\\ParameterBag`. This is mostly used -to attach information that belongs to the Request and that needs to be -accessed from many different points in your application. For information -on how this is used in the Symfony2 framework, see :ref:`read more`. - -Identifying a Request -~~~~~~~~~~~~~~~~~~~~~ - -In your application, you need a way to identify a request; most of the time, -this is done via the "path info" of the request, which can be accessed via the -:method:`Symfony\\Component\\HttpFoundation\\Request::getPathInfo` method:: - - // for a request to http://example.com/blog/index.php/post/hello-world - // the path info is "/post/hello-world" - $request->getPathInfo(); - -Simulating a Request -~~~~~~~~~~~~~~~~~~~~ - -Instead of creating a Request based on the PHP globals, you can also simulate -a Request:: - - $request = Request::create( - '/hello-world', - 'GET', - array('name' => 'Fabien') - ); - -The :method:`Symfony\\Component\\HttpFoundation\\Request::create` method -creates a request based on a path info, a method and some parameters (the -query parameters or the request ones depending on the HTTP method); and of -course, you can also override all other variables as well (by default, Symfony -creates sensible defaults for all the PHP global variables). - -Based on such a request, you can override the PHP global variables via -:method:`Symfony\\Component\\HttpFoundation\\Request::overrideGlobals`:: - - $request->overrideGlobals(); - -.. tip:: - - You can also duplicate an existing query via - :method:`Symfony\\Component\\HttpFoundation\\Request::duplicate` or - change a bunch of parameters with a single call to - :method:`Symfony\\Component\\HttpFoundation\\Request::initialize`. - -Accessing the Session -~~~~~~~~~~~~~~~~~~~~~ - -If you have a session attached to the Request, you can access it via the -:method:`Symfony\\Component\\HttpFoundation\\Request::getSession` method; -the -:method:`Symfony\\Component\\HttpFoundation\\Request::hasPreviousSession` -method tells you if the request contains a Session which was started in one of -the previous requests. - -Accessing `Accept-*` Headers Data -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can easily access basic data extracted from ``Accept-*`` headers -by using the following methods: - -* :method:`Symfony\\Component\\HttpFoundation\\Request::getAcceptableContentTypes`: - returns the list of accepted content types ordered by descending quality; - -* :method:`Symfony\\Component\\HttpFoundation\\Request::getLanguages`: - returns the list of accepted languages ordered by descending quality; - -* :method:`Symfony\\Component\\HttpFoundation\\Request::getCharsets`: - returns the list of accepted charsets ordered by descending quality; - -.. versionadded:: 2.2 - The :class:`Symfony\\Component\\HttpFoundation\\AcceptHeader` class is new in Symfony 2.2. - -If you need to get full access to parsed data from ``Accept``, ``Accept-Language``, -``Accept-Charset`` or ``Accept-Encoding``, you can use -:class:`Symfony\\Component\\HttpFoundation\\AcceptHeader` utility class:: - - use Symfony\Component\HttpFoundation\AcceptHeader; - - $accept = AcceptHeader::fromString($request->headers->get('Accept')); - if ($accept->has('text/html')) { - $item = $accept->get('html'); - $charset = $item->getAttribute('charset', 'utf-8'); - $quality = $item->getQuality(); - } - - // accepts items are sorted by descending quality - $accepts = AcceptHeader::fromString($request->headers->get('Accept'))->all(); - -Accessing other Data -~~~~~~~~~~~~~~~~~~~~ - -The Request class has many other methods that you can use to access the -request information. Have a look at the API for more information about them. - -Response --------- - -A :class:`Symfony\\Component\\HttpFoundation\\Response` object holds all the -information that needs to be sent back to the client from a given request. The -constructor takes up to three arguments: the response content, the status -code, and an array of HTTP headers:: - - use Symfony\Component\HttpFoundation\Response; - - $response = new Response( - 'Content', - 200, - array('content-type' => 'text/html') - ); - -These information can also be manipulated after the Response object creation:: - - $response->setContent('Hello World'); - - // the headers public attribute is a ResponseHeaderBag - $response->headers->set('Content-Type', 'text/plain'); - - $response->setStatusCode(404); - -When setting the ``Content-Type`` of the Response, you can set the charset, -but it is better to set it via the -:method:`Symfony\\Component\\HttpFoundation\\Response::setCharset` method:: - - $response->setCharset('ISO-8859-1'); - -Note that by default, Symfony assumes that your Responses are encoded in -UTF-8. - -Sending the Response -~~~~~~~~~~~~~~~~~~~~ - -Before sending the Response, you can ensure that it is compliant with the HTTP -specification by calling the -:method:`Symfony\\Component\\HttpFoundation\\Response::prepare` method:: - - $response->prepare($request); - -Sending the response to the client is then as simple as calling -:method:`Symfony\\Component\\HttpFoundation\\Response::send`:: - - $response->send(); - -Setting Cookies -~~~~~~~~~~~~~~~ - -The response cookies can be manipulated though the ``headers`` public -attribute:: - - use Symfony\Component\HttpFoundation\Cookie; - - $response->headers->setCookie(new Cookie('foo', 'bar')); - -The -:method:`Symfony\\Component\\HttpFoundation\\ResponseHeaderBag::setCookie` -method takes an instance of -:class:`Symfony\\Component\\HttpFoundation\\Cookie` as an argument. - -You can clear a cookie via the -:method:`Symfony\\Component\\HttpFoundation\\ResponseHeaderBag::clearCookie` method. - -Managing the HTTP Cache -~~~~~~~~~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\HttpFoundation\\Response` class has a rich set -of methods to manipulate the HTTP headers related to the cache: - -* :method:`Symfony\\Component\\HttpFoundation\\Response::setPublic`; -* :method:`Symfony\\Component\\HttpFoundation\\Response::setPrivate`; -* :method:`Symfony\\Component\\HttpFoundation\\Response::expire`; -* :method:`Symfony\\Component\\HttpFoundation\\Response::setExpires`; -* :method:`Symfony\\Component\\HttpFoundation\\Response::setMaxAge`; -* :method:`Symfony\\Component\\HttpFoundation\\Response::setSharedMaxAge`; -* :method:`Symfony\\Component\\HttpFoundation\\Response::setTtl`; -* :method:`Symfony\\Component\\HttpFoundation\\Response::setClientTtl`; -* :method:`Symfony\\Component\\HttpFoundation\\Response::setLastModified`; -* :method:`Symfony\\Component\\HttpFoundation\\Response::setEtag`; -* :method:`Symfony\\Component\\HttpFoundation\\Response::setVary`; - -The :method:`Symfony\\Component\\HttpFoundation\\Response::setCache` method -can be used to set the most commonly used cache information in one method -call:: - - $response->setCache(array( - 'etag' => 'abcdef', - 'last_modified' => new \DateTime(), - 'max_age' => 600, - 's_maxage' => 600, - 'private' => false, - 'public' => true, - )); - -To check if the Response validators (``ETag``, ``Last-Modified``) match a -conditional value specified in the client Request, use the -:method:`Symfony\\Component\\HttpFoundation\\Response::isNotModified` -method:: - - if ($response->isNotModified($request)) { - $response->send(); - } - -If the Response is not modified, it sets the status code to 304 and remove the -actual response content. - -Redirecting the User -~~~~~~~~~~~~~~~~~~~~ - -To redirect the client to another URL, you can use the -:class:`Symfony\\Component\\HttpFoundation\\RedirectResponse` class:: - - use Symfony\Component\HttpFoundation\RedirectResponse; - - $response = new RedirectResponse('http://example.com/'); - -Streaming a Response -~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.1 - Support for streamed responses was added in Symfony 2.1. - -The :class:`Symfony\\Component\\HttpFoundation\\StreamedResponse` class allows -you to stream the Response back to the client. The response content is -represented by a PHP callable instead of a string:: - - use Symfony\Component\HttpFoundation\StreamedResponse; - - $response = new StreamedResponse(); - $response->setCallback(function () { - echo 'Hello World'; - flush(); - sleep(2); - echo 'Hello World'; - flush(); - }); - $response->send(); - -.. note:: - - The ``flush()`` function does not flush buffering. If ``ob_start()`` has - been called before or the ``output_buffering`` php.ini option is enabled, - you must call ``ob_flush()`` before ``flush()``. - - Additionally, PHP isn't the only layer that can buffer output. Your web - server might also buffer based on its configuration. Even more, if you - use fastcgi, buffering can't be disabled at all. - -.. _component-http-foundation-serving-files: - -Serving Files -~~~~~~~~~~~~~ - -.. versionadded:: 2.1 - The ``makeDisposition`` method was added in Symfony 2.1. - -When sending a file, you must add a ``Content-Disposition`` header to your -response. While creating this header for basic file downloads is easy, using -non-ASCII filenames is more involving. The -:method:`Symfony\\Component\\HttpFoundation\\Response::makeDisposition` -abstracts the hard work behind a simple API:: - - use Symfony\Component\HttpFoundation\ResponseHeaderBag; - - $d = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'foo.pdf'); - - $response->headers->set('Content-Disposition', $d); - -.. versionadded:: 2.2 - The :class:`Symfony\\Component\\HttpFoundation\\BinaryFileResponse` - class was added in Symfony 2.2. - -Alternatively, if you are serving a static file, you can use a -:class:`Symfony\\Component\\HttpFoundation\\BinaryFileResponse`:: - - use Symfony\Component\HttpFoundation\BinaryFileResponse - - $file = 'path/to/file.txt'; - $response = new BinaryFileResponse($file); - -The ``BinaryFileResponse`` will automatically handle ``Range`` and -``If-Range`` headers from the request. It also supports ``X-Sendfile`` -(see for `Nginx`_ and `Apache`_). To make use of it, you need to determine -whether or not the ``X-Sendfile-Type`` header should be trusted and call -:method:`Symfony\\Component\\HttpFoundation\\BinaryFileResponse::trustXSendfileTypeHeader` -if it should:: - - $response::trustXSendfileTypeHeader(); - -You can still set the ``Content-Type`` of the sent file, or change its ``Content-Disposition``:: - - $response->headers->set('Content-Type', 'text/plain') - $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'filename.txt'); - - -.. _component-http-foundation-json-response: - -Creating a JSON Response -~~~~~~~~~~~~~~~~~~~~~~~~ - -Any type of response can be created via the -:class:`Symfony\\Component\\HttpFoundation\\Response` class by setting the -right content and headers. A JSON response might look like this:: - - use Symfony\Component\HttpFoundation\Response; - - $response = new Response(); - $response->setContent(json_encode(array( - 'data' => 123, - ))); - $response->headers->set('Content-Type', 'application/json'); - -.. versionadded:: 2.1 - The :class:`Symfony\\Component\\HttpFoundation\\JsonResponse` - class was added in Symfony 2.1. - -There is also a helpful :class:`Symfony\\Component\\HttpFoundation\\JsonResponse` -class, which can make this even easier:: - - use Symfony\Component\HttpFoundation\JsonResponse; - - $response = new JsonResponse(); - $response->setData(array( - 'data' => 123 - )); - -This encodes your array of data to JSON and sets the ``Content-Type`` header -to ``application/json``. - -.. caution:: - - To avoid `JSON Hijacking`_, you should pass an associative array as the - outer-most array to ``JsonResponse`` and not an indexed array so that - the final result is an object (e.g. ``{"object": "not inside an array"}``) - instead of an array (e.g. ``[{"object": "inside an array"}]``). - -JSONP Callback -~~~~~~~~~~~~~~ - -If you're using JSONP, you can set the callback function that the data should -be passed to:: - - $response->setCallback('handleResponse'); - -In this case, the ``Content-Type`` header will be ``text/javascript`` and -the response content will look like this: - -.. code-block:: javascript - - handleResponse({'data': 123}); - -Session -------- - -The session information is in its own document: :doc:`/components/http_foundation/sessions`. - -.. _Packagist: https://packagist.org/packages/symfony/http-foundation -.. _Nginx: http://wiki.nginx.org/XSendfile -.. _Apache: https://tn123.org/mod_xsendfile/ -.. _`JSON Hijacking`: http://haacked.com/archive/2009/06/25/json-hijacking.aspx diff --git a/components/http_foundation/session_configuration.rst b/components/http_foundation/session_configuration.rst deleted file mode 100644 index f445f740b76..00000000000 --- a/components/http_foundation/session_configuration.rst +++ /dev/null @@ -1,263 +0,0 @@ -.. index:: - single: HTTP - single: HttpFoundation, Sessions - -Configuring Sessions and Save Handlers -====================================== - -This section deals with how to configure session management and fine tune it -to your specific needs. This documentation covers save handlers, which -store and retrieve session data, and configuring session behaviour. - -Save Handlers -~~~~~~~~~~~~~ - -The PHP session workflow has 6 possible operations that may occur. The normal -session follows `open`, `read`, `write` and `close`, with the possibility of -`destroy` and `gc` (garbage collection which will expire any old sessions: `gc` -is called randomly according to PHP's configuration and if called, it is invoked -after the `open` operation). You can read more about this at -`php.net/session.customhandler`_ - - -Native PHP Save Handlers ------------------------- - -So-called 'native' handlers, are save handlers which are either compiled into -PHP or provided by PHP extensions, such as PHP-Sqlite, PHP-Memcached and so on. - -All native save handlers are internal to PHP and as such, have no public facing API. -They must be configured by PHP ini directives, usually ``session.save_path`` and -potentially other driver specific directives. Specific details can be found in -docblock of the ``setOptions()`` method of each class. - -While native save handlers can be activated by directly using -``ini_set('session.save_handler', $name);``, Symfony2 provides a convenient way to -activate these in the same way as custom handlers. - -Symfony2 provides drivers for the following native save handler as an example: - - * :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NativeFileSessionHandler` - -Example usage:: - - use Symfony\Component\HttpFoundation\Session\Session; - use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; - use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler; - - $storage = new NativeSessionStorage(array(), new NativeFileSessionHandler()); - $session = new Session($storage); - -.. note:: - - With the exception of the ``files`` handler which is built into PHP and always available, - the availability of the other handlers depends on those PHP extensions being active at runtime. - -.. note:: - - Native save handlers provide a quick solution to session storage, however, in complex systems - where you need more control, custom save handlers may provide more freedom and flexibility. - Symfony2 provides several implementations which you may further customise as required. - - -Custom Save Handlers --------------------- - -Custom handlers are those which completely replace PHP's built in session save -handlers by providing six callback functions which PHP calls internally at -various points in the session workflow. - -Symfony2 HttpFoundation provides some by default and these can easily serve as -examples if you wish to write your own. - - * :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\PdoSessionHandler` - * :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MemcacheSessionHandler` - * :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MemcachedSessionHandler` - * :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MongoDbSessionHandler` - * :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NullSessionHandler` - -Example usage:: - - use Symfony\Component\HttpFoundation\Session\Session; - use Symfony\Component\HttpFoundation\Session\Storage\SessionStorage; - use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; - - $storage = new NativeSessionStorage(array(), new PdoSessionHandler()); - $session = new Session($storage); - - -Configuring PHP Sessions -~~~~~~~~~~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage` -can configure most of the PHP ini configuration directives which are documented -at `php.net/session.configuration`_. - -To configure these settings, pass the keys (omitting the initial ``session.`` part -of the key) as a key-value array to the ``$options`` constructor argument. -Or set them via the -:method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage::setOptions` -method. - -For the sake of clarity, some key options are explained in this documentation. - -Session Cookie Lifetime -~~~~~~~~~~~~~~~~~~~~~~~ - -For security, session tokens are generally recommended to be sent as session cookies. -You can configure the lifetime of session cookies by specifying the lifetime -(in seconds) using the ``cookie_lifetime`` key in the constructor's ``$options`` -argument in :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage`. - -Setting a ``cookie_lifetime`` to ``0`` will cause the cookie to live only as -long as the browser remains open. Generally, ``cookie_lifetime`` would be set to -a relatively large number of days, weeks or months. It is not uncommon to set -cookies for a year or more depending on the application. - -Since session cookies are just a client-side token, they are less important in -controlling the fine details of your security settings which ultimately can only -be securely controlled from the server side. - -.. note:: - - The ``cookie_lifetime`` setting is the number of seconds the cookie should live - for, it is not a Unix timestamp. The resulting session cookie will be stamped - with an expiry time of ``time()``+``cookie_lifetime`` where the time is taken - from the server. - -Configuring Garbage Collection -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When a session opens, PHP will call the ``gc`` handler randomly according to the -probability set by ``session.gc_probability`` / ``session.gc_divisor``. For -example if these were set to ``5/100`` respectively, it would mean a probability -of 5%. Similarly, ``3/4`` would mean a 3 in 4 chance of being called, i.e. 75%. - -If the garbage collection handler is invoked, PHP will pass the value stored in -the PHP ini directive ``session.gc_maxlifetime``. The meaning in this context is -that any stored session that was saved more than ``maxlifetime`` ago should be -deleted. This allows one to expire records based on idle time. - -You can configure these settings by passing ``gc_probability``, ``gc_divisor`` -and ``gc_maxlifetime`` in an array to the constructor of -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage` -or to the :method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage::setOptions` -method. - -Session Lifetime -~~~~~~~~~~~~~~~~ - -When a new session is created, meaning Symfony2 issues a new session cookie -to the client, the cookie will be stamped with an expiry time. This is -calculated by adding the PHP runtime configuration value in -``session.cookie_lifetime`` with the current server time. - -.. note:: - - PHP will only issue a cookie once. The client is expected to store that cookie - for the entire lifetime. A new cookie will only be issued when the session is - destroyed, the browser cookie is deleted, or the session ID is regenerated - using the ``migrate()`` or ``invalidate()`` methods of the ``Session`` class. - - The initial cookie lifetime can be set by configuring ``NativeSessionStorage`` - using the ``setOptions(array('cookie_lifetime' => 1234))`` method. - -.. note:: - - A cookie lifetime of ``0`` means the cookie expires when the browser is closed. - -Session Idle Time/Keep Alive -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There are often circumstances where you may want to protect, or minimize -unauthorized use of a session when a user steps away from their terminal while -logged in by destroying the session after a certain period of idle time. For -example, it is common for banking applications to log the user out after just -5 to 10 minutes of inactivity. Setting the cookie lifetime here is not -appropriate because that can be manipulated by the client, so we must do the expiry -on the server side. The easiest way is to implement this via garbage collection -which runs reasonably frequently. The cookie ``lifetime`` would be set to a -relatively high value, and the garbage collection ``maxlifetime`` would be set -to destroy sessions at whatever the desired idle period is. - -The other option is to specifically checking if a session has expired after the -session is started. The session can be destroyed as required. This method of -processing can allow the expiry of sessions to be integrated into the user -experience, for example, by displaying a message. - -Symfony2 records some basic meta-data about each session to give you complete -freedom in this area. - -Session meta-data -~~~~~~~~~~~~~~~~~ - -Sessions are decorated with some basic meta-data to enable fine control over the -security settings. The session object has a getter for the meta-data, -:method:`Symfony\\Component\\HttpFoundation\\Session\\Session::getMetadataBag` which -exposes an instance of :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\MetadataBag`:: - - $session->getMetadataBag()->getCreated(); - $session->getMetadataBag()->getLastUsed(); - -Both methods return a Unix timestamp (relative to the server). - -This meta-data can be used to explicitly expire a session on access, e.g.:: - - $session->start(); - if (time() - $session->getMetadataBag()->getLastUsed() > $maxIdleTime) { - $session->invalidate(); - throw new SessionExpired(); // redirect to expired session page - } - -It is also possible to tell what the ``cookie_lifetime`` was set to for a -particular cookie by reading the ``getLifetime()`` method:: - - $session->getMetadataBag()->getLifetime(); - -The expiry time of the cookie can be determined by adding the created -timestamp and the lifetime. - -PHP 5.4 compatibility -~~~~~~~~~~~~~~~~~~~~~ - -Since PHP 5.4.0, :phpclass:`SessionHandler` and :phpclass:`SessionHandlerInterface` -are available. Symfony 2.1 provides forward compatibility for the :phpclass:`SessionHandlerInterface` -so it can be used under PHP 5.3. This greatly improves inter-operability with other -libraries. - -:phpclass:`SessionHandler` is a special PHP internal class which exposes native save -handlers to PHP user-space. - -In order to provide a solution for those using PHP 5.4, Symfony2 has a special -class called :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NativeSessionHandler` -which under PHP 5.4, extends from `\SessionHandler` and under PHP 5.3 is just a -empty base class. This provides some interesting opportunities to leverage -PHP 5.4 functionality if it is available. - -Save Handler Proxy -~~~~~~~~~~~~~~~~~~ - -There are two kinds of save handler class proxies which inherit from -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\AbstractProxy`: -they are :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NativeProxy` -and :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\SessionHandlerProxy`. - -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage` -automatically injects storage handlers into a save handler proxy unless already -wrapped by one. - -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NativeProxy` -is used automatically under PHP 5.3 when internal PHP save handlers are specified -using the `Native*SessionHandler` classes, while -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\SessionHandlerProxy` -will be used to wrap any custom save handlers, that implement :phpclass:`SessionHandlerInterface`. - -Under PHP 5.4 and above, all session handlers implement :phpclass:`SessionHandlerInterface` -including `Native*SessionHandler` classes which inherit from :phpclass:`SessionHandler`. - -The proxy mechanism allows you to get more deeply involved in session save handler -classes. A proxy for example could be used to encrypt any session transaction -without knowledge of the specific save handler. - -.. _`php.net/session.customhandler`: http://php.net/session.customhandler -.. _`php.net/session.configuration`: http://php.net/session.configuration diff --git a/components/http_foundation/session_php_bridge.rst b/components/http_foundation/session_php_bridge.rst deleted file mode 100644 index 5b55417d983..00000000000 --- a/components/http_foundation/session_php_bridge.rst +++ /dev/null @@ -1,49 +0,0 @@ -.. index:: - single: HTTP - single: HttpFoundation, Sessions - -Integrating with Legacy Sessions -================================ - -Sometimes it may be necessary to integrate Symfony into a legacy application -where you do not initially have the level of control you require. - -As stated elsewhere, Symfony Sessions are designed to replace the use of -PHP's native ``session_*()`` functions and use of the ``$_SESSION`` -superglobal. Additionally, it is mandatory for Symfony to start the session. - -However when there really are circumstances where this is not possible, you -can use a special storage bridge -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\PhpBridgeSessionStorage` -which is designed to allow Symfony to work with a session started outside of -the Symfony Session framework. You are warned that things can interrupt this -use-case unless you are careful: for example the legacy application erases -``$_SESSION``. - -A typical use of this might look like this:: - - use Symfony\Component\HttpFoundation\Session\Session; - use Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorage; - - // legacy application configures session - ini_set('session.save_handler', 'files'); - ini_set('session.save_path', '/tmp'); - session_start(); - - // Get Symfony to interface with this existing session - $session = new Session(new PhpBridgeSessionStorage()); - - // symfony will now interface with the existing PHP session - $session->start(); - -This will allow you to start using the Symfony Session API and allow migration -of your application to Symfony sessions. - -.. note:: - - Symfony sessions store data like attributes in special 'Bags' which use a - key in the ``$_SESSION`` superglobal. This means that a Symfony session - cannot access arbitrary keys in ``$_SESSION`` that may be set by the legacy - application, although all the ``$_SESSION`` contents will be saved when - the session is saved. - diff --git a/components/http_foundation/session_testing.rst b/components/http_foundation/session_testing.rst deleted file mode 100644 index 7a938b924b9..00000000000 --- a/components/http_foundation/session_testing.rst +++ /dev/null @@ -1,59 +0,0 @@ -.. index:: - single: HTTP - single: HttpFoundation, Sessions - -Testing with Sessions -===================== - -Symfony2 is designed from the ground up with code-testability in mind. In order -to make your code which utilizes session easily testable we provide two separate -mock storage mechanisms for both unit testing and functional testing. - -Testing code using real sessions is tricky because PHP's workflow state is global -and it is not possible to have multiple concurrent sessions in the same PHP -process. - -The mock storage engines simulate the PHP session workflow without actually -starting one allowing you to test your code without complications. You may also -run multiple instances in the same PHP process. - -The mock storage drivers do not read or write the system globals -`session_id()` or `session_name()`. Methods are provided to simulate this if -required: - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\SessionStorageInterface::getId`: Gets the - session ID. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\SessionStorageInterface::setId`: Sets the - session ID. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\SessionStorageInterface::getName`: Gets the - session name. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\SessionStorageInterface::setName`: Sets the - session name. - -Unit Testing ------------- - -For unit testing where it is not necessary to persist the session, you should -simply swap out the default storage engine with -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockArraySessionStorage`:: - - use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; - use Symfony\Component\HttpFoundation\Session\Session; - - $session = new Session(new MockArraySessionStorage()); - -Functional Testing ------------------- - -For functional testing where you may need to persist session data across -separate PHP processes, simply change the storage engine to -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockFileSessionStorage`:: - - use Symfony\Component\HttpFoundation\Session\Session; - use Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage; - - $session = new Session(new MockFileSessionStorage()); - diff --git a/components/http_foundation/sessions.rst b/components/http_foundation/sessions.rst deleted file mode 100644 index dbd855f4bf0..00000000000 --- a/components/http_foundation/sessions.rst +++ /dev/null @@ -1,335 +0,0 @@ -.. index:: - single: HTTP - single: HttpFoundation, Sessions - -Session Management -================== - -The Symfony2 HttpFoundation Component has a very powerful and flexible session -subsystem which is designed to provide session management through a simple -object-oriented interface using a variety of session storage drivers. - -.. versionadded:: 2.1 - The :class:`Symfony\\Component\\HttpFoundation\\Session\\SessionInterface` interface, - as well as a number of other changes, are new as of Symfony 2.1. - -Sessions are used via the simple :class:`Symfony\\Component\\HttpFoundation\\Session\\Session` -implementation of :class:`Symfony\\Component\\HttpFoundation\\Session\\SessionInterface` interface. - -Quick example:: - - use Symfony\Component\HttpFoundation\Session\Session; - - $session = new Session(); - $session->start(); - - // set and get session attributes - $session->set('name', 'Drak'); - $session->get('name'); - - // set flash messages - $session->getFlashBag()->add('notice', 'Profile updated'); - - // retrieve messages - foreach ($session->getFlashBag()->get('notice', array()) as $message) { - echo "
$message
"; - } - -.. note:: - - Symfony sessions are designed to replace several native PHP functions. - Applications should avoid using ``session_start()``, ``session_regenerate_id()``, - ``session_id()``, ``session_name()``, and ``session_destroy()`` and instead - use the APIs in the following section. - -.. note:: - - While it is recommended to explicitly start a session, a sessions will actually - start on demand, that is, if any session request is made to read/write session - data. - -.. caution:: - - Symfony sessions are incompatible with PHP ini directive ``session.auto_start = 1`` - This directive should be turned off in ``php.ini``, in the webserver directives or - in ``.htaccess``. - -Session API -~~~~~~~~~~~ - -The :class:`Symfony\\Component\\HttpFoundation\\Session\\Session` class implements -:class:`Symfony\\Component\\HttpFoundation\\Session\\SessionInterface`. - -The :class:`Symfony\\Component\\HttpFoundation\\Session\\Session` has a simple API -as follows divided into a couple of groups. - -Session workflow - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::start`: - Starts the session - do not use ``session_start()``. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::migrate`: - Regenerates the session ID - do not use ``session_regenerate_id()``. - This method can optionally change the lifetime of the new cookie that will - be emitted by calling this method. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::invalidate`: - Clears all session data and regenerates session ID. Do not use ``session_destroy()``. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::getId`: Gets the - session ID. Do not use ``session_id()``. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::setId`: Sets the - session ID. Do not use ``session_id()``. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::getName`: Gets the - session name. Do not use ``session_name()``. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::setName`: Sets the - session name. Do not use ``session_name()``. - -Session attributes - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::set`: - Sets an attribute by key; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::get`: - Gets an attribute by key; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::all`: - Gets all attributes as an array of key => value; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::has`: - Returns true if the attribute exists; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::keys`: - Returns an array of stored attribute keys; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::replace`: - Sets multiple attributes at once: takes a keyed array and sets each key => value pair. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::remove`: - Deletes an attribute by key; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::clear`: - Clear all attributes; - -The attributes are stored internally in an "Bag", a PHP object that acts like -an array. A few methods exist for "Bag" management: - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::registerBag`: - Registers a :class:`Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface` - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::getBag`: - Gets a :class:`Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface` by - bag name. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::getFlashBag`: - Gets the :class:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface`. - This is just a shortcut for convenience. - -Session meta-data - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Session::getMetadataBag`: - Gets the :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\MetadataBag` - which contains information about the session. - - -Session Data Management -~~~~~~~~~~~~~~~~~~~~~~~ - -PHP's session management requires the use of the ``$_SESSION`` super-global, -however, this interferes somewhat with code testability and encapsulation in a -OOP paradigm. To help overcome this, Symfony2 uses 'session bags' linked to the -session to encapsulate a specific dataset of 'attributes' or 'flash messages'. - -This approach also mitigates namespace pollution within the ``$_SESSION`` -super-global because each bag stores all its data under a unique namespace. -This allows Symfony2 to peacefully co-exist with other applications or libraries -that might use the ``$_SESSION`` super-global and all data remains completely -compatible with Symfony2's session management. - -Symfony2 provides 2 kinds of storage bags, with two separate implementations. -Everything is written against interfaces so you may extend or create your own -bag types if necessary. - -:class:`Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface` has -the following API which is intended mainly for internal purposes: - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface::getStorageKey`: - Returns the key which the bag will ultimately store its array under in ``$_SESSION``. - Generally this value can be left at its default and is for internal use. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface::initialize`: - This is called internally by Symfony2 session storage classes to link bag data - to the session. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface::getName`: - Returns the name of the session bag. - - -Attributes -~~~~~~~~~~ - -The purpose of the bags implementing the :class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface` -is to handle session attribute storage. This might include things like user ID, -and remember me login settings or other user based state information. - -* :class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBag` - This is the standard default implementation. - -* :class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\NamespacedAttributeBag` - This implementation allows for attributes to be stored in a structured namespace. - -Any plain `key => value` storage system is limited in the extent to which -complex data can be stored since each key must be unique. You can achieve -namespacing by introducing a naming convention to the keys so different parts of -your application could operate without clashing. For example, `module1.foo` and -`module2.foo`. However, sometimes this is not very practical when the attributes -data is an array, for example a set of tokens. In this case, managing the array -becomes a burden because you have to retrieve the array then process it and -store it again:: - - $tokens = array('tokens' => array('a' => 'a6c1e0b6', - 'b' => 'f4a7b1f3')); - -So any processing of this might quickly get ugly, even simply adding a token to -the array:: - - $tokens = $session->get('tokens'); - $tokens['c'] = $value; - $session->set('tokens', $tokens); - -With structured namespacing, the key can be translated to the array -structure like this using a namespace character (defaults to `/`):: - - $session->set('tokens/c', $value); - -This way you can easily access a key within the stored array directly and easily. - -:class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface` -has a simple API - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::set`: - Sets an attribute by key; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::get`: - Gets an attribute by key; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::all`: - Gets all attributes as an array of key => value; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::has`: - Returns true if the attribute exists; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::keys`: - Returns an array of stored attribute keys; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::replace`: - Sets multiple attributes at once: takes a keyed array and sets each key => value pair. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::remove`: - Deletes an attribute by key; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::clear`: - Clear the bag; - - -Flash messages -~~~~~~~~~~~~~~ - -The purpose of the :class:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface` -is to provide a way of setting and retrieving messages on a per session basis. -The usual workflow for flash messages would be set in an request, and displayed -after a page redirect. For example, a user submits a form which hits an update -controller, and after processing the controller redirects the page to either the -updated page or an error page. Flash messages set in the previous page request -would be displayed immediately on the subsequent page load for that session. -This is however just one application for flash messages. - -* :class:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\AutoExpireFlashBag` - This implementation messages set in one page-load will - be available for display only on the next page load. These messages will auto - expire regardless of if they are retrieved or not. - -* :class:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBag` - In this implementation, messages will remain in the session until - they are explicitly retrieved or cleared. This makes it possible to use ESI - caching. - -:class:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface` -has a simple API - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::add`: - Adds a flash message to the stack of specified type; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::set`: - Sets flashes by type; This method conveniently takes both singles messages as - a ``string`` or multiple messages in an ``array``. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::get`: - Gets flashes by type and clears those flashes from the bag; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::setAll`: - Sets all flashes, accepts a keyed array of arrays ``type => array(messages)``; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::all`: - Gets all flashes (as a keyed array of arrays) and clears the flashes from the bag; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::peek`: - Gets flashes by type (read only); - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::peekAll`: - Gets all flashes (read only) as keyed array of arrays; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::has`: - Returns true if the type exists, false if not; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::keys`: - Returns an array of the stored flash types; - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::clear`: - Clears the bag; - -For simple applications it is usually sufficient to have one flash message per -type, for example a confirmation notice after a form is submitted. However, -flash messages are stored in a keyed array by flash ``$type`` which means your -application can issue multiple messages for a given type. This allows the API -to be used for more complex messaging in your application. - -Examples of setting multiple flashes:: - - use Symfony\Component\HttpFoundation\Session\Session; - - $session = new Session(); - $session->start(); - - // add flash messages - $session->getFlashBag()->add( - 'warning', - 'Your config file is writable, it should be set read-only' - ); - $session->getFlashBag()->add('error', 'Failed to update name'); - $session->getFlashBag()->add('error', 'Another error'); - -Displaying the flash messages might look like this: - -Simple, display one type of message:: - - // display warnings - foreach ($session->getFlashBag()->get('warning', array()) as $message) { - echo "
$message
"; - } - - // display errors - foreach ($session->getFlashBag()->get('error', array()) as $message) { - echo "
$message
"; - } - -Compact method to process display all flashes at once:: - - foreach ($session->getFlashBag()->all() as $type => $messages) { - foreach ($messages as $message) { - echo "
$message
\n"; - } - } diff --git a/components/http_foundation/trusting_proxies.rst b/components/http_foundation/trusting_proxies.rst deleted file mode 100644 index 9309c00f975..00000000000 --- a/components/http_foundation/trusting_proxies.rst +++ /dev/null @@ -1,55 +0,0 @@ -.. index:: - single: Request; Trusted Proxies - -Trusting Proxies -================ - -If you find yourself behind some sort of proxy - like a load balancer - then -certain header information may be sent to you using special ``X-Forwarded-*`` -headers. For example, the ``Host`` HTTP header is usually used to return -the requested host. But when you're behind a proxy, the true host may be -stored in a ``X-Forwarded-Host`` header. - -Since HTTP headers can be spoofed, Symfony2 does *not* trust these proxy -headers by default. If you are behind a proxy, you should manually whitelist -your proxy. - -.. versionadded:: 2.3 - CIDR notation support was introduced, so you can whitelist whole - subnets (e.g. ``10.0.0.0/8``, ``fc00::/7``). - -.. code-block:: php - - use Symfony\Component\HttpFoundation\Request; - - $request = Request::createFromGlobals(); - // only trust proxy headers coming from this IP addresses - $request->setTrustedProxies(array('192.0.0.1', '10.0.0.0/8')); - -Configuring Header Names ------------------------- - -By default, the following proxy headers are trusted: - -* ``X-Forwarded-For`` Used in :method:`Symfony\\Component\\HttpFoundation\\Request::getClientIp`; -* ``X-Forwarded-Host`` Used in :method:`Symfony\\Component\\HttpFoundation\\Request::getHost`; -* ``X-Forwarded-Port`` Used in :method:`Symfony\\Component\\HttpFoundation\\Request::getPort`; -* ``X-Forwarded-Proto`` Used in :method:`Symfony\\Component\\HttpFoundation\\Request::getScheme` and :method:`Symfony\\Component\\HttpFoundation\\Request::isSecure`; - -If your reverse proxy uses a different header name for any of these, you -can configure that header name via :method:`Symfony\\Component\\HttpFoundation\\Request::setTrustedHeaderName`:: - - $request->setTrustedHeaderName(Request::HEADER_CLIENT_IP, 'X-Proxy-For'); - $request->setTrustedHeaderName(Request::HEADER_CLIENT_HOST, 'X-Proxy-Host'); - $request->setTrustedHeaderName(Request::HEADER_CLIENT_PORT, 'X-Proxy-Port'); - $request->setTrustedHeaderName(Request::HEADER_CLIENT_PROTO, 'X-Proxy-Proto'); - -Not trusting certain Headers ----------------------------- - -By default, if you whitelist your proxy's IP address, then all four headers -listed above are trusted. If you need to trust some of these headers but -not others, you can do that as well:: - - // disables trusting the ``X-Forwarded-Proto`` header, the default header is used - $request->setTrustedHeaderName(Request::HEADER_CLIENT_PROTO, ''); diff --git a/components/http_kernel.rst b/components/http_kernel.rst new file mode 100644 index 00000000000..62d1e92d89b --- /dev/null +++ b/components/http_kernel.rst @@ -0,0 +1,761 @@ +The HttpKernel Component +======================== + + The HttpKernel component provides a structured process for converting + a ``Request`` into a ``Response`` by making use of the EventDispatcher + component. It's flexible enough to create a full-stack framework (Symfony) + or an advanced CMS (Drupal). + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/http-kernel + +.. include:: /components/require_autoload.rst.inc + +.. _the-workflow-of-a-request: + +The Request-Response Lifecycle +------------------------------ + +.. seealso:: + + This article explains how to use the HttpKernel features as an independent + component in any PHP application. In Symfony applications everything is + already configured and ready to use. Read the :doc:`/controller` and + :doc:`/event_dispatcher` articles to learn about how to use it to create + controllers and define events in Symfony applications. + +Every HTTP web interaction begins with a request and ends with a response. +Your job as a developer is to create PHP code that reads the request information +(e.g. the URL) and creates and returns a response (e.g. an HTML page or JSON string). +This is a simplified overview of the request-response lifecycle in Symfony applications: + +#. The **user** asks for a **resource** in a **browser**; +#. The **browser** sends a **request** to the **server**; +#. **Symfony** gives the **application** a **Request** object; +#. The **application** generates a **Response** object using the data of the **Request** object; +#. The **server** sends back the **response** to the **browser**; +#. The **browser** displays the **resource** to the **user**. + +Typically, some sort of framework or system is built to handle all the repetitive +tasks (e.g. routing, security, etc) so that a developer can build each *page* of +the application. Exactly *how* these systems are built varies greatly. The HttpKernel +component provides an interface that formalizes the process of starting with a +request and creating the appropriate response. The component is meant to be the +heart of any application or framework, no matter how varied the architecture of +that system:: + + namespace Symfony\Component\HttpKernel; + + use Symfony\Component\HttpFoundation\Request; + + interface HttpKernelInterface + { + // ... + + /** + * @return Response A Response instance + */ + public function handle( + Request $request, + int $type = self::MAIN_REQUEST, + bool $catch = true + ): Response; + } + +Internally, :method:`HttpKernel::handle() ` - +the concrete implementation of :method:`HttpKernelInterface::handle() ` - +defines a lifecycle that starts with a :class:`Symfony\\Component\\HttpFoundation\\Request` +and ends with a :class:`Symfony\\Component\\HttpFoundation\\Response`. + +.. raw:: html + + + +The exact details of this lifecycle are the key to understanding how the kernel +(and the Symfony Framework or any other library that uses the kernel) works. + +HttpKernel: Driven by Events +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``HttpKernel::handle()`` method works internally by dispatching events. +This makes the method both flexible, but also a bit abstract, since all the +"work" of a framework/application built with HttpKernel is actually done +in event listeners. + +To help explain this process, this document looks at each step of the process +and talks about how one specific implementation of the HttpKernel - the Symfony +Framework - works. + +Initially, using the :class:`Symfony\\Component\\HttpKernel\\HttpKernel` does +not take many steps. You create an +:doc:`event dispatcher ` and a +:ref:`controller and argument resolver ` +(explained below). To complete your working kernel, you'll add more event +listeners to the events discussed below:: + + use Symfony\Component\EventDispatcher\EventDispatcher; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\RequestStack; + use Symfony\Component\HttpKernel\Controller\ArgumentResolver; + use Symfony\Component\HttpKernel\Controller\ControllerResolver; + use Symfony\Component\HttpKernel\HttpKernel; + + // create the Request object + $request = Request::createFromGlobals(); + + $dispatcher = new EventDispatcher(); + // ... add some event listeners + + // create your controller and argument resolvers + $controllerResolver = new ControllerResolver(); + $argumentResolver = new ArgumentResolver(); + + // instantiate the kernel + $kernel = new HttpKernel($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver); + + // actually execute the kernel, which turns the request into a response + // by dispatching events, calling a controller, and returning the response + $response = $kernel->handle($request); + + // send the headers and echo the content + $response->send(); + + // trigger the kernel.terminate event + $kernel->terminate($request, $response); + +See ":ref:`A full working example `" for a more concrete implementation. + +For general information on adding listeners to the events below, see +:ref:`Creating an Event Listener `. + +.. seealso:: + + There is a wonderful tutorial series on using the HttpKernel component and + other Symfony components to create your own framework. See + :doc:`/create_framework/introduction`. + +.. _component-http-kernel-kernel-request: + +1) The ``kernel.request`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Typical Purposes**: To add more information to the ``Request``, initialize +parts of the system, or return a ``Response`` if possible (e.g. a security +layer that denies access). + +:ref:`Kernel Events Information Table ` + +The first event that is dispatched inside :method:`HttpKernel::handle ` +is ``kernel.request``, which may have a variety of different listeners. + +Listeners of this event can be quite varied. Some listeners - such as a security +listener - might have enough information to create a ``Response`` object immediately. +For example, if a security listener determined that a user doesn't have access, +that listener may return a :class:`Symfony\\Component\\HttpFoundation\\RedirectResponse` +to the login page or a 403 Access Denied response. + +If a ``Response`` is returned at this stage, the process skips directly to +the :ref:`kernel.response ` event. + +Other listeners initialize things or add more information to the request. +For example, a listener might determine and set the locale on the ``Request`` +object. + +Another common listener is routing. A router listener may process the ``Request`` +and determine the controller that should be rendered (see the next section). +In fact, the ``Request`` object has an ":ref:`attributes `" +bag which is a perfect spot to store this extra, application-specific data +about the request. This means that if your router listener somehow determines +the controller, it can store it on the ``Request`` attributes (which can be used +by your controller resolver). + +Overall, the purpose of the ``kernel.request`` event is either to create and +return a ``Response`` directly, or to add information to the ``Request`` +(e.g. setting the locale or setting some other information on the ``Request`` +attributes). + +.. note:: + + When setting a response for the ``kernel.request`` event, the propagation + is stopped. This means listeners with lower priority won't be executed. + +.. sidebar:: ``kernel.request`` in the Symfony Framework + + The most important listener to ``kernel.request`` in the Symfony Framework + is the :class:`Symfony\\Component\\HttpKernel\\EventListener\\RouterListener`. + This class executes the routing layer, which returns an *array* of information + about the matched request, including the ``_controller`` and any placeholders + that are in the route's pattern (e.g. ``{slug}``). See the + :doc:`Routing documentation
`. + + This array of information is stored in the :class:`Symfony\\Component\\HttpFoundation\\Request` + object's ``attributes`` array. Adding the routing information here doesn't + do anything yet, but is used next when resolving the controller. + +.. _component-http-kernel-resolve-controller: + +2) Resolve the Controller +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Assuming that no ``kernel.request`` listener was able to create a ``Response``, +the next step in HttpKernel is to determine and prepare (i.e. resolve) the +controller. The controller is the part of the end-application's code that +is responsible for creating and returning the ``Response`` for a specific page. +The only requirement is that it is a PHP callable - i.e. a function, method +on an object or a ``Closure``. + +But *how* you determine the exact controller for a request is entirely up +to your application. This is the job of the "controller resolver" - a class +that implements :class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface` +and is one of the constructor arguments to ``HttpKernel``. + +Your job is to create a class that implements the interface and fill in its +method: ``getController()``. In fact, one default implementation already +exists, which you can use directly or learn from: +:class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver`. +This implementation is explained more in the sidebar below:: + + namespace Symfony\Component\HttpKernel\Controller; + + use Symfony\Component\HttpFoundation\Request; + + interface ControllerResolverInterface + { + public function getController(Request $request): callable|false; + } + +Internally, the ``HttpKernel::handle()`` method first calls +:method:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface::getController` +on the controller resolver. This method is passed the ``Request`` and is responsible +for somehow determining and returning a PHP callable (the controller) based +on the request's information. + +.. sidebar:: Resolving the Controller in the Symfony Framework + + The Symfony Framework uses the built-in + :class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver` + class (actually, it uses a subclass with some extra functionality + mentioned below). This class leverages the information that was placed + on the ``Request`` object's ``attributes`` property during the ``RouterListener``. + + **getController** + + The ``ControllerResolver`` looks for a ``_controller`` + key on the ``Request`` object's attributes property (recall that this + information is typically placed on the ``Request`` via the ``RouterListener``). + This string is then transformed into a PHP callable by doing the following: + + a) If the ``_controller`` key doesn't follow the recommended PHP namespace + format (e.g. ``App\Controller\DefaultController::index``) its format is + transformed into it. For example, the legacy ``FooBundle:Default:index`` + format would be changed to ``Acme\FooBundle\Controller\DefaultController::indexAction``. + This transformation is specific to the :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\ControllerResolver` + sub-class used by the Symfony Framework. + + b) A new instance of your controller class is instantiated with no + constructor arguments. + +.. _component-http-kernel-kernel-controller: + +3) The ``kernel.controller`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Typical Purposes**: Initialize things or change the controller just before +the controller is executed. + +:ref:`Kernel Events Information Table ` + +After the controller callable has been determined, ``HttpKernel::handle()`` +dispatches the ``kernel.controller`` event. Listeners to this event might initialize +some part of the system that needs to be initialized after certain things +have been determined (e.g. the controller, routing information) but before +the controller is executed. + +Another typical use-case for this event is to retrieve the attributes from +the controller using the :method:`Symfony\\Component\\HttpKernel\\Event\\ControllerEvent::getAttributes` +method. See the Symfony section below for some examples. + +Listeners to this event can also change the controller callable completely +by calling :method:`ControllerEvent::setController ` +on the event object that's passed to listeners on this event. + +.. sidebar:: ``kernel.controller`` in the Symfony Framework + + An interesting listener to ``kernel.controller`` in the Symfony + Framework is :class:`Symfony\\Component\\HttpKernel\\EventListener\\CacheAttributeListener`. + This class fetches ``#[Cache]`` attribute configuration from the + controller and uses it to configure :doc:`HTTP caching ` + on the response. + + There are a few other minor listeners to the ``kernel.controller`` event in + the Symfony Framework that deal with collecting profiler data when the + profiler is enabled. + +4) Getting the Controller Arguments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Next, ``HttpKernel::handle()`` calls +:method:`ArgumentResolverInterface::getArguments() `. +Remember that the controller returned in ``getController()`` is a callable. +The purpose of ``getArguments()`` is to return the array of arguments that +should be passed to that controller. Exactly how this is done is completely +up to your design, though the built-in :class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver` +is a good example. + +At this point the kernel has a PHP callable (the controller) and an array +of arguments that should be passed when executing that callable. + +.. sidebar:: Getting the Controller Arguments in the Symfony Framework + + Now that you know exactly what the controller callable (usually a method + inside a controller object) is, the ``ArgumentResolver`` uses `reflection`_ + on the callable to return an array of the *names* of each of the arguments. + It then iterates over each of these arguments and uses the following tricks + to determine which value should be passed for each argument: + + a) If the ``Request`` attributes bag contains a key that matches the name + of the argument, that value is used. For example, if the first argument + to a controller is ``$slug`` and there is a ``slug`` key in the ``Request`` + ``attributes`` bag, that value is used (and typically this value came + from the ``RouterListener``). + + b) If the argument in the controller is type-hinted with Symfony's + :class:`Symfony\\Component\\HttpFoundation\\Request` object, the + ``Request`` is passed in as the value. + + c) If the function or method argument is `variadic`_ and the ``Request`` + ``attributes`` bag contains an array for that argument, they will all be + available through the `variadic`_ argument. + + This functionality is provided by resolvers implementing the + :class:`Symfony\\Component\\HttpKernel\\Controller\\ValueResolverInterface`. + There are four implementations which provide the default behavior of + Symfony but customization is the key here. By implementing the + ``ValueResolverInterface`` yourself and passing this to the + ``ArgumentResolver``, you can extend this functionality. + +.. _component-http-kernel-calling-controller: + +5) Calling the Controller +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The next step of ``HttpKernel::handle()`` is executing the controller. + +The job of the controller is to build the response for the given resource. +This could be an HTML page, a JSON string or anything else. Unlike every +other part of the process so far, this step is implemented by the "end-developer", +for each page that is built. + +Usually, the controller will return a ``Response`` object. If this is true, +then the work of the kernel is just about done! In this case, the next step +is the :ref:`kernel.response ` event. + +But if the controller returns anything besides a ``Response``, then the kernel +has a little bit more work to do - :ref:`kernel.view ` +(since the end goal is *always* to generate a ``Response`` object). + +.. note:: + + A controller must return *something*. If a controller returns ``null``, + an exception will be thrown immediately. + +.. _component-http-kernel-kernel-view: + +6) The ``kernel.view`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Typical Purposes**: Transform a non-``Response`` return value from a controller +into a ``Response`` + +:ref:`Kernel Events Information Table ` + +If the controller doesn't return a ``Response`` object, then the kernel dispatches +another event - ``kernel.view``. The job of a listener to this event is to +use the return value of the controller (e.g. an array of data or an object) +to create a ``Response``. + +This can be useful if you want to use a "view" layer: instead of returning +a ``Response`` from the controller, you return data that represents the page. +A listener to this event could then use this data to create a ``Response`` that +is in the correct format (e.g HTML, JSON, etc). + +At this stage, if no listener sets a response on the event, then an exception +is thrown: either the controller *or* one of the view listeners must always +return a ``Response``. + +.. note:: + + When setting a response for the ``kernel.view`` event, the propagation + is stopped. This means listeners with lower priority won't be executed. + +.. sidebar:: ``kernel.view`` in the Symfony Framework + + There is a default listener inside the Symfony Framework for the ``kernel.view`` + event. If your controller action returns an array, and you apply the + :ref:`#[Template] attribute ` to that + controller action, then this listener renders a template, passes the array + you returned from your controller to that template, and creates a ``Response`` + containing the returned content from that template. + + Additionally, a popular community bundle `FOSRestBundle`_ implements + a listener on this event which aims to give you a robust view layer + capable of using a single controller to return many different content-type + responses (e.g. HTML, JSON, XML, etc). + +.. _component-http-kernel-kernel-response: + +7) The ``kernel.response`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Typical Purposes**: Modify the ``Response`` object just before it is sent + +:ref:`Kernel Events Information Table ` + +The end goal of the kernel is to transform a ``Request`` into a ``Response``. The +``Response`` might be created during the :ref:`kernel.request ` +event, returned from the :ref:`controller `, +or returned by one of the listeners to the :ref:`kernel.view ` +event. + +Regardless of who creates the ``Response``, another event - ``kernel.response`` +is dispatched directly afterwards. A typical listener to this event will modify +the ``Response`` object in some way, such as modifying headers, adding cookies, +or even changing the content of the ``Response`` itself (e.g. injecting some +JavaScript before the end ```` tag of an HTML response). + +After this event is dispatched, the final ``Response`` object is returned +from :method:`Symfony\\Component\\HttpKernel\\HttpKernel::handle`. In the +most typical use-case, you can then call the :method:`Symfony\\Component\\HttpFoundation\\Response::send` +method, which sends the headers and prints the ``Response`` content. + +.. sidebar:: ``kernel.response`` in the Symfony Framework + + There are several minor listeners on this event inside the Symfony Framework, + and most modify the response in some way. For example, the + :class:`Symfony\\Bundle\\WebProfilerBundle\\EventListener\\WebDebugToolbarListener` + injects some JavaScript at the bottom of your page in the ``dev`` environment + which causes the web debug toolbar to be displayed. Another listener, + :class:`Symfony\\Component\\Security\\Http\\Firewall\\ContextListener` + serializes the current user's information into the + session so that it can be reloaded on the next request. + +.. _component-http-kernel-kernel-terminate: + +8) The ``kernel.terminate`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Typical Purposes**: To perform some "heavy" action after the response has +been streamed to the user + +:ref:`Kernel Events Information Table ` + +The final event of the HttpKernel process is ``kernel.terminate`` and is unique +because it occurs *after* the ``HttpKernel::handle()`` method, and after the +response is sent to the user. Recall from above, then the code that uses +the kernel, ends like this:: + + // sends the headers and echoes the content + $response->send(); + + // triggers the kernel.terminate event + $kernel->terminate($request, $response); + +As you can see, by calling ``$kernel->terminate`` after sending the response, +you will trigger the ``kernel.terminate`` event where you can perform certain +actions that you may have delayed in order to return the response as quickly +as possible to the client (e.g. sending emails). + +.. warning:: + + Internally, the HttpKernel makes use of the :phpfunction:`fastcgi_finish_request` + PHP function. This means that at the moment, only the `PHP FPM`_ API and the + `FrankenPHP`_ server are able to send a response to the client while the server's PHP process + still performs some tasks. With all other server APIs, listeners to ``kernel.terminate`` + are still executed, but the response is not sent to the client until they + are all completed. + +.. note:: + + Using the ``kernel.terminate`` event is optional, and should only be + called if your kernel implements :class:`Symfony\\Component\\HttpKernel\\TerminableInterface`. + +.. _component-http-kernel-kernel-exception: + +9) Handling Exceptions: the ``kernel.exception`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Typical Purposes**: Handle some type of exception and create an appropriate +``Response`` to return for the exception + +:ref:`Kernel Events Information Table ` + +If an exception is thrown at any point inside ``HttpKernel::handle()``, another +event - ``kernel.exception`` is dispatched. Internally, the body of the ``handle()`` +method is wrapped in a try-catch block. When any exception is thrown, the +``kernel.exception`` event is dispatched so that your system can somehow respond +to the exception. + +.. raw:: html + + + +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\\ExceptionEvent::getThrowable` +method. A typical listener on this event will check for a certain type of +exception and create an appropriate error ``Response``. + +For example, to generate a 404 page, you might throw a special type of exception +and then add a listener on this event that looks for this exception and +creates and returns a 404 ``Response``. In fact, the HttpKernel component +comes with an :class:`Symfony\\Component\\HttpKernel\\EventListener\\ErrorListener`, +which if you choose to use, will do this and more by default (see the sidebar +below for more details). + +The :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` exposes the +:method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::isKernelTerminating` +method, which you can use to determine if the kernel is currently terminating +at the moment the exception was thrown. + +.. versionadded:: 7.1 + + The + :method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::isKernelTerminating` + method was introduced in Symfony 7.1. + +.. note:: + + When setting a response for the ``kernel.exception`` event, the propagation + is stopped. This means listeners with lower priority won't be executed. + +.. sidebar:: ``kernel.exception`` in the Symfony Framework + + There are two main listeners to ``kernel.exception`` when using the + Symfony Framework. + + **ErrorListener in the HttpKernel Component** + + The first comes core to the HttpKernel component + and is called :class:`Symfony\\Component\\HttpKernel\\EventListener\\ErrorListener`. + The listener has several goals: + + 1) The thrown exception is converted into a + :class:`Symfony\\Component\\ErrorHandler\\Exception\\FlattenException` + object, which contains all the information about the request, but which + can be printed and serialized. + + 2) If the original exception implements + :class:`Symfony\\Component\\HttpKernel\\Exception\\HttpExceptionInterface`, + then ``getStatusCode()`` and ``getHeaders()`` are called on the exception + and used to populate the headers and status code of the ``FlattenException`` + object. The idea is that these are used in the next step when creating + the final response. If you want to set custom HTTP headers, you can always + use the ``setHeaders()`` method on exceptions derived from the + :class:`Symfony\\Component\\HttpKernel\\Exception\\HttpException` class. + + 3) If the original exception implements + :class:`Symfony\\Component\\HttpFoundation\\Exception\\RequestExceptionInterface`, + then the status code of the ``FlattenException`` object is populated with + ``400`` and no other headers are modified. + + 4) A controller is executed and passed the flattened exception. The exact + controller to render is passed as a constructor argument to this listener. + This controller will return the final ``Response`` for this error page. + + **ExceptionListener in the Security Component** + + The other important listener is the + :class:`Symfony\\Component\\Security\\Http\\Firewall\\ExceptionListener`. + The goal of this listener is to handle security exceptions and, when + appropriate, *help* the user to authenticate (e.g. redirect to the login + page). + +.. _http-kernel-creating-listener: + +Creating an Event Listener +-------------------------- + +As you've seen, you can create and attach event listeners to any of the events +dispatched during the ``HttpKernel::handle()`` cycle. Typically a listener is a PHP +class with a method that's executed, but it can be anything. For more information +on creating and attaching event listeners, see :doc:`/components/event_dispatcher`. + +The name of each of the "kernel" events is defined as a constant on the +:class:`Symfony\\Component\\HttpKernel\\KernelEvents` class. Additionally, each +event listener is passed a single argument, which is some subclass of :class:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent`. +This object contains information about the current state of the system and +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\\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\\TerminateEvent` +kernel.exception ``KernelEvents::EXCEPTION`` :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` +=========================== ====================================== ======================================================================== + +.. _http-kernel-working-example: + +A full Working Example +---------------------- + +When using the HttpKernel component, you're free to attach any listeners +to the core events, use any controller resolver that implements the +:class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface` and +use any argument resolver that implements the +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolverInterface`. +However, the HttpKernel component comes with some built-in listeners and everything +else that can be used to create a working example:: + + use Symfony\Component\EventDispatcher\EventDispatcher; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\RequestStack; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Controller\ArgumentResolver; + use Symfony\Component\HttpKernel\Controller\ControllerResolver; + use Symfony\Component\HttpKernel\EventListener\RouterListener; + use Symfony\Component\HttpKernel\HttpKernel; + use Symfony\Component\Routing\Matcher\UrlMatcher; + use Symfony\Component\Routing\RequestContext; + use Symfony\Component\Routing\Route; + use Symfony\Component\Routing\RouteCollection; + + $routes = new RouteCollection(); + $routes->add('hello', new Route('/hello/{name}', [ + '_controller' => function (Request $request): Response { + return new Response( + sprintf("Hello %s", $request->get('name')) + ); + }] + )); + + $request = Request::createFromGlobals(); + + $matcher = new UrlMatcher($routes, new RequestContext()); + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber(new RouterListener($matcher, new RequestStack())); + + $controllerResolver = new ControllerResolver(); + $argumentResolver = new ArgumentResolver(); + + $kernel = new HttpKernel($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver); + + $response = $kernel->handle($request); + $response->send(); + + $kernel->terminate($request, $response); + +.. _http-kernel-sub-requests: + +Sub Requests +------------ + +In addition to the "main" request that's sent into ``HttpKernel::handle()``, +you can also send a so-called "sub request". A sub request looks and acts like +any other request, but typically serves to render just one small portion of +a page instead of a full page. You'll most commonly make sub-requests from +your controller (or perhaps from inside a template, that's being rendered by +your controller). + +.. raw:: html + + + +To execute a sub request, use ``HttpKernel::handle()``, but change the second +argument as follows:: + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpKernel\HttpKernelInterface; + + // ... + + // create some other request manually as needed + $request = new Request(); + // for example, possibly set its _controller manually + $request->attributes->set('_controller', '...'); + + $response = $kernel->handle($request, HttpKernelInterface::SUB_REQUEST); + // do something with this response + +This creates another full request-response cycle where this new ``Request`` is +transformed into a ``Response``. The only difference internally is that some +listeners (e.g. security) may only act upon the main request. Each listener +is passed some subclass of :class:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent`, +whose :method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::isMainRequest` +method can be used to check if the current request is a "main" or "sub" request. + +For example, a listener that only needs to act on the main request may +look like this:: + + use Symfony\Component\HttpKernel\Event\RequestEvent; + // ... + + public function onKernelRequest(RequestEvent $event): void + { + if (!$event->isMainRequest()) { + return; + } + + // ... + } + +.. note:: + + The default value of the ``_format`` request attribute is ``html``. If your + sub request returns a different format (e.g. ``json``) you can set it by + defining the ``_format`` attribute explicitly on the request:: + + $request->attributes->set('_format', 'json'); + +.. _http-kernel-resource-locator: + +Locating Resources +------------------ + +The HttpKernel component is responsible of the bundle mechanism used in Symfony +applications. One of the key features of the bundles is that you can use logic +paths instead of physical paths to refer to any of their resources (config files, +templates, controllers, translation files, etc.) + +This allows to import resources even if you don't know where in the filesystem a +bundle will be installed. For example, the ``services.xml`` file stored in the +``Resources/config/`` directory of a bundle called FooBundle can be referenced as +``@FooBundle/Resources/config/services.xml`` instead of ``__DIR__/Resources/config/services.xml``. + +This is possible thanks to the :method:`Symfony\\Component\\HttpKernel\\Kernel::locateResource` +method provided by the kernel, which transforms logical paths into physical paths:: + + $path = $kernel->locateResource('@FooBundle/Resources/config/services.xml'); + +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /reference/events + +.. _reflection: https://www.php.net/manual/en/book.reflection.php +.. _FOSRestBundle: https://github.com/friendsofsymfony/FOSRestBundle +.. _`PHP FPM`: https://www.php.net/manual/en/install.fpm.php +.. _variadic: https://www.php.net/manual/en/functions.arguments.php#functions.variable-arg-list +.. _`FrankenPHP`: https://frankenphp.dev diff --git a/components/http_kernel/index.rst b/components/http_kernel/index.rst deleted file mode 100644 index 202549bc9bd..00000000000 --- a/components/http_kernel/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -HTTP Kernel -=========== - -.. toctree:: - :maxdepth: 2 - - introduction diff --git a/components/http_kernel/introduction.rst b/components/http_kernel/introduction.rst deleted file mode 100644 index 309b20b6a42..00000000000 --- a/components/http_kernel/introduction.rst +++ /dev/null @@ -1,689 +0,0 @@ -.. index:: - single: HTTP - single: HttpKernel - single: Components; HttpKernel - -The HttpKernel Component -======================== - - The HttpKernel Component provides a structured process for converting - a ``Request`` into a ``Response`` by making use of the event dispatcher. - It's flexible enough to create a full-stack framework (Symfony), a micro-framework - (Silex) or an advanced CMS system (Drupal). - -Installation ------------- - -You can install the component in many different ways: - -* Use the official Git repository (https://github.com/symfony/HttpKernel); -* :doc:`Install it via Composer` (``symfony/http-kernel`` on Packagist_). - -The Workflow of a Request -------------------------- - -Every HTTP web interaction begins with a request and ends with a response. -Your job as a developer is to create PHP code that reads the request information -(e.g. the URL) and creates and returns a response (e.g. an HTML page or JSON string). - -.. image:: /images/components/http_kernel/request-response-flow.png - :align: center - -Typically, some sort of framework or system is built to handle all the repetitive -tasks (e.g. routing, security, etc) so that a developer can easily build -each *page* of the application. Exactly *how* these systems are built varies -greatly. The HttpKernel component provides an interface that formalizes -the process of starting with a request and creating the appropriate response. -The component is meant to be the heart of any application or framework, no -matter how varied the architecture of that system:: - - namespace Symfony\Component\HttpKernel; - - use Symfony\Component\HttpFoundation\Request; - - interface HttpKernelInterface - { - // ... - - /** - * @return Response A Response instance - */ - public function handle( - Request $request, - $type = self::MASTER_REQUEST, - $catch = true - ); - } - -Internally, :method:`HttpKernel::handle()` - -the concrete implementation of :method:`HttpKernelInterface::handle()` - -defines a workflow that starts with a :class:`Symfony\\Component\\HttpFoundation\\Request` -and ends with a :class:`Symfony\\Component\\HttpFoundation\\Response`. - -.. image:: /images/components/http_kernel/01-workflow.png - :align: center - -The exact details of this workflow are the key to understanding how the kernel -(and the Symfony Framework or any other library that uses the kernel) works. - -HttpKernel: Driven by Events -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``HttpKernel::handle()`` method works internally by dispatching events. -This makes the method both flexible, but also a bit abstract, since all the -"work" of a framework/application built with HttpKernel is actually done -in event listeners. - -To help explain this process, this document looks at each step of the process -and talks about how one specific implementation of the HttpKernel - the Symfony -Framework - works. - -Initially, using the :class:`Symfony\\Component\\HttpKernel\\HttpKernel` -is really simple, and involves creating an :doc:`event dispatcher` -and a :ref:`controller resolver` -(explained below). To complete your working kernel, you'll add more event -listeners to the events discussed below:: - - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpKernel\HttpKernel; - use Symfony\Component\EventDispatcher\EventDispatcher; - use Symfony\Component\HttpKernel\Controller\ControllerResolver; - - // create the Request object - $request = Request::createFromGlobals(); - - $dispatcher = new EventDispatcher(); - // ... add some event listeners - - // create your controller resolver - $resolver = new ControllerResolver(); - // instantiate the kernel - $kernel = new HttpKernel($dispatcher, $resolver); - - // actually execute the kernel, which turns the request into a response - // by dispatching events, calling a controller, and returning the response - $response = $kernel->handle($request); - - // echo the content and send the headers - $response->send(); - - // triggers the kernel.terminate event - $kernel->terminate($request, $response); - -See ":ref:`http-kernel-working-example`" for a more concrete implementation. - -For general information on adding listeners to the events below, see -:ref:`http-kernel-creating-listener`. - -.. tip:: - - Fabien Potencier also wrote a wonderful series on using the ``HttpKernel`` - component and other Symfony2 components to create your own framework. See - `Create your own framework... on top of the Symfony2 Components`_. - -.. _component-http-kernel-kernel-request: - -1) The ``kernel.request`` event -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Typical Purposes**: To add more information to the ``Request``, initialize -parts of the system, or return a ``Response`` if possible (e.g. a security -layer that denies access) - -:ref:`Kernel Events Information Table` - -The first event that is dispatched inside :method:`HttpKernel::handle` -is ``kernel.request``, which may have a variety of different listeners. - -.. image:: /images/components/http_kernel/02-kernel-request.png - :align: center - -Listeners of this event can be quite varied. Some listeners - such as a security -listener - might have enough information to create a ``Response`` object immediately. -For example, if a security listener determined that a user doesn't have access, -that listener may return a :class:`Symfony\\Component\\HttpFoundation\\RedirectResponse` -to the login page or a 403 Access Denied response. - -If a ``Response`` is returned at this stage, the process skips directly to -the :ref:`kernel.response` event. - -.. image:: /images/components/http_kernel/03-kernel-request-response.png - :align: center - -Other listeners simply initialize things or add more information to the request. -For example, a listener might determine and set the locale on the ``Request`` -object. - -Another common listener is routing. A router listener may process the ``Request`` -and determine the controller that should be rendered (see the next section). -In fact, the ``Request`` object has an ":ref:`attributes`" -bag which is a perfect spot to store this extra, application-specific data -about the request. This means that if your router listener somehow determines -the controller, it can store it on the ``Request`` attributes (which can be used -by your controller resolver). - -Overall, the purpose of the ``kernel.request`` event is either to create and -return a ``Response`` directly, or to add information to the ``Request`` -(e.g. setting the locale or setting some other information on the ``Request`` -attributes). - -.. sidebar:: ``kernel.request`` in the Symfony Framework - - The most important listener to ``kernel.request`` in the Symfony Framework - is the :class:`Symfony\\Component\\HttpKernel\\EventListener\\RouterListener`. - This class executes the routing layer, which returns an *array* of information - about the matched request, including the ``_controller`` and any placeholders - that are in the route's pattern (e.g. ``{slug}``). See - :doc:`Routing Component`. - - This array of information is stored in the :class:`Symfony\\Component\\HttpFoundation\\Request` - object's ``attributes`` array. Adding the routing information here doesn't - do anything yet, but is used next when resolving the controller. - -.. _component-http-kernel-resolve-controller: - -2) Resolve the Controller -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Assuming that no ``kernel.request`` listener was able to create a ``Response``, -the next step in HttpKernel is to determine and prepare (i.e. resolve) the -controller. The controller is the part of the end-application's code that -is responsible for creating and returning the ``Response`` for a specific page. -The only requirement is that it is a PHP callable - i.e. a function, method -on an object, or a ``Closure``. - -But *how* you determine the exact controller for a request is entirely up -to your application. This is the job of the "controller resolver" - a class -that implements :class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface` -and is one of the constructor arguments to ``HttpKernel``. - -.. image:: /images/components/http_kernel/04-resolve-controller.png - :align: center - -Your job is to create a class that implements the interface and fill in its -two methods: ``getController`` and ``getArguments``. In fact, one default -implementation already exists, which you can use directly or learn from: -:class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver`. -This implementation is explained more in the sidebar below:: - - namespace Symfony\Component\HttpKernel\Controller; - - use Symfony\Component\HttpFoundation\Request; - - interface ControllerResolverInterface - { - public function getController(Request $request); - - public function getArguments(Request $request, $controller); - } - -Internally, the ``HttpKernel::handle`` method first calls -:method:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface::getController` -on the controller resolver. This method is passed the ``Request`` and is responsible -for somehow determining and returning a PHP callable (the controller) based -on the request's information. - -The second method, :method:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface::getArguments`, -will be called after another event - ``kernel.controller`` - is dispatched. - -.. sidebar:: Resolving the Controller in the Symfony2 Framework - - The Symfony Framework uses the built-in - :class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver` - class (actually, it uses a sub-class, which some extra functionality - mentioned below). This class leverages the information that was placed - on the ``Request`` object's ``attributes`` property during the ``RouterListener``. - - **getController** - - The ``ControllerResolver`` looks for a ``_controller`` - key on the ``Request`` object's attributes property (recall that this - information is typically placed on the ``Request`` via the ``RouterListener``). - This string is then transformed into a PHP callable by doing the following: - - a) The ``AcmeDemoBundle:Default:index`` format of the ``_controller`` key - is changed to another string that contains the full class and method - name of the controller by following the convention used in Symfony2 - e.g. - ``Acme\DemoBundle\Controller\DefaultController::indexAction``. This transformation - is specific to the :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\ControllerResolver` - sub-class used by the Symfony2 Framework. - - b) A new instance of your controller class is instantiated with no - constructor arguments. - - c) If the controller implements :class:`Symfony\\Component\\DependencyInjection\\ContainerAwareInterface`, - ``setContainer`` is called on the controller object and the container - is passed to it. This step is also specific to the :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\ControllerResolver` - sub-class used by the Symfony2 Framework. - - There are also a few other variations on the above process (e.g. if - you're registering your controllers as services). - -.. _component-http-kernel-kernel-controller: - -3) The ``kernel.controller`` event ----------------------------------- - -**Typical Purposes**: Initialize things or change the controller just before -the controller is executed. - -:ref:`Kernel Events Information Table` - -After the controller callable has been determined, ``HttpKernel::handle`` -dispatches the ``kernel.controller`` event. Listeners to this event might initialize -some part of the system that needs to be initialized after certain things -have been determined (e.g. the controller, routing information) but before -the controller is executed. For some examples, see the Symfony2 section below. - -.. image:: /images/components/http_kernel/06-kernel-controller.png - :align: center - -Listeners to this event can also change the controller callable completely -by calling :method:`FilterControllerEvent::setController` -on the event object that's passed to listeners on this event. - -.. sidebar:: ``kernel.controller`` in the Symfony Framework - - There are a few minor listeners to the ``kernel.controller`` event in - the Symfony Framework, and many deal with collecting profiler data when - the profiler is enabled. - - One interesting listener comes from the :doc:`SensioFrameworkExtraBundle `, - which is packaged with the Symfony Standard Edition. This listener's - :doc:`@ParamConverter` - functionality allows you to pass a full object (e.g. a ``Post`` object) - to your controller instead of a scalar value (e.g. an ``id`` parameter - that was on your route). The listener - ``ParamConverterListener`` - uses - reflection to look at each of the arguments of the controller and tries - to use different methods to convert those to objects, which are then - stored in the ``attributes`` property of the ``Request`` object. Read the - next section to see why this is important. - -4) Getting the Controller Arguments ------------------------------------ - -Next, ``HttpKernel::handle`` calls -:method:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface::getArguments`. -Remember that the controller returned in ``getController`` is a callable. -The purpose of ``getArguments`` is to return the array of arguments that -should be passed to that controller. Exactly how this is done is completely -up to your design, though the built-in :class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver` -is a good example. - -.. image:: /images/components/http_kernel/07-controller-arguments.png - :align: center - -At this point the kernel has a PHP callable (the controller) and an array -of arguments that should be passed when executing that callable. - -.. sidebar:: Getting the Controller Arguments in the Symfony2 Framework - - Now that you know exactly what the controller callable (usually a method - inside a controller object) is, the ``ControllerResolver`` uses `reflection`_ - on the callable to return an array of the *names* of each of the arguments. - It then iterates over each of these arguments and uses the following tricks - to determine which value should be passed for each argument: - - a) If the ``Request`` attributes bag contains a key that matches the name - of the argument, that value is used. For example, if the first argument - to a controller is ``$slug``, and there is a ``slug`` key in the ``Request`` - ``attributes`` bag, that value is used (and typically this value came - from the ``RouterListener``). - - b) If the argument in the controller is type-hinted with Symfony's - :class:`Symfony\\Component\\HttpFoundation\\Request` object, then the - ``Request`` is passed in as the value. - -.. _component-http-kernel-calling-controller: - -5) Calling the Controller -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The next step is simple! ``HttpKernel::handle`` executes the controller. - -.. image:: /images/components/http_kernel/08-call-controller.png - :align: center - -The job of the controller is to build the response for the given resource. -This could be an HTML page, a JSON string or anything else. Unlike every -other part of the process so far, this step is implemented by the "end-developer", -for each page that is built. - -Usually, the controller will return a ``Response`` object. If this is true, -then the work of the kernel is just about done! In this case, the next step -is the :ref:`kernel.response` event. - -.. image:: /images/components/http_kernel/09-controller-returns-response.png - :align: center - -But if the controller returns anything besides a ``Response``, then the kernel -has a little bit more work to do - :ref:`kernel.view` -(since the end goal is *always* to generate a ``Response`` object). - -.. note:: - - A controller must return *something*. If a controller returns ``null``, - an exception will be thrown immediately. - -.. _component-http-kernel-kernel-view: - -6) The ``kernel.view`` event -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Typical Purposes**: Transform a non-``Response`` return value from a controller -into a ``Response`` - -:ref:`Kernel Events Information Table` - -If the controller doesn't return a ``Response`` object, then the kernel dispatches -another event - ``kernel.view``. The job of a listener to this event is to -use the return value of the controller (e.g. an array of data or an object) -to create a ``Response``. - -.. image:: /images/components/http_kernel/10-kernel-view.png - :align: center - -This can be useful if you want to use a "view" layer: instead of returning -a ``Response`` from the controller, you return data that represents the page. -A listener to this event could then use this data to create a ``Response`` that -is in the correct format (e.g HTML, json, etc). - -At this stage, if no listener sets a response on the event, then an exception -is thrown: either the controller *or* one of the view listeners must always -return a ``Response``. - -.. sidebar:: ``kernel.view`` in the Symfony Framework - - There is no default listener inside the Symfony Framework for the ``kernel.view`` - event. However, one core bundle - - :doc:`SensioFrameworkExtraBundle ` - - *does* add a listener to this event. If your controller returns an array, - and you place the :doc:`@Template` - annotation above the controller, then this listener renders a template, - passes the array you returned from your controller to that template, - and creates a ``Response`` containing the returned content from that - template. - - Additionally, a popular community bundle `FOSRestBundle`_ implements - a listener on this event which aims to give you a robust view layer - capable of using a single controller to return many different content-type - responses (e.g. HTML, JSON, XML, etc). - -.. _component-http-kernel-kernel-response: - -7) The ``kernel.response`` event -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Typical Purposes**: Modify the ``Response`` object just before it is sent - -:ref:`Kernel Events Information Table` - -The end goal of the kernel is to transform a ``Request`` into a ``Response``. The -``Response`` might be created during the :ref:`kernel.request` -event, returned from the :ref:`controller`, -or returned by one of the listeners to the :ref:`kernel.view` -event. - -Regardless of who creates the ``Response``, another event - ``kernel.response`` -is dispatched directly afterwards. A typical listener to this event will modify -the ``Response`` object in some way, such as modifying headers, adding cookies, -or even changing the content of the ``Response`` itself (e.g. injecting some -JavaScript before the end ```` tag of an HTML response). - -After this event is dispatched, the final ``Response`` object is returned -from :method:`Symfony\\Component\\HttpKernel\\HttpKernel::handle`. In the -most typical use-case, you can then call the :method:`Symfony\\Component\\HttpFoundation\\Response::send` -method, which sends the headers and prints the ``Response`` content. - -.. sidebar:: ``kernel.response`` in the Symfony Framework - - There are several minor listeners on this event inside the Symfony Framework, - and most modify the response in some way. For example, the - :class:`Symfony\\Bundle\\WebProfilerBundle\\EventListener\\WebDebugToolbarListener` - injects some JavaScript at the bottom of your page in the ``dev`` environment - which causes the web debug toolbar to be displayed. Another listener, - :class:`Symfony\\Component\\Security\\Http\\Firewall\\ContextListener` - serializes the current user's information into the - session so that it can be reloaded on the next request. - -.. _component-http-kernel-kernel-terminate: - -8) The ``kernel.terminate`` event -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.1 - The ``kernel.terminate`` event is new to Symfony 2.1. - -**Typical Purposes**: To perform some "heavy" action after the response has -been streamed to the user - -:ref:`Kernel Events Information Table` - -The final event of the HttpKernel process is ``kernel.terminate`` and is unique -because it occurs *after* the ``HttpKernel::handle`` method, and after the -response is sent to the user. Recall from above, then the code that uses -the kernel, ends like this:: - - // echo the content and send the headers - $response->send(); - - // triggers the kernel.terminate event - $kernel->terminate($request, $response); - -As you can see, by calling ``$kernel->terminate`` after sending the response, -you will trigger the ``kernel.terminate`` event where you can perform certain -actions that you may have delayed in order to return the response as quickly -as possible to the client (e.g. sending emails). - -.. note:: - - Using the ``kernel.terminate`` event is optional, and should only be - called if your kernel implements :class:`Symfony\\Component\\HttpKernel\\TerminableInterface`. - -.. sidebar:: ``kernel.terminate`` in the Symfony Framework - - If you use the ``SwiftmailerBundle`` with Symfony2 and use ``memory`` - spooling, then the :class:`Symfony\\Bundle\\SwiftmailerBundle\\EventListener\\EmailSenderListener` - is activated, which actually delivers any emails that you scheduled to - send during the request. - -.. _component-http-kernel-kernel-exception: - -Handling Exceptions:: the ``kernel.exception`` event -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Typical Purposes**: Handle some type of exception and create an appropriate -``Response`` to return for the exception - -:ref:`Kernel Events Information Table` - -If an exception is thrown at any point inside ``HttpKernel::handle``, another -event - ``kernel.exception`` is thrown. Internally, the body of the ``handle`` -function is wrapped in a try-catch block. When any exception is thrown, the -``kernel.exception`` event is dispatched so that your system can somehow respond -to the exception. - -.. image:: /images/components/http_kernel/11-kernel-exception.png - :align: center - -Each listener to this event is passed a :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent` -object, which you can use to access the original exception via the -:method:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent::getException` -method. A typical listener on this event will check for a certain type of -exception and create an appropriate error ``Response``. - -For example, to generate a 404 page, you might throw a special type of exception -and then add a listener on this event that looks for this exception and -creates and returns a 404 ``Response``. In fact, the ``HttpKernel`` component -comes with an :class:`Symfony\\Component\\HttpKernel\\EventListener\\ExceptionListener`, -which if you choose to use, will do this and more by default (see the sidebar -below for more details). - -.. sidebar:: ``kernel.exception`` in the Symfony Framework - - There are two main listeners to ``kernel.exception`` when using the - Symfony Framework. - - **ExceptionListener in HttpKernel** - - The first comes core to the ``HttpKernel`` component - and is called :class:`Symfony\\Component\\HttpKernel\\EventListener\\ExceptionListener`. - The listener has several goals: - - 1) The thrown exception is converted into a - :class:`Symfony\\Component\\HttpKernel\\Exception\\FlattenException` - object, which contains all the information about the request, but which - can be printed and serialized. - - 2) If the original exception implements - :class:`Symfony\\Component\\HttpKernel\\Exception\\HttpExceptionInterface`, - then ``getStatusCode`` and ``getHeaders`` are called on the exception - and used to populate the headers and status code of the ``FlattenException`` - object. The idea is that these are used in the next step when creating - the final response. - - 3) A controller is executed and passed the flattened exception. The exact - controller to render is passed as a constructor argument to this listener. - This controller will return the final ``Response`` for this error page. - - **ExceptionListener in Security** - - The other important listener is the - :class:`Symfony\\Component\\Security\\Http\\Firewall\\ExceptionListener`. - The goal of this listener is to handle security exceptions and, when - appropriate, *help* the user to authenticate (e.g. redirect to the login - page). - -.. _http-kernel-creating-listener: - -Creating an Event Listener --------------------------- - -As you've seen, you can create and attach event listeners to any of the events -dispatched during the ``HttpKernel::handle`` cycle. Typically a listener is a PHP -class with a method that's executed, but it can be anything. For more information -on creating and attaching event listeners, see :doc:`/components/event_dispatcher/introduction`. - -The name of each of the "kernel" events is defined as a constant on the -:class:`Symfony\\Component\\HttpKernel\\KernelEvents` class. Additionally, each -event listener is passed a single argument, which is some sub-class of :class:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent`. -This object contains information about the current state of the system and -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.view | ``KernelEvents::VIEW`` | :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForControllerResultEvent` | -+-------------------+-------------------------------+-------------------------------------------------------------------------------------+ -| kernel.response | ``KernelEvents::RESPONSE`` | :class:`Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent` | -+-------------------+-------------------------------+-------------------------------------------------------------------------------------+ -| kernel.terminate | ``KernelEvents::TERMINATE`` | :class:`Symfony\\Component\\HttpKernel\\Event\\PostResponseEvent` | -+-------------------+-------------------------------+-------------------------------------------------------------------------------------+ -| kernel.exception | ``KernelEvents::EXCEPTION`` | :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent` | -+-------------------+-------------------------------+-------------------------------------------------------------------------------------+ - -.. _http-kernel-working-example: - -A Full Working Example ----------------------- - -When using the HttpKernel component, you're free to attach any listeners -to the core events and use any controller resolver that implements the -:class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface`. -However, the HttpKernel component comes with some built-in listeners and -a built-in ControllerResolver that can be used to create a working example:: - - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\HttpKernel\HttpKernel; - use Symfony\Component\EventDispatcher\EventDispatcher; - use Symfony\Component\HttpKernel\Controller\ControllerResolver; - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\Matcher\UrlMatcher; - use Symfony\Component\Routing\RequestContext; - - $routes = new RouteCollection(); - $routes->add('hello', new Route('/hello/{name}', array( - '_controller' => function (Request $request) { - return new Response(sprintf("Hello %s", $request->get('name'))); - } - ), - )); - - $request = Request::createFromGlobals(); - - $matcher = new UrlMatcher($routes, new RequestContext()); - - $dispatcher = new EventDispatcher(); - $dispatcher->addSubscriber(new RouterListener($matcher)); - - $resolver = new ControllerResolver(); - $kernel = new HttpKernel($dispatcher, $resolver); - - $response = $kernel->handle($request); - $response->send(); - - $kernel->terminate($request, $response); - -Sub Requests ------------- - -In addition to the "main" request that's sent into ``HttpKernel::handle``, -you can also send so-called "sub request". A sub request looks and acts like -any other request, but typically serves to render just one small portion of -a page instead of a full page. You'll most commonly make sub-requests from -your controller (or perhaps from inside a template, that's being rendered by -your controller). - -.. image:: /images/components/http_kernel/sub-request.png - :align: center - -To execute a sub request, use ``HttpKernel::handle``, but change the second -arguments as follows:: - - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpKernel\HttpKernelInterface; - - // ... - - // create some other request manually as needed - $request = new Request(); - // for example, possibly set its _controller manually - $request->attributes->add('_controller', '...'); - - $response = $kernel->handle($request, HttpKernelInterface::SUB_REQUEST); - // do something with this response - -This creates another full request-response cycle where this new ``Request`` is -transformed into a ``Response``. The only difference internally is that some -listeners (e.g. security) may only act upon the master request. Each listener -is passed some sub-class of :class:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent`, -whose :method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::getRequestType` -can be used to figure out 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\HttpKernelInterface; - // ... - - public function onKernelRequest(GetResponseEvent $event) - { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { - return; - } - - // ... - } - -.. _Packagist: https://packagist.org/packages/symfony/http-kernel -.. _reflection: http://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 diff --git a/components/index.rst b/components/index.rst deleted file mode 100644 index c7ab9d0e113..00000000000 --- a/components/index.rst +++ /dev/null @@ -1,31 +0,0 @@ -The Components -============== - -.. toctree:: - :hidden: - - using_components - class_loader - config/index - console/index - css_selector - debug - dom_crawler - dependency_injection/index - event_dispatcher/index - filesystem - finder - http_foundation/index - http_kernel/index - intl - options_resolver - process - property_access/index - routing/index - security/index - serializer - stopwatch - templating - yaml/index - -.. include:: /components/map.rst.inc diff --git a/components/intl.rst b/components/intl.rst index cf42b9778d5..ba3cbdcb959 100644 --- a/components/intl.rst +++ b/components/intl.rst @@ -1,413 +1,425 @@ -.. index:: - single: Intl - single: Components; Intl - The Intl Component ================== - A PHP replacement layer for the C `intl extension`_ that also provides - access to the localization data of the `ICU library`_. - -.. versionadded:: 2.3 - - The Intl component was added in Symfony 2.3. In earlier versions of Symfony, - you should use the Locale component instead. + This component provides access to the localization data of the `ICU library`_. -.. caution:: +.. seealso:: - The replacement layer is limited to the locale "en". If you want to use - other locales, you should `install the intl extension`_ instead. + This article explains how to use the Intl features as an independent component + in any PHP application. Read the :doc:`/translation` article to learn about + how to internationalize and manage the user locale in Symfony applications. Installation ------------ -You can install the component in two different ways: +.. code-block:: terminal -* Using the official Git repository (https://github.com/symfony/Intl); -* :doc:`Install it via Composer` (``symfony/intl`` on `Packagist`_). + $ composer require symfony/intl -If you install the component via Composer, the following classes and functions -of the intl extension will be automatically provided if the intl extension is -not loaded: +.. include:: /components/require_autoload.rst.inc -* :phpclass:`Collator` -* :phpclass:`IntlDateFormatter` -* :phpclass:`Locale` -* :phpclass:`NumberFormatter` -* :phpfunction:`intl_error_name` -* :phpfunction:`intl_is_failure` -* :phpfunction:`intl_get_error_code` -* :phpfunction:`intl_get_error_message` +Accessing ICU Data +------------------ -When the intl extension is not available, the following classes are used to -replace the intl classes: +This component provides the following ICU data: -* :class:`Symfony\\Component\\Intl\\Collator\\Collator` -* :class:`Symfony\\Component\\Intl\\DateFormatter\\IntlDateFormatter` -* :class:`Symfony\\Component\\Intl\\Locale\\Locale` -* :class:`Symfony\\Component\\Intl\\NumberFormatter\\NumberFormatter` -* :class:`Symfony\\Component\\Intl\\Globals\\IntlGlobals` +* `Language and Script Names`_ +* `Country Names`_ +* `Locales`_ +* `Currencies`_ +* `Timezones`_ -Composer automatically exposes these classes in the global namespace. +Language and Script Names +~~~~~~~~~~~~~~~~~~~~~~~~~ -If you don't use Composer but the -:doc:`Symfony ClassLoader component`, you need to -expose them manually by adding the following lines to your autoload code:: +The :class:`Symfony\\Component\\Intl\\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 (2T)`_ list:: - if (!function_exists('intl_is_failure')) { - require '/path/to/Icu/Resources/stubs/functions.php'; + use Symfony\Component\Intl\Languages; - $loader->registerPrefixFallback('/path/to/Icu/Resources/stubs'); - } + \Locale::setDefault('en'); -.. sidebar:: ICU and Deployment Problems + $languages = Languages::getNames(); + // ('languageCode' => 'languageName') + // => ['ab' => 'Abkhazian', 'ace' => 'Achinese', ...] - The intl extension internally uses the `ICU library`_ to obtain localization - data such as number formats in different languages, country names and more. - To make this data accessible to userland PHP libraries, Symfony2 ships a copy - in the `ICU component`_. + $languages = Languages::getAlpha3Names(); + // ('languageCode' => 'languageName') + // => ['abk' => 'Abkhazian', 'ace' => 'Achinese', ...] - Depending on the ICU version compiled with your intl extension, a matching - version of that component needs to be installed. It sounds complicated, - but usually Composer does this for you automatically: + $language = Languages::getName('fr'); + // => 'French' - * 1.0.*: when the intl extension is not available - * 1.1.*: when intl is compiled with ICU 4.0 or higher - * 1.2.*: when intl is compiled with ICU 4.4 or higher + $language = Languages::getAlpha3Name('fra'); + // => 'French' - These versions are important when you deploy your application to a **server with - a lower ICU version** than your development machines, because deployment will - fail if +All methods accept the translation locale as the last, optional parameter, +which defaults to the current default locale:: - * the development machines are compiled with ICU 4.4 or higher, but the - server is compiled with a lower ICU version than 4.4; - * the intl extension is available on the development machines but not on - the server. + $languages = Languages::getNames('de'); + // => ['ab' => 'Abchasisch', 'ace' => 'Aceh', ...] - For example, consider that your development machines ship ICU 4.8 and the server - ICU 4.2. When you run ``php composer.phar update`` on the development machine, version - 1.2.* of the ICU component will be installed. But after deploying the - application, ``php composer.phar install`` will fail with the following error: + $languages = Languages::getAlpha3Names('de'); + // => ['abk' => 'Abchasisch', 'ace' => 'Aceh', ...] - .. code-block:: bash + $language = Languages::getName('fr', 'de'); + // => 'Französisch' - $ php composer.phar install - Loading composer repositories with package information - Installing dependencies from lock file - Your requirements could not be resolved to an installable set of packages. + $language = Languages::getAlpha3Name('fra', 'de'); + // => 'Französisch' - Problem 1 - - symfony/icu 1.2.x requires lib-icu >=4.4 -> the requested linked - library icu has the wrong version installed or is missing from your - system, make sure to have the extension providing it. +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:: - The error tells you that the requested version of the ICU component, version - 1.2, is not compatible with PHP's ICU version 4.2. + $isValidLanguage = Languages::exists($languageCode); - One solution to this problem is to run ``php composer.phar update`` instead of - ``php composer.phar install``. It is highly recommended **not** to do this. The - ``update`` command will install the latest versions of each Composer dependency - to your production server and potentially break the application. +Or if you have an alpha3 language code you want to check:: - A better solution is to fix your composer.json to the version required by the - production server. First, determine the ICU version on the server: + $isValidLanguage = Languages::alpha3CodeExists($alpha3Code); - .. code-block:: bash +You may convert codes between two-letter alpha2 and three-letter alpha3 codes:: - $ php -i | grep ICU - ICU version => 4.2.1 + $alpha3Code = Languages::getAlpha3Code($alpha2Code); - Then fix the ICU component in your composer.json file to a matching version: + $alpha2Code = Languages::getAlpha2Code($alpha3Code); - .. code-block:: json +The :class:`Symfony\\Component\\Intl\\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):: - "require: { - "symfony/icu": "1.1.*" - } + use Symfony\Component\Intl\Scripts; - Set the version to + \Locale::setDefault('en'); - * "1.0.*" if the server does not have the intl extension installed; - * "1.1.*" if the server is compiled with ICU 4.2 or lower. + $scripts = Scripts::getNames(); + // ('scriptCode' => 'scriptName') + // => ['Adlm' => 'Adlam', 'Afak' => 'Afaka', ...] - Finally, run ``php composer.phar update symfony/icu`` on your development machine, test - extensively and deploy again. The installation of the dependencies will now - succeed. + $script = Scripts::getName('Hans'); + // => 'Simplified' -Writing and Reading Resource Bundles ------------------------------------- +All methods accept the translation locale as the last, optional parameter, +which defaults to the current default locale:: -The :phpclass:`ResourceBundle` class is not currently supporting 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: + $scripts = Scripts::getNames('de'); + // => ['Adlm' => 'Adlam', 'Afak' => 'Afaka', ...] -* `TextBundleWriter`_ -* `PhpBundleWriter`_ -* `BinaryBundleReader`_ -* `PhpBundleReader`_ -* `BufferedBundleReader`_ -* `StructuredBundleReader`_ + $script = Scripts::getName('Hans', 'de'); + // => 'Vereinfacht' -Continue reading if you are interested in how to use these classes. Otherwise -skip this section and jump to `Accessing ICU Data`_. +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:: -TextBundleWriter -~~~~~~~~~~~~~~~~ + $isValidScript = Scripts::exists($scriptCode); -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:: +Country Names +~~~~~~~~~~~~~ - use Symfony\Component\Intl\ResourceBundle\Writer\TextBundleWriter; - use Symfony\Component\Intl\ResourceBundle\Compiler\BundleCompiler; +The :class:`Symfony\\Component\\Intl\\Countries` class provides access to the +name of all countries according to the `ISO 3166-1 alpha-2`_ list and the +`ISO 3166-1 alpha-3`_ list of officially recognized countries and territories:: - $writer = new TextBundleWriter(); - $writer->write('/path/to/bundle', 'en', array( - 'Data' => array( - 'entry1', - 'entry2', - // ... - ), - )); + use Symfony\Component\Intl\Countries; - $compiler = new BundleCompiler(); - $compiler->compile('/path/to/bundle', '/path/to/binary/bundle'); + \Locale::setDefault('en'); -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. + $countries = Countries::getNames(); + // ('alpha2Code' => 'countryName') + // => ['AF' => 'Afghanistan', 'AX' => 'Åland Islands', ...] -PhpBundleWriter -~~~~~~~~~~~~~~~ + $countries = Countries::getAlpha3Names(); + // ('alpha3Code' => 'countryName') + // => ['AFG' => 'Afghanistan', 'ALA' => 'Åland Islands', ...] -The :class:`Symfony\\Component\\Intl\\ResourceBundle\\Writer\\PhpBundleWriter` -writes an array or an array-like object to a .php resource bundle:: + $country = Countries::getName('GB'); + // => 'United Kingdom' - use Symfony\Component\Intl\ResourceBundle\Writer\PhpBundleWriter; + $country = Countries::getAlpha3Name('NOR'); + // => 'Norway' - $writer = new PhpBundleWriter(); - $writer->write('/path/to/bundle', 'en', array( - 'Data' => array( - 'entry1', - 'entry2', - // ... - ), - )); +All methods accept the translation locale as the last, optional parameter, +which defaults to the current default locale:: -BinaryBundleReader -~~~~~~~~~~~~~~~~~~ + $countries = Countries::getNames('de'); + // => ['AF' => 'Afghanistan', 'EG' => 'Ägypten', ...] -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:: + $countries = Countries::getAlpha3Names('de'); + // => ['AFG' => 'Afghanistan', 'EGY' => 'Ägypten', ...] - use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader; + $country = Countries::getName('GB', 'de'); + // => 'Vereinigtes Königreich' - $reader = new BinaryBundleReader(); - $data = $reader->read('/path/to/bundle', 'en'); + $country = Countries::getAlpha3Name('GBR', 'de'); + // => 'Vereinigtes Königreich' - echo $data['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:: -PhpBundleReader -~~~~~~~~~~~~~~~ + $isValidCountry = Countries::exists($alpha2Code); -The :class:`Symfony\\Component\\Intl\\ResourceBundle\\Reader\\PhpBundleReader` -reads resource bundles from .php files and returns an array or an array-like -object:: +Or if you have an alpha3 country code you want to check:: - use Symfony\Component\Intl\ResourceBundle\Reader\PhpBundleReader; + $isValidCountry = Countries::alpha3CodeExists($alpha3Code); - $reader = new PhpBundleReader(); - $data = $reader->read('/path/to/bundle', 'en'); +You may convert codes between two-letter alpha2 and three-letter alpha3 codes:: - echo $data['Data']['entry1']; + $alpha3Code = Countries::getAlpha3Code($alpha2Code); -BufferedBundleReader -~~~~~~~~~~~~~~~~~~~~ + $alpha2Code = Countries::getAlpha2Code($alpha3Code); -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:: +Numeric Country Codes +~~~~~~~~~~~~~~~~~~~~~ - use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader; - use Symfony\Component\Intl\ResourceBundle\Reader\BufferedBundleReader; +The `ISO 3166-1 numeric`_ standard defines three-digit country codes to represent +countries, dependent territories, and special areas of geographical interest. - $reader = new BufferedBundleReader(new BinaryBundleReader(), 10); +The main advantage over the ISO 3166-1 alphabetic codes (alpha-2 and alpha-3) is +that these numeric codes are independent from the writing system. The alphabetic +codes use the 26-letter English alphabet, which might be unavailable or difficult +to use for people and systems using non-Latin scripts (e.g. Arabic or Japanese). - // actually reads the file - $data = $reader->read('/path/to/bundle', 'en'); +The :class:`Symfony\\Component\\Intl\\Countries` class provides access to these +numeric country codes:: - // returns data from the buffer - $data = $reader->read('/path/to/bundle', 'en'); + use Symfony\Component\Intl\Countries; - // actually reads the file - $data = $reader->read('/path/to/bundle', 'fr'); + \Locale::setDefault('en'); -StructuredBundleReader -~~~~~~~~~~~~~~~~~~~~~~ + $numericCodes = Countries::getNumericCodes(); + // ('alpha2Code' => 'numericCode') + // => ['AA' => '958', 'AD' => '020', ...] -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:: + $numericCode = Countries::getNumericCode('FR'); + // => '250' - use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader; - use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReader; + $alpha2 = Countries::getAlpha2FromNumeric('250'); + // => 'FR' - $reader = new StructuredBundleReader(new BinaryBundleReader()); + $exists = Countries::numericCodeExists('250'); + // => true - $data = $reader->read('/path/to/bundle', 'en'); +Locales +~~~~~~~ - // Produces an error if the key "Data" does not exist - echo $data['Data']['entry1']; +A locale is the combination of a language, a region and some parameters that +define the interface preferences of the user. For example, "Chinese" is the +language and ``zh_Hans_MO`` is the locale for "Chinese" (language) + "Simplified" +(script) + "Macau SAR China" (region). The :class:`Symfony\\Component\\Intl\\Locales` +class provides access to the name of all locales:: - // Returns null if the key "Data" does not exist - echo $reader->readEntry('/path/to/bundle', 'en', array('Data', 'entry1')); + use Symfony\Component\Intl\Locales; -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``:: + \Locale::setDefault('en'); - echo $reader->readEntry('/path/to/bundle', 'en', array('Data', 'entry1'), false); + $locales = Locales::getNames(); + // ('localeCode' => 'localeName') + // => ['af' => 'Afrikaans', 'af_NA' => 'Afrikaans (Namibia)', ...] -Accessing ICU Data ------------------- + $locale = Locales::getName('zh_Hans_MO'); + // => 'Chinese (Simplified, Macau SAR China)' -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: +All methods accept the translation locale as the last, optional parameter, +which defaults to the current default locale:: -* `Language and Script Names`_ -* `Country Names`_ -* `Locales`_ -* `Currencies`_ + $locales = Locales::getNames('de'); + // => ['af' => 'Afrikaans', 'af_NA' => 'Afrikaans (Namibia)', ...] -Language and Script Names -~~~~~~~~~~~~~~~~~~~~~~~~~ + $locale = Locales::getName('zh_Hans_MO', 'de'); + // => 'Chinesisch (Vereinfacht, Sonderverwaltungsregion Macau)' + +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:: + + $isValidLocale = Locales::exists($localeCode); + +Currencies +~~~~~~~~~~ -The translations of language and script names can be found in the language -bundle:: +The :class:`Symfony\\Component\\Intl\\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'); - $languages = Intl::getLanguageBundle()->getLanguageNames(); - // => array('ab' => 'Abkhazian', ...) + $currencies = Currencies::getNames(); + // ('currencyCode' => 'currencyName') + // => ['AFN' => 'Afghan Afghani', 'ALL' => 'Albanian Lek', ...] - $language = Intl::getLanguageBundle()->getLanguageName('de'); - // => 'German' + $currency = Currencies::getName('INR'); + // => 'Indian Rupee' - $language = Intl::getLanguageBundle()->getLanguageName('de', 'AT'); - // => 'Austrian German' + $symbol = Currencies::getSymbol('INR'); + // => '₹' - $scripts = Intl::getLanguageBundle()->getScriptNames(); - // => array('Arab' => 'Arabic', ...) +The fraction digits methods return the number of decimal digits to display when +formatting numbers with this currency. Depending on the currency, this value +can change if the number is used in cash transactions or in other scenarios +(e.g. accounting):: - $script = Intl::getLanguageBundle()->getScriptName('Hans'); - // => 'Simplified' + // Indian rupee defines the same value for both + $fractionDigits = Currencies::getFractionDigits('INR'); // returns: 2 + $cashFractionDigits = Currencies::getCashFractionDigits('INR'); // returns: 2 -All methods accept the translation locale as the last, optional parameter, -which defaults to the current default locale:: + // Swedish krona defines different values + $fractionDigits = Currencies::getFractionDigits('SEK'); // returns: 2 + $cashFractionDigits = Currencies::getCashFractionDigits('SEK'); // returns: 0 - $languages = Intl::getLanguageBundle()->getLanguageNames('de'); - // => array('ab' => 'Abchasisch', ...) +Some currencies require to round numbers to the nearest increment of some value +(e.g. 5 cents). This increment might be different if numbers are formatted for +cash transactions or other scenarios (e.g. accounting):: -Country Names -~~~~~~~~~~~~~ + // Indian rupee defines the same value for both + $roundingIncrement = Currencies::getRoundingIncrement('INR'); // returns: 0 + $cashRoundingIncrement = Currencies::getCashRoundingIncrement('INR'); // returns: 0 -The translations of country names can be found in the region bundle:: + // Canadian dollar defines different values because they have eliminated + // the smaller coins (1-cent and 2-cent) and prices in cash must be rounded to + // 5 cents (e.g. if price is 7.42 you pay 7.40; if price is 7.48 you pay 7.50) + $roundingIncrement = Currencies::getRoundingIncrement('CAD'); // returns: 0 + $cashRoundingIncrement = Currencies::getCashRoundingIncrement('CAD'); // returns: 5 - use Symfony\Component\Intl\Intl; +All methods (except for ``getFractionDigits()``, ``getCashFractionDigits()``, +``getRoundingIncrement()`` and ``getCashRoundingIncrement()``) accept the +translation locale as the last, optional parameter, which defaults to the +current default locale:: - \Locale::setDefault('en'); + $currencies = Currencies::getNames('de'); + // => ['AFN' => 'Afghanischer Afghani', 'EGP' => 'Ägyptisches Pfund', ...] - $countries = Intl::getRegionBundle()->getCountryNames(); - // => array('AF' => 'Afghanistan', ...) + $currency = Currencies::getName('INR', 'de'); + // => 'Indische Rupie' - $country = Intl::getRegionBundle()->getCountryName('GB'); - // => 'United Kingdom' +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:: -All methods accept the translation locale as the last, optional parameter, -which defaults to the current default locale:: + $isValidCurrency = Currencies::exists($currencyCode); - $countries = Intl::getRegionBundle()->getCountryNames('de'); - // => array('AF' => 'Afghanistan', ...) +.. _component-intl-timezones: -Locales -~~~~~~~ +Timezones +~~~~~~~~~ -The translations of locale names can be found in the locale bundle:: +The :class:`Symfony\\Component\\Intl\\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\Intl; + use Symfony\Component\Intl\Timezones; \Locale::setDefault('en'); - $locales = Intl::getLocaleBundle()->getLocaleNames(); - // => array('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'); - // => array('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(); - // => array('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 + + $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' + +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:: + + $isValidTimezone = Timezones::exists($timezoneId); + +.. _component-intl-emoji-transliteration: + +Emoji Transliteration +~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides utilities to translate emojis into their textual representation +in all languages. Read the documentation about :ref:`emoji transliteration ` +to learn more about this feature. + +Disk Space +---------- + +If you need to save disk space (e.g. because you deploy to some service with tight size +constraints), run this command (e.g. as an automated script after ``composer install``) to compress the +internal Symfony Intl data files using the PHP ``zlib`` extension: - $fractionDigits = Intl::getCurrencyBundle()->getFractionDigits('INR'); - // => 2 +.. code-block:: terminal - $roundingIncrement = Intl::getCurrencyBundle()->getRoundingIncrement('INR'); - // => 0 + # adjust the path to the 'compress' binary based on your application installation + $ php ./vendor/symfony/intl/Resources/bin/compress -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:: +Learn more +---------- - $currencies = Intl::getCurrencyBundle()->getCurrencyNames('de'); - // => array('AFN' => 'Afghanische Afghani', ...) +.. toctree:: + :maxdepth: 1 + :glob: -That's all you need to know for now. Have fun coding! + /reference/forms/types/country + /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: http://www.php.net/manual/en/book.intl.php -.. _install the intl extension: http://www.php.net/manual/en/intl.setup.php -.. _ICU library: http://site.icu-project.org/ +.. _ICU library: https://icu.unicode.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 +.. _`ISO 3166-1 alpha-3`: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3 +.. _`ISO 3166-1 numeric`: https://en.wikipedia.org/wiki/ISO_3166-1_numeric +.. _`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 (2T)`: https://en.wikipedia.org/wiki/ISO_639-2 diff --git a/components/ldap.rst b/components/ldap.rst new file mode 100644 index 00000000000..e52a341986c --- /dev/null +++ b/components/ldap.rst @@ -0,0 +1,200 @@ +The Ldap Component +================== + + The Ldap component provides a means to connect to an LDAP server (OpenLDAP or Active Directory). + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/ldap + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +The :class:`Symfony\\Component\\Ldap\\Ldap` class provides methods to authenticate +and query against an LDAP server. + +The ``Ldap`` class uses an :class:`Symfony\\Component\\Ldap\\Adapter\\AdapterInterface` +to communicate with an LDAP server. The :class:`adapter ` +for PHP's built-in LDAP extension, for example, can be configured using the +following options: + +``host`` + IP or hostname of the LDAP server + +``port`` + Port used to access the LDAP server + +``version`` + The version of the LDAP protocol to use + +``encryption`` + The encryption protocol: ``ssl``, ``tls`` or ``none`` (default) + +``connection_string`` + You may use this option instead of ``host`` and ``port`` to connect to the + LDAP server + +``optReferrals`` + Specifies whether to automatically follow referrals returned by the LDAP server + +``options`` + LDAP server's options as defined in + :class:`ConnectionOptions ` + +For example, to connect to a start-TLS secured LDAP server:: + + use Symfony\Component\Ldap\Ldap; + + $ldap = Ldap::create('ext_ldap', [ + 'host' => 'my-server', + 'encryption' => 'ssl', + ]); + +Or you could directly specify a connection string:: + + use Symfony\Component\Ldap\Ldap; + + $ldap = Ldap::create('ext_ldap', ['connection_string' => 'ldaps://my-server:636']); + +The :method:`Symfony\\Component\\Ldap\\Ldap::bind` method +authenticates a previously configured connection using both the +distinguished name (DN) and the password of a user:: + + use Symfony\Component\Ldap\Ldap; + // ... + + $ldap->bind($dn, $password); + +.. danger:: + + When the LDAP server allows unauthenticated binds, a blank password will always be valid. + +You can also use the :method:`Symfony\\Component\\Ldap\\Ldap::saslBind` method +for binding to an LDAP server using `SASL`_:: + + // this method defines other optional arguments like $mech, $realm, $authcId, etc. + $ldap->saslBind($dn, $password); + +After binding to the LDAP server, you can use the :method:`Symfony\\Component\\Ldap\\Ldap::whoami` +method to get the distinguished name (DN) of the authenticated and authorized user. + +.. versionadded:: 7.2 + + The ``saslBind()`` and ``whoami()`` methods were introduced in Symfony 7.2. + +Once bound (or if you enabled anonymous authentication on your +LDAP server), you may query the LDAP server using the +:method:`Symfony\\Component\\Ldap\\Ldap::query` method:: + + use Symfony\Component\Ldap\Ldap; + // ... + + $query = $ldap->query('dc=symfony,dc=com', '(&(objectclass=person)(ou=Maintainers))'); + $results = $query->execute(); + + foreach ($results as $entry) { + // Do something with the results + } + +By default, LDAP entries are lazy-loaded. If you wish to fetch +all entries in a single call and do something with the results' +array, you may use the +:method:`Symfony\\Component\\Ldap\\Adapter\\ExtLdap\\Collection::toArray` method:: + + use Symfony\Component\Ldap\Ldap; + // ... + + $query = $ldap->query('dc=symfony,dc=com', '(&(objectclass=person)(ou=Maintainers))'); + $results = $query->execute()->toArray(); + + // Do something with the results array + +By default, LDAP queries use the ``Symfony\Component\Ldap\Adapter\QueryInterface::SCOPE_SUB`` +scope, which corresponds to the ``LDAP_SCOPE_SUBTREE`` scope of the +:phpfunction:`ldap_search` function. You can also use ``SCOPE_BASE`` (related +to the ``LDAP_SCOPE_BASE`` scope of :phpfunction:`ldap_read`) and ``SCOPE_ONE`` +(related to the ``LDAP_SCOPE_ONELEVEL`` scope of :phpfunction:`ldap_list`):: + + use Symfony\Component\Ldap\Adapter\QueryInterface; + + $query = $ldap->query('dc=symfony,dc=com', '...', ['scope' => QueryInterface::SCOPE_ONE]); + +Use the ``filter`` option to only retrieve some specific attributes: + + $query = $ldap->query('dc=symfony,dc=com', '...', ['filter' => ['cn', 'mail']); + +Creating or Updating Entries +---------------------------- + +The Ldap component provides means to create new LDAP entries, update or even +delete existing ones:: + + use Symfony\Component\Ldap\Entry; + use Symfony\Component\Ldap\Ldap; + // ... + + $entry = new Entry('cn=Fabien Potencier,dc=symfony,dc=com', [ + 'sn' => ['fabpot'], + 'objectClass' => ['inetOrgPerson'], + ]); + + $entryManager = $ldap->getEntryManager(); + + // Creating a new entry + $entryManager->add($entry); + + // Finding and updating an existing entry + $query = $ldap->query('dc=symfony,dc=com', '(&(objectclass=person)(ou=Maintainers))'); + $result = $query->execute(); + $entry = $result[0]; + + $phoneNumber = $entry->getAttribute('phoneNumber'); + $isContractor = $entry->hasAttribute('contractorCompany'); + // attribute names in getAttribute() and hasAttribute() methods are case-sensitive + // pass FALSE as the second method argument to make them case-insensitive + $isContractor = $entry->hasAttribute('contractorCompany', false); + + $entry->setAttribute('email', ['fabpot@symfony.com']); + $entryManager->update($entry); + + // Adding or removing values to a multi-valued attribute is more efficient than using update() + $entryManager->addAttributeValues($entry, 'telephoneNumber', ['+1.111.222.3333', '+1.222.333.4444']); + $entryManager->removeAttributeValues($entry, 'telephoneNumber', ['+1.111.222.3333', '+1.222.333.4444']); + + // Removing an existing entry + $entryManager->remove(new Entry('cn=Test User,dc=symfony,dc=com')); + +Batch Updating +______________ + +Use the entry manager's :method:`Symfony\\Component\\Ldap\\Adapter\\ExtLdap\\EntryManager::applyOperations` +method to update multiple attributes at once:: + + use Symfony\Component\Ldap\Entry; + use Symfony\Component\Ldap\Ldap; + // ... + + $entry = new Entry('cn=Fabien Potencier,dc=symfony,dc=com', [ + 'sn' => ['fabpot'], + 'objectClass' => ['inetOrgPerson'], + ]); + + $entryManager = $ldap->getEntryManager(); + + // Adding multiple email addresses at once + $entryManager->applyOperations($entry->getDn(), [ + new UpdateOperation(LDAP_MODIFY_BATCH_ADD, 'mail', 'new1@example.com'), + new UpdateOperation(LDAP_MODIFY_BATCH_ADD, 'mail', 'new2@example.com'), + ]); + +Possible operation types are ``LDAP_MODIFY_BATCH_ADD``, ``LDAP_MODIFY_BATCH_REMOVE``, +``LDAP_MODIFY_BATCH_REMOVE_ALL``, ``LDAP_MODIFY_BATCH_REPLACE``. Parameter +``$values`` must be ``NULL`` when using ``LDAP_MODIFY_BATCH_REMOVE_ALL`` +operation type. + +.. _`SASL`: https://en.wikipedia.org/wiki/Simple_Authentication_and_Security_Layer diff --git a/components/lock.rst b/components/lock.rst new file mode 100644 index 00000000000..b8ba38c8fc7 --- /dev/null +++ b/components/lock.rst @@ -0,0 +1,1051 @@ +The Lock Component +================== + + The Lock Component creates and manages `locks`_, a mechanism to provide + exclusive access to a shared resource. + +If you're using the Symfony Framework, read the +:doc:`Symfony Framework Lock documentation `. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/lock + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +Locks are used to guarantee exclusive access to some shared resource. In +Symfony applications, you can use locks for example to ensure that a command is +not executed more than once at the same time (on the same or different servers). + +Locks are created using a :class:`Symfony\\Component\\Lock\\LockFactory` class, +which in turn requires another class to manage the storage of locks:: + + use Symfony\Component\Lock\LockFactory; + use Symfony\Component\Lock\Store\SemaphoreStore; + + $store = new SemaphoreStore(); + $factory = new LockFactory($store); + +The lock is created by calling the :method:`Symfony\\Component\\Lock\\LockFactory::createLock` +method. Its first argument is an arbitrary string that represents the locked +resource. Then, a call to the :method:`Symfony\\Component\\Lock\\LockInterface::acquire` +method will try to acquire the lock:: + + // ... + $lock = $factory->createLock('pdf-creation'); + + if ($lock->acquire()) { + // The resource "pdf-creation" is locked. + // You can compute and generate the invoice safely here. + + $lock->release(); + } + +If the lock can not be acquired, the method returns ``false``. The ``acquire()`` +method can be safely called repeatedly, even if the lock is already acquired. + +.. note:: + + Unlike other implementations, the Lock Component distinguishes lock + instances even when they are created for the same resource. It means that for + a given scope and resource one lock instance can be acquired multiple times. + If a lock has to be used by several services, they should share the same ``Lock`` + instance returned by the ``LockFactory::createLock`` method. + +.. tip:: + + If you don't release the lock explicitly, it will be released automatically + upon instance destruction. In some cases, it can be useful to lock a resource + across several requests. To disable the automatic release behavior, set the + third argument of the ``createLock()`` method to ``false``. + +Serializing Locks +----------------- + +The :class:`Symfony\\Component\\Lock\\Key` contains the state of the +:class:`Symfony\\Component\\Lock\\Lock` and can be serialized. This +allows the user to begin a long job in a process by acquiring the lock, and +continue the job in another process using the same lock. + +First, you may create a serializable class containing the resource and the +key of the lock:: + + // src/Lock/RefreshTaxonomy.php + namespace App\Lock; + + use Symfony\Component\Lock\Key; + + class RefreshTaxonomy + { + public function __construct( + private object $article, + private Key $key, + ) { + } + + public function getArticle(): object + { + return $this->article; + } + + public function getKey(): Key + { + return $this->key; + } + } + +Then, you can use this class to dispatch all that's needed for another process +to handle the rest of the job:: + + use App\Lock\RefreshTaxonomy; + use Symfony\Component\Lock\Key; + + $key = new Key('article.'.$article->getId()); + $lock = $factory->createLockFromKey( + $key, + 300, // ttl + false // autoRelease + ); + $lock->acquire(true); + + $this->bus->dispatch(new RefreshTaxonomy($article, $key)); + +.. note:: + + Don't forget to set the ``autoRelease`` argument to ``false`` in the + ``Lock`` instantiation to avoid releasing the lock when the destructor is + called. + +Not all stores are compatible with serialization and cross-process locking: for +example, the kernel will automatically release semaphores acquired by the +:ref:`SemaphoreStore ` store. If you use an incompatible +store (see :ref:`lock stores ` for supported stores), an +exception will be thrown when the application tries to serialize the key. + +.. _lock-blocking-locks: + +Blocking Locks +-------------- + +By default, when a lock cannot be acquired, the ``acquire`` method returns +``false`` immediately. To wait (indefinitely) until the lock can be created, +pass ``true`` as the argument of the ``acquire()`` method. This is called a +**blocking lock** because the execution of your application stops until the +lock is acquired:: + + use Symfony\Component\Lock\LockFactory; + use Symfony\Component\Lock\Store\FlockStore; + + $store = new FlockStore('/var/stores'); + $factory = new LockFactory($store); + + $lock = $factory->createLock('pdf-creation'); + $lock->acquire(true); + +When the store does not support blocking locks by implementing the +:class:`Symfony\\Component\\Lock\\BlockingStoreInterface` interface (see +:ref:`lock stores ` for supported stores), the ``Lock`` class +will retry to acquire the lock in a non-blocking way until the lock is +acquired. + +Expiring Locks +-------------- + +Locks created remotely are difficult to manage because there is no way for the +remote ``Store`` to know if the locker process is still alive. Due to bugs, +fatal errors or segmentation faults, it cannot be guaranteed that the +``release()`` method will be called, which would cause the resource to be +locked infinitely. + +The best solution in those cases is to create **expiring locks**, which are +released automatically after some amount of time has passed (called TTL for +*Time To Live*). This time, in seconds, is configured as the second argument of +the ``createLock()`` method. If needed, these locks can also be released early +with the ``release()`` method. + +The trickiest part when working with expiring locks is choosing the right TTL. +If it's too short, other processes could acquire the lock before finishing the +job; if it's too long and the process crashes before calling the ``release()`` +method, the resource will stay locked until the timeout:: + + // ... + // create an expiring lock that lasts 30 seconds (default is 300.0) + $lock = $factory->createLock('pdf-creation', ttl: 30); + + if (!$lock->acquire()) { + return; + } + try { + // perform a job during less than 30 seconds + } finally { + $lock->release(); + } + +.. tip:: + + To avoid leaving the lock in a locked state, it's recommended to wrap the + job in a try/catch/finally block to always try to release the expiring lock. + +In case of long-running tasks, it's better to start with a not too long TTL and +then use the :method:`Symfony\\Component\\Lock\\LockInterface::refresh` method +to reset the TTL to its original value:: + + // ... + $lock = $factory->createLock('pdf-creation', ttl: 30); + + if (!$lock->acquire()) { + return; + } + try { + while (!$finished) { + // perform a small part of the job. + + // renew the lock for 30 more seconds. + $lock->refresh(); + } + } finally { + $lock->release(); + } + +.. tip:: + + Another useful technique for long-running tasks is to pass a custom TTL as + an argument of the ``refresh()`` method to change the default lock TTL:: + + $lock = $factory->createLock('pdf-creation', ttl: 30); + // ... + // refresh the lock for 30 seconds + $lock->refresh(); + // ... + // refresh the lock for 600 seconds (next refresh() call will be 30 seconds again) + $lock->refresh(600); + +This component also provides two useful methods related to expiring locks: +``getRemainingLifetime()`` (which returns ``null`` or a ``float`` +as seconds) and ``isExpired()`` (which returns a boolean). + +Automatically Releasing The Lock +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Locks are automatically released when their Lock objects are destroyed. This is +an implementation detail that is important when sharing Locks between +processes. In the example below, ``pcntl_fork()`` creates two processes and the +Lock will be released automatically as soon as one process finishes:: + + // ... + $lock = $factory->createLock('pdf-creation'); + if (!$lock->acquire()) { + return; + } + + $pid = pcntl_fork(); + if (-1 === $pid) { + // Could not fork + exit(1); + } elseif ($pid) { + // Parent process + sleep(30); + } else { + // Child process + echo 'The lock will be released now.'; + exit(0); + } + // ... + +.. note:: + + In order for the above example to work, the `PCNTL`_ extension must be + installed. + +To disable this behavior, set the ``autoRelease`` argument of +``LockFactory::createLock()`` to ``false``. That will make the lock acquired +for 3600 seconds or until ``Lock::release()`` is called:: + + $lock = $factory->createLock( + 'pdf-creation', + 3600, // ttl + false // autoRelease + ); + +Shared Locks +------------ + +A shared or `readers-writer lock`_ is a synchronization primitive that allows +concurrent access for read-only operations, while write operations require +exclusive access. This means that multiple threads can read the data in parallel +but an exclusive lock is needed for writing or modifying data. They are used for +example for data structures that cannot be updated atomically and are invalid +until the update is complete. + +Use the :method:`Symfony\\Component\\Lock\\SharedLockInterface::acquireRead` +method to acquire a read-only lock, and +:method:`Symfony\\Component\\Lock\\LockInterface::acquire` method to acquire a +write lock:: + + $lock = $factory->createLock('user-'.$user->id); + if (!$lock->acquireRead()) { + return; + } + +Similar to the ``acquire()`` method, pass ``true`` as the argument of ``acquireRead()`` +to acquire the lock in a blocking mode:: + + $lock = $factory->createLock('user-'.$user->id); + $lock->acquireRead(true); + +.. note:: + + The `priority policy`_ of Symfony's shared locks depends on the underlying + store (e.g. Redis store prioritizes readers vs writers). + +When a read-only lock is acquired with the ``acquireRead()`` method, it's +possible to **promote** the lock, and change it to a write lock, by calling the +``acquire()`` method:: + + $lock = $factory->createLock('user-'.$userId); + $lock->acquireRead(true); + + if (!$this->shouldUpdate($userId)) { + return; + } + + $lock->acquire(true); // Promote the lock to a write lock + $this->update($userId); + +In the same way, it's possible to **demote** a write lock, and change it to a +read-only lock by calling the ``acquireRead()`` method. + +When the provided store does not implement the +:class:`Symfony\\Component\\Lock\\SharedLockStoreInterface` interface (see +:ref:`lock stores ` for supported stores), the ``Lock`` class +will fallback to a write lock by calling the ``acquire()`` method. + +The Owner of The Lock +--------------------- + +Locks that are acquired for the first time are :ref:`owned ` by the ``Lock`` instance that acquired +it. If you need to check whether the current ``Lock`` instance is (still) the owner of +a lock, you can use the ``isAcquired()`` method:: + + if ($lock->isAcquired()) { + // We (still) own the lock + } + +Because some lock stores have expiring locks, it is possible for an instance to +lose the lock it acquired automatically:: + + // If we cannot acquire ourselves, it means some other process is already working on it + if (!$lock->acquire()) { + return; + } + + $this->beginTransaction(); + + // Perform a very long process that might exceed TTL of the lock + + if ($lock->isAcquired()) { + // Still all good, no other instance has acquired the lock in the meantime, we're safe + $this->commit(); + } else { + // Bummer! Our lock has apparently exceeded TTL and another process has started in + // the meantime so it's not safe for us to commit. + $this->rollback(); + throw new \Exception('Process failed'); + } + +.. warning:: + + A common pitfall might be to use the ``isAcquired()`` method to check if + a lock has already been acquired by any process. As you can see in this example + you have to use ``acquire()`` for this. The ``isAcquired()`` method is used to check + if the lock has been acquired by the **current process** only. + +.. _lock-owner-technical-details: + +.. note:: + + Technically, the true owners of the lock are the ones that share the same instance of ``Key``, + not ``Lock``. But from a user perspective, ``Key`` is internal and you will likely only be working + with the ``Lock`` instance so it's easier to think of the ``Lock`` instance as being the one that + is the owner of the lock. + +.. _lock-stores: + +Available Stores +---------------- + +Locks are created and managed in ``Stores``, which are classes that implement +:class:`Symfony\\Component\\Lock\\PersistingStoreInterface` and, optionally, +:class:`Symfony\\Component\\Lock\\BlockingStoreInterface`. + +The component includes the following built-in store types: + +========================================================== ====== ======== ======== ======= ============= +Store Scope Blocking Expiring Sharing Serialization +========================================================== ====== ======== ======== ======= ============= +:ref:`FlockStore ` local yes no yes no +:ref:`MemcachedStore ` remote no yes no yes +:ref:`MongoDbStore ` remote no yes no yes +:ref:`PdoStore ` remote no yes no yes +:ref:`DoctrineDbalStore ` remote no yes no yes +:ref:`PostgreSqlStore ` remote yes no yes no +:ref:`DoctrineDbalPostgreSqlStore ` remote yes no yes no +:ref:`RedisStore ` remote no yes yes yes +:ref:`SemaphoreStore ` local yes no no no +:ref:`ZookeeperStore ` remote no no no no +========================================================== ====== ======== ======== ======= ============= + +.. tip:: + + Symfony includes two other special stores that are mostly useful for testing: + ``InMemoryStore``, which saves locks in memory during a process, and ``NullStore``, + which doesn't persist anything. + +.. versionadded:: 7.2 + + The :class:`Symfony\\Component\\Lock\\Store\\NullStore` was introduced in Symfony 7.2. + +.. _lock-store-flock: + +FlockStore +~~~~~~~~~~ + +The FlockStore uses the file system on the local computer to create the locks. +It does not support expiration, but the lock is automatically released when the +lock object goes out of scope and is freed by the garbage collector (for example +when the PHP process ends):: + + use Symfony\Component\Lock\Store\FlockStore; + + // the argument is the path of the directory where the locks are created + // if none is given, sys_get_temp_dir() is used internally. + $store = new FlockStore('/var/stores'); + +.. warning:: + + Beware that some file systems (such as some types of NFS) do not support + locking. In those cases, it's better to use a directory on a local disk + drive or a remote store. + +.. _lock-store-memcached: + +MemcachedStore +~~~~~~~~~~~~~~ + +The MemcachedStore saves locks on a Memcached server, it requires a Memcached +connection implementing the ``\Memcached`` class. This store does not +support blocking, and expects a TTL to avoid stalled locks:: + + use Symfony\Component\Lock\Store\MemcachedStore; + + $memcached = new \Memcached(); + $memcached->addServer('localhost', 11211); + + $store = new MemcachedStore($memcached); + +.. note:: + + Memcached does not support TTL lower than 1 second. + +.. _lock-store-mongodb: + +MongoDbStore +~~~~~~~~~~~~ + +The MongoDbStore saves locks on a MongoDB server ``>=2.2``, it requires a +``\MongoDB\Collection`` or ``\MongoDB\Client`` from `mongodb/mongodb`_ or a +`MongoDB Connection String`_. +This store does not support blocking and expects a TTL to +avoid stalled locks:: + + use Symfony\Component\Lock\Store\MongoDbStore; + + $mongo = 'mongodb://localhost/database?collection=lock'; + $options = [ + 'gcProbability' => 0.001, + 'database' => 'myapp', + 'collection' => 'lock', + 'uriOptions' => [], + 'driverOptions' => [], + ]; + $store = new MongoDbStore($mongo, $options); + +The ``MongoDbStore`` takes the following ``$options`` (depending on the first parameter type): + +============= ================================================================================================ +Option Description +============= ================================================================================================ +gcProbability Should a TTL Index be created expressed as a probability from 0.0 to 1.0 (Defaults to ``0.001``) +database The name of the database +collection The name of the collection +uriOptions Array of URI options for `MongoDBClient::__construct`_ +driverOptions Array of driver options for `MongoDBClient::__construct`_ +============= ================================================================================================ + +When the first parameter is a: + +``MongoDB\Collection``: + +- ``$options['database']`` is ignored +- ``$options['collection']`` is ignored + +``MongoDB\Client``: + +- ``$options['database']`` is mandatory +- ``$options['collection']`` is mandatory + +MongoDB Connection String: + +- ``$options['database']`` is used otherwise ``/path`` from the DSN, at least one is mandatory +- ``$options['collection']`` is used otherwise ``?collection=`` from the DSN, at least one is mandatory + +.. note:: + + The ``collection`` querystring parameter is not part of the `MongoDB Connection String`_ definition. + It is used to allow constructing a ``MongoDbStore`` using a `Data Source Name (DSN)`_ without ``$options``. + +.. _lock-store-pdo: + +PdoStore +~~~~~~~~ + +The PdoStore saves locks in an SQL database. It requires a `PDO`_ connection or a `Data Source Name (DSN)`_. +This store does not support blocking, and expects a TTL to avoid stalled locks:: + + use Symfony\Component\Lock\Store\PdoStore; + + // a PDO instance or DSN for lazy connecting through PDO + $databaseConnectionOrDSN = 'mysql:host=127.0.0.1;dbname=app'; + $store = new PdoStore($databaseConnectionOrDSN, ['db_username' => 'myuser', 'db_password' => 'mypassword']); + +.. note:: + + This store does not support TTL lower than 1 second. + +The table where values are stored is created automatically on the first call to +the :method:`Symfony\\Component\\Lock\\Store\\PdoStore::save` method. +You can also create this table explicitly by calling the +:method:`Symfony\\Component\\Lock\\Store\\PdoStore::createTable` method in +your code. + +.. _lock-store-dbal: + +DoctrineDbalStore +~~~~~~~~~~~~~~~~~ + +The DoctrineDbalStore saves locks in an SQL database. It is identical to PdoStore +but requires a `Doctrine DBAL Connection`_, or a `Doctrine DBAL URL`_. This store +does not support blocking, and expects a TTL to avoid stalled locks:: + + use Symfony\Component\Lock\Store\DoctrineDbalStore; + + // a Doctrine DBAL connection or DSN + $connectionOrURL = 'mysql://myuser:mypassword@127.0.0.1/app'; + $store = new DoctrineDbalStore($connectionOrURL); + +.. note:: + + This store does not support TTL lower than 1 second. + +The table where values are stored will be automatically generated when your run +the command: + +.. code-block:: terminal + + $ php bin/console make:migration + +If you prefer to create the table yourself and it has not already been created, you can +create this table explicitly by calling the +:method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::createTable` method. +You can also add this table to your schema by calling +:method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::configureSchema` method +in your code + +If the table has not been created upstream, it will be created automatically on the first call to +the :method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::save` method. + +.. _lock-store-pgsql: + +PostgreSqlStore +~~~~~~~~~~~~~~~ + +The PostgreSqlStore uses `Advisory Locks`_ provided by PostgreSQL. It requires a +`PDO`_ connection or a `Data Source Name (DSN)`_. It supports native blocking, as well as sharing +locks:: + + use Symfony\Component\Lock\Store\PostgreSqlStore; + + // a PDO instance or DSN for lazy connecting through PDO + $databaseConnectionOrDSN = 'pgsql:host=localhost;port=5634;dbname=app'; + $store = new PostgreSqlStore($databaseConnectionOrDSN, ['db_username' => 'myuser', 'db_password' => 'mypassword']); + +In opposite to the ``PdoStore``, the ``PostgreSqlStore`` does not need a table to +store locks and it does not expire. + +.. _lock-store-dbal-pgsql: + +DoctrineDbalPostgreSqlStore +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The DoctrineDbalPostgreSqlStore uses `Advisory Locks`_ provided by PostgreSQL. +It is identical to PostgreSqlStore but requires a `Doctrine DBAL Connection`_ or +a `Doctrine DBAL URL`_. It supports native blocking, as well as sharing locks:: + + use Symfony\Component\Lock\Store\DoctrineDbalPostgreSqlStore; + + // a Doctrine Connection or DSN + $databaseConnectionOrDSN = 'postgresql+advisory://myuser:mypassword@127.0.0.1:5634/lock'; + $store = new DoctrineDbalPostgreSqlStore($databaseConnectionOrDSN); + +In opposite to the ``DoctrineDbalStore``, the ``DoctrineDbalPostgreSqlStore`` does not need a table to +store locks and does not expire. + +.. _lock-store-redis: + +RedisStore +~~~~~~~~~~ + +The RedisStore saves locks on a Redis server, it requires a Redis connection +implementing the ``\Redis``, ``\RedisArray``, ``\RedisCluster``, ``\Relay\Relay`` or +``\Predis`` classes. This store does not support blocking, and expects a TTL to +avoid stalled locks:: + + use Symfony\Component\Lock\Store\RedisStore; + + $redis = new \Redis(); + $redis->connect('localhost'); + + $store = new RedisStore($redis); + +.. _lock-store-semaphore: + +SemaphoreStore +~~~~~~~~~~~~~~ + +The SemaphoreStore uses the `PHP semaphore functions`_ to create the locks:: + + use Symfony\Component\Lock\Store\SemaphoreStore; + + $store = new SemaphoreStore(); + +.. _lock-store-combined: + +CombinedStore +~~~~~~~~~~~~~ + +The CombinedStore is designed for High Availability applications because it +manages several stores in sync (for example, several Redis servers). When a +lock is acquired, it forwards the call to all the managed stores, and it +collects their responses. If a simple majority of stores have acquired the +lock, then the lock is considered acquired:: + + use Symfony\Component\Lock\Store\CombinedStore; + use Symfony\Component\Lock\Store\RedisStore; + use Symfony\Component\Lock\Strategy\ConsensusStrategy; + + $stores = []; + foreach (['server1', 'server2', 'server3'] as $server) { + $redis = new \Redis(); + $redis->connect($server); + + $stores[] = new RedisStore($redis); + } + + $store = new CombinedStore($stores, new ConsensusStrategy()); + +Instead of the simple majority strategy (``ConsensusStrategy``) an +``UnanimousStrategy`` can be used to require the lock to be acquired in all +the stores:: + + use Symfony\Component\Lock\Store\CombinedStore; + use Symfony\Component\Lock\Strategy\UnanimousStrategy; + + $store = new CombinedStore($stores, new UnanimousStrategy()); + +.. warning:: + + In order to get high availability when using the ``ConsensusStrategy``, the + minimum cluster size must be three servers. This allows the cluster to keep + working when a single server fails (because this strategy requires that the + lock is acquired for more than half of the servers). + +.. _lock-store-zookeeper: + +ZookeeperStore +~~~~~~~~~~~~~~ + +The ZookeeperStore saves locks on a `ZooKeeper`_ server. It requires a ZooKeeper +connection implementing the ``\Zookeeper`` class. This store does not +support blocking and expiration but the lock is automatically released when the +PHP process is terminated:: + + use Symfony\Component\Lock\Store\ZookeeperStore; + + $zookeeper = new \Zookeeper('localhost:2181'); + // use the following to define a high-availability cluster: + // $zookeeper = new \Zookeeper('localhost1:2181,localhost2:2181,localhost3:2181'); + + $store = new ZookeeperStore($zookeeper); + +.. note:: + + Zookeeper does not require a TTL as the nodes used for locking are ephemeral + and die when the PHP process is terminated. + +Reliability +----------- + +The component guarantees that the same resource can't be locked twice as long as +the component is used in the following way. + +Remote Stores +~~~~~~~~~~~~~ + +Remote stores (:ref:`MemcachedStore `, +:ref:`MongoDbStore `, +:ref:`PdoStore `, +:ref:`PostgreSqlStore `, +: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 +the ``Lock``. + +Every concurrent process must store the ``Lock`` on the same server. Otherwise two +different machines may allow two different processes to acquire the same ``Lock``. + +.. warning:: + + To guarantee that the same server will always be safe, do not use Memcached + behind a LoadBalancer, a cluster or round-robin DNS. Even if the main server + is down, the calls must not be forwarded to a backup or failover server. + +Expiring Stores +~~~~~~~~~~~~~~~ + +Expiring stores (:ref:`MemcachedStore `, +:ref:`MongoDbStore `, +: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. + +The ``Lock`` provides several methods to check its health. The ``isExpired()`` +method checks whether or not its lifetime is over and the ``getRemainingLifetime()`` +method returns its time to live in seconds. + +Using the above methods, a robust code would be:: + + // ... + $lock = $factory->createLock('pdf-creation', 30); + + if (!$lock->acquire()) { + return; + } + while (!$finished) { + if ($lock->getRemainingLifetime() <= 5) { + if ($lock->isExpired()) { + // lock was lost, perform a rollback or send a notification + throw new \RuntimeException('Lock lost during the overall process'); + } + + $lock->refresh(); + } + + // Perform the task whose duration MUST be less than 5 seconds + } + +.. warning:: + + Choose wisely the lifetime of the ``Lock`` and check whether its remaining + time to live is enough to perform the task. + +.. warning:: + + Storing a ``Lock`` usually takes a few milliseconds, but network conditions + may increase that time a lot (up to a few seconds). Take that into account + when choosing the right TTL. + +By design, locks are stored on servers with a defined lifetime. If the date or +time of the machine changes, a lock could be released sooner than expected. + +.. warning:: + + To guarantee that date won't change, the NTP service should be disabled + and the date should be updated when the service is stopped. + +FlockStore +~~~~~~~~~~ + +By using the file system, this ``Store`` is reliable as long as concurrent +processes use the same physical directory to store locks. + +Processes must run on the same machine, virtual machine or container. +Be careful when updating a Kubernetes or Swarm service because, for a short +period of time, there can be two containers running in parallel. + +The absolute path to the directory must remain the same. Be careful of symlinks +that could change at anytime: Capistrano and blue/green deployment often use +that trick. Be careful when the path to that directory changes between two +deployments. + +Some file systems (such as some types of NFS) do not support locking. + +.. warning:: + + All concurrent processes must use the same physical file system by running + on the same machine and using the same absolute path to the lock directory. + + Using a ``FlockStore`` in an HTTP context is incompatible with multiple + front servers, unless to ensure that the same resource will always be + locked on the same machine or to use a well configured shared file system. + +Files on the file system can be removed during a maintenance operation. For +instance, to clean up the ``/tmp`` directory or after a reboot of the machine +when a directory uses ``tmpfs``. It's not an issue if the lock is released when +the process ended, but it is in case of ``Lock`` reused between requests. + +.. danger:: + + Do not store locks on a volatile file system if they have to be reused in + several requests. + +MemcachedStore +~~~~~~~~~~~~~~ + +The way Memcached works is to store items in memory. That means that by using +the :ref:`MemcachedStore ` the locks are not persisted +and may disappear by mistake at any time. + +If the Memcached service or the machine hosting it restarts, every lock would +be lost without notifying the running processes. + +.. warning:: + + To avoid that someone else acquires a lock after a restart, it's recommended + to delay service start and wait at least as long as the longest lock TTL. + +By default Memcached uses a LRU mechanism to remove old entries when the service +needs space to add new items. + +.. warning:: + + The number of items stored in Memcached must be under control. If it's not + possible, LRU should be disabled and Lock should be stored in a dedicated + Memcached service away from Cache. + +When the Memcached service is shared and used for multiple usage, Locks could be +removed by mistake. For instance some implementation of the PSR-6 ``clear()`` +method uses the Memcached's ``flush()`` method which purges and removes everything. + +.. danger:: + + The method ``flush()`` must not be called, or locks should be stored in a + dedicated Memcached service away from Cache. + +MongoDbStore +~~~~~~~~~~~~ + +.. warning:: + + The locked resource name is indexed in the ``_id`` field of the lock + collection. Beware that an indexed field's value in MongoDB can be + `a maximum of 1024 bytes in length`_ including the structural overhead. + +A TTL index must be used to automatically clean up expired locks. +Such an index can be created manually: + +.. code-block:: javascript + + db.lock.createIndex( + { "expires_at": 1 }, + { "expireAfterSeconds": 0 } + ) + +Alternatively, the method ``MongoDbStore::createTtlIndex(int $expireAfterSeconds = 0)`` +can be called once to create the TTL index during database setup. Read more +about `Expire Data from Collections by Setting TTL`_ in MongoDB. + +.. tip:: + + ``MongoDbStore`` will attempt to automatically create a TTL index. It's + recommended to set constructor option ``gcProbability`` to ``0.0`` to + disable this behavior if you have manually dealt with TTL index creation. + +.. warning:: + + This store relies on all PHP application and database nodes to have + synchronized clocks for lock expiry to occur at the correct time. To ensure + locks don't expire prematurely; the lock TTL should be set with enough extra + time in ``expireAfterSeconds`` to account for any clock drift between nodes. + +``writeConcern`` and ``readConcern`` are not specified by MongoDbStore meaning +the collection's settings will take effect. +``readPreference`` is ``primary`` for all queries. +Read more about `Replica Set Read and Write Semantics`_ in MongoDB. + +PdoStore +~~~~~~~~ + +The PdoStore relies on the `ACID`_ properties of the SQL engine. + +.. warning:: + + In a cluster configured with multiple primaries, ensure writes are + synchronously propagated to every node, or always use the same node. + +.. warning:: + + Some SQL engines like MySQL allow to disable the unique constraint check. + Ensure that this is not the case ``SET unique_checks=1;``. + +In order to purge old locks, this store uses a current datetime to define an +expiration date reference. This mechanism relies on all server nodes to +have synchronized clocks. + +.. warning:: + + To ensure locks don't expire prematurely; the TTLs should be set with + enough extra time to account for any clock drift between nodes. + +PostgreSqlStore +~~~~~~~~~~~~~~~ + +The PostgreSqlStore relies on the `Advisory Locks`_ properties of the PostgreSQL +database. That means that by using :ref:`PostgreSqlStore ` +the locks will be automatically released at the end of the session in case the +client cannot unlock for any reason. + +If the PostgreSQL service or the machine hosting it restarts, every lock would +be lost without notifying the running processes. + +If the TCP connection is lost, the PostgreSQL may release locks without +notifying the application. + +RedisStore +~~~~~~~~~~ + +The way Redis works is to store items in memory. That means that by using +the :ref:`RedisStore ` the locks are not persisted +and may disappear by mistake at any time. + +If the Redis service or the machine hosting it restarts, every locks would +be lost without notifying the running processes. + +.. warning:: + + To avoid that someone else acquires a lock after a restart, it's recommended + to delay service start and wait at least as long as the longest lock TTL. + +.. tip:: + + Redis can be configured to persist items on disk, but this option would + slow down writes on the service. This could go against other uses of the + server. + +When the Redis service is shared and used for multiple usages, locks could be +removed by mistake. + +.. danger:: + + The command ``FLUSHDB`` must not be called, or locks should be stored in a + dedicated Redis service away from Cache. + +CombinedStore +~~~~~~~~~~~~~ + +Combined stores allow the storage of locks across several backends. It's a common +mistake to think that the lock mechanism will be more reliable. This is wrong. +The ``CombinedStore`` will be, at best, as reliable as the least reliable of +all managed stores. As soon as one managed store returns erroneous information, +the ``CombinedStore`` won't be reliable. + +.. warning:: + + All concurrent processes must use the same configuration, with the same + amount of managed stored and the same endpoint. + +.. tip:: + + Instead of using a cluster of Redis or Memcached servers, it's better to use + a ``CombinedStore`` with a single server per managed store. + +SemaphoreStore +~~~~~~~~~~~~~~ + +Semaphores are handled by the Kernel level. In order to be reliable, processes +must run on the same machine, virtual machine or container. Be careful when +updating a Kubernetes or Swarm service because for a short period of time, there +can be two running containers in parallel. + +.. warning:: + + All concurrent processes must use the same machine. Before starting a + concurrent process on a new machine, check that other processes are stopped + on the old one. + +.. warning:: + + When running on systemd with non-system user and option ``RemoveIPC=yes`` + (default value), locks are deleted by systemd when that user logs out. + Check that process is run with a system user (UID <= SYS_UID_MAX) with + ``SYS_UID_MAX`` defined in ``/etc/login.defs``, or set the option + ``RemoveIPC=off`` in ``/etc/systemd/logind.conf``. + +ZookeeperStore +~~~~~~~~~~~~~~ + +The way ZookeeperStore works is by maintaining locks as ephemeral nodes on the +server. That means that by using :ref:`ZookeeperStore ` +the locks will be automatically released at the end of the session in case the +client cannot unlock for any reason. + +If the ZooKeeper service or the machine hosting it restarts, every lock would +be lost without notifying the running processes. + +.. tip:: + + To use ZooKeeper's high-availability feature, you can setup a cluster of + multiple servers so that in case one of the server goes down, the majority + will still be up and serving the requests. All the available servers in the + cluster will see the same state. + +.. note:: + + As this store does not support multi-level node locks, since the clean up of + intermediate nodes becomes an overhead, all locks are maintained at the root + level. + +Overall +~~~~~~~ + +Changing the configuration of stores should be done very carefully. For +instance, during the deployment of a new version. Processes with new +configuration must not be started while old processes with old configuration +are still running. + +.. _`a maximum of 1024 bytes in length`: https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit +.. _`ACID`: https://en.wikipedia.org/wiki/ACID +.. _`Advisory Locks`: https://www.postgresql.org/docs/current/explicit-locking.html +.. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name +.. _`Doctrine DBAL Connection`: https://github.com/doctrine/dbal/blob/master/src/Connection.php +.. _`Doctrine DBAL URL`: https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +.. _`Expire Data from Collections by Setting TTL`: https://docs.mongodb.com/manual/tutorial/expire-data/ +.. _`locks`: https://en.wikipedia.org/wiki/Lock_(computer_science) +.. _`MongoDB Connection String`: https://docs.mongodb.com/manual/reference/connection-string/ +.. _`mongodb/mongodb`: https://packagist.org/packages/mongodb/mongodb +.. _`MongoDBClient::__construct`: https://docs.mongodb.com/php-library/current/reference/method/MongoDBClient__construct/ +.. _`PDO`: https://www.php.net/pdo +.. _`PHP semaphore functions`: https://www.php.net/manual/en/book.sem.php +.. _`Replica Set Read and Write Semantics`: https://docs.mongodb.com/manual/applications/replication/ +.. _`ZooKeeper`: https://zookeeper.apache.org/ +.. _`readers-writer lock`: https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock +.. _`priority policy`: https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock#Priority_policies +.. _`PCNTL`: https://www.php.net/manual/book.pcntl.php diff --git a/components/map.rst.inc b/components/map.rst.inc deleted file mode 100644 index e29df506e55..00000000000 --- a/components/map.rst.inc +++ /dev/null @@ -1,118 +0,0 @@ -* :doc:`/components/using_components` - -* **Class Loader** - - * :doc:`/components/class_loader` - -* :doc:`/components/config/index` - - * :doc:`/components/config/introduction` - * :doc:`/components/config/resources` - * :doc:`/components/config/caching` - * :doc:`/components/config/definition` - -* :doc:`/components/console/index` - - * :doc:`/components/console/introduction` - * :doc:`/components/console/usage` - * :doc:`/components/console/single_command_tool` - * :doc:`/components/console/events` - * :doc:`/components/console/helpers/index` - -* **CSS Selector** - - * :doc:`/components/css_selector` - -* **Debug** - - * :doc:`/components/debug` - -* :doc:`/components/dependency_injection/index` - - * :doc:`/components/dependency_injection/introduction` - * :doc:`/components/dependency_injection/types` - * :doc:`/components/dependency_injection/parameters` - * :doc:`/components/dependency_injection/definitions` - * :doc:`/components/dependency_injection/compilation` - * :doc:`/components/dependency_injection/tags` - * :doc:`/components/dependency_injection/factories` - * :doc:`/components/dependency_injection/configurators` - * :doc:`/components/dependency_injection/parentservices` - * :doc:`/components/dependency_injection/advanced` - * :doc:`/components/dependency_injection/workflow` - -* **DOM Crawler** - - * :doc:`/components/dom_crawler` - -* :doc:`/components/event_dispatcher/index` - - * :doc:`/components/event_dispatcher/introduction` - * :doc:`/components/event_dispatcher/container_aware_dispatcher` - * :doc:`/components/event_dispatcher/generic_event` - -* **Filesystem** - - * :doc:`/components/filesystem` - -* **Finder** - - * :doc:`/components/finder` - -* :doc:`/components/http_foundation/index` - - * :doc:`/components/http_foundation/introduction` - * :doc:`/components/http_foundation/sessions` - * :doc:`/components/http_foundation/session_configuration` - * :doc:`/components/http_foundation/session_testing` - * :doc:`/components/http_foundation/session_php_bridge` - * :doc:`/components/http_foundation/trusting_proxies` - -* :doc:`/components/http_kernel/index` - - * :doc:`/components/http_kernel/introduction` - -* **Intl** - - * :doc:`/components/intl` - -* **Options Resolver** - - * :doc:`/components/options_resolver` - -* **Process** - - * :doc:`/components/process` - -* :doc:`/components/property_access/index` - - * :doc:`/components/property_access/introduction` - -* :doc:`/components/routing/index` - - * :doc:`/components/routing/introduction` - * :doc:`/components/routing/hostname_pattern` - -* **Serializer** - - * :doc:`/components/serializer` - -* **Stopwatch** - - * :doc:`/components/stopwatch` - -* :doc:`/components/security/index` - - * :doc:`/components/security/introduction` - * :doc:`/components/security/firewall` - * :doc:`/components/security/authentication` - * :doc:`/components/security/authorization` - -* **Templating** - - * :doc:`/components/templating` - -* :doc:`/components/yaml/index` - - * :doc:`/components/yaml/introduction` - * :doc:`/components/yaml/yaml_format` diff --git a/components/messenger.rst b/components/messenger.rst new file mode 100644 index 00000000000..8d6652fb160 --- /dev/null +++ b/components/messenger.rst @@ -0,0 +1,365 @@ +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`_. + +.. seealso:: + + This article explains how to use the Messenger features as an independent + component in any PHP application. Read the :doc:`/messenger` article to + learn about how to use it in Symfony applications. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/messenger + +.. include:: /components/require_autoload.rst.inc + +Concepts +-------- + +.. raw:: html + + + +**Sender**: + Responsible for serializing and sending messages to *something*. This + something can be a message broker or a third party API for example. + +**Receiver**: + Responsible for retrieving, deserializing and forwarding messages to handler(s). + This can be a message queue puller or an API endpoint for example. + +**Handler**: + Responsible for handling messages using the business logic applicable to the messages. + Handlers are called by the ``HandleMessageMiddleware`` middleware. + +**Middleware**: + Middleware can access the message and its wrapper (the envelope) while it is + dispatched through the bus. + Literally *"the software in the middle"*, those are not about core concerns + (business logic) of an application. Instead, they are cross cutting concerns + applicable throughout the application and affecting the entire message bus. + For instance: logging, validating a message, starting a transaction, ... + They are also responsible for calling the next middleware in the chain, + which means they can tweak the envelope, by adding stamps to it or even + replacing it, as well as interrupt the middleware chain. Middleware are called + both when a message is originally dispatched and again later when a message + is received from a transport. + +**Envelope**: + Messenger specific concept, it gives full flexibility inside the message bus, + by wrapping the messages into it, allowing to add useful information inside + through *envelope stamps*. + +**Envelope Stamps**: + Piece of information you need to attach to your message: serializer context + to use for transport, markers identifying a received message or any sort of + metadata your middleware or transport layer may use. + +Bus +--- + +The bus is used to dispatch messages. The behavior of the bus is in its ordered +middleware stack. The component comes with a set of middleware that you can use. + +When using the message bus with Symfony's FrameworkBundle, the following middleware +are configured for you: + +#. :class:`Symfony\\Component\\Messenger\\Middleware\\SendMessageMiddleware` (enables asynchronous processing, logs the processing of your messages if you provide a logger) +#. :class:`Symfony\\Component\\Messenger\\Middleware\\HandleMessageMiddleware` (calls the registered handler(s)) + +Example:: + + use App\Message\MyMessage; + use App\MessageHandler\MyMessageHandler; + use Symfony\Component\Messenger\Handler\HandlersLocator; + use Symfony\Component\Messenger\MessageBus; + use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; + + $handler = new MyMessageHandler(); + + $bus = new MessageBus([ + new HandleMessageMiddleware(new HandlersLocator([ + MyMessage::class => [$handler], + ])), + ]); + + $bus->dispatch(new MyMessage(/* ... */)); + +.. note:: + + Every middleware needs to implement the :class:`Symfony\\Component\\Messenger\\Middleware\\MiddlewareInterface`. + +Handlers +-------- + +Once dispatched to the bus, messages will be handled by a "message handler". A +message handler is a PHP callable (i.e. a function or an instance of a class) +that will do the required processing for your message:: + + namespace App\MessageHandler; + + use App\Message\MyMessage; + + class MyMessageHandler + { + public function __invoke(MyMessage $message): void + { + // Message processing... + } + } + +.. _messenger-envelopes: + +Adding Metadata to Messages (Envelopes) +--------------------------------------- + +If you need to add metadata or some configuration to a message, wrap it with the +:class:`Symfony\\Component\\Messenger\\Envelope` class and add stamps. +For example, to set the serialization groups used when the message goes +through the transport layer, use the ``SerializerStamp`` stamp:: + + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\Stamp\SerializerStamp; + + $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'], + ])) + ); + +Here are some important envelope stamps that are shipped with the Symfony Messenger: + +* :class:`Symfony\\Component\\Messenger\\Stamp\\DelayStamp`, + to delay handling of an asynchronous message. +* :class:`Symfony\\Component\\Messenger\\Stamp\\DispatchAfterCurrentBusStamp`, + to make the message be handled after the current bus has executed. Read more + at :ref:`messenger-transactional-messages`. +* :class:`Symfony\\Component\\Messenger\\Stamp\\HandledStamp`, + a stamp that marks the message as handled by a specific handler. + Allows accessing the handler returned value and the handler name. +* :class:`Symfony\\Component\\Messenger\\Stamp\\ReceivedStamp`, + an internal stamp that marks the message as received from a transport. +* :class:`Symfony\\Component\\Messenger\\Stamp\\SentStamp`, + a stamp that marks the message as sent by a specific sender. + Allows accessing the sender FQCN and the alias if available from the + :class:`Symfony\\Component\\Messenger\\Transport\\Sender\\SendersLocator`. +* :class:`Symfony\\Component\\Messenger\\Stamp\\SerializerStamp`, + to configure the serialization groups used by the transport. +* :class:`Symfony\\Component\\Messenger\\Stamp\\ValidationStamp`, + to configure the validation groups used when the validation middleware is enabled. +* :class:`Symfony\\Component\\Messenger\\Stamp\\ErrorDetailsStamp`, + an internal stamp when a message fails due to an exception in the handler. +* :class:`Symfony\\Component\\Scheduler\\Messenger\\ScheduledStamp`, + a stamp that marks the message as produced by a scheduler. This helps + differentiate it from messages created "manually". You can learn more about it + in the :doc:`Scheduler documentation `. + +.. note:: + + The :class:`Symfony\\Component\\Messenger\\Stamp\\ErrorDetailsStamp` stamp + contains a :class:`Symfony\\Component\\ErrorHandler\\Exception\\FlattenException`, + which is a representation of the exception that made the message fail. You can + get this exception with the + :method:`Symfony\\Component\\Messenger\\Stamp\\ErrorDetailsStamp::getFlattenException` + method. This exception is normalized thanks to the + :class:`Symfony\\Component\\Messenger\\Transport\\Serialization\\Normalizer\\FlattenExceptionNormalizer` + which helps error reporting in the Messenger context. + +Instead of dealing directly with the messages in the middleware you receive the envelope. +Hence you can inspect the envelope content and its stamps, or add any:: + + use App\Message\Stamp\AnotherStamp; + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\Middleware\MiddlewareInterface; + use Symfony\Component\Messenger\Middleware\StackInterface; + use Symfony\Component\Messenger\Stamp\ReceivedStamp; + + class MyOwnMiddleware implements MiddlewareInterface + { + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + if (null !== $envelope->last(ReceivedStamp::class)) { + // Message just has been received... + + // You could for example add another stamp. + $envelope = $envelope->with(new AnotherStamp(/* ... */)); + } else { + // Message was just originally dispatched + } + + return $stack->next()->handle($envelope, $stack); + } + } + +The above example will forward the message to the next middleware with an +additional stamp *if* the message has just been received (i.e. has at least one +``ReceivedStamp`` stamp). You can create your own stamps by implementing +:class:`Symfony\\Component\\Messenger\\Stamp\\StampInterface`. + +If you want to examine all stamps on an envelope, use the ``$envelope->all()`` +method, which returns all stamps grouped by type (FQCN). Alternatively, you can +iterate through all stamps of a specific type by using the FQCN as first +parameter of this method (e.g. ``$envelope->all(ReceivedStamp::class)``). + +.. note:: + + Any stamp must be serializable using the Symfony Serializer component + if going through transport using the :class:`Symfony\\Component\\Messenger\\Transport\\Serialization\\Serializer` + base serializer. + +Transports +---------- + +In order to send and receive messages, you will have to configure a transport. A +transport will be responsible for communicating with your message broker or 3rd parties. + +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 (using the :doc:`Mime ` and +:doc:`Mailer ` components). + +Using the :class:`Symfony\\Component\\Messenger\\Transport\\Sender\\SenderInterface`, +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 + { + public function __construct( + private MailerInterface $mailer, + private string $toEmail, + ) { + } + + public function send(Envelope $envelope): Envelope + { + $message = $envelope->getMessage(); + + if (!$message instanceof ImportantAction) { + throw new \InvalidArgumentException(sprintf('This transport only supports "%s" messages.', ImportantAction::class)); + } + + $this->mailer->send( + (new Email()) + ->to($this->toEmail) + ->subject('Important action made') + ->html('

Important action

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

') + ); + + return $envelope; + } + } + +Your own Receiver +~~~~~~~~~~~~~~~~~ + +A receiver is responsible for getting messages from a source and dispatching +them to the application. + +Imagine you already processed some "orders" in your application using a +``NewOrder`` message. Now you want to integrate with a 3rd party or a legacy +application but you can't use an API and need to use a shared CSV file with new +orders. + +You will read this CSV file and dispatch a ``NewOrder`` message. All you need to +do is to write your own CSV receiver:: + + namespace App\MessageReceiver; + + use App\Message\NewOrder; + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\Exception\MessageDecodingFailedException; + use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; + use Symfony\Component\Serializer\SerializerInterface; + + class NewOrdersFromCsvFileReceiver implements ReceiverInterface + { + private $connection; + + public function __construct( + private SerializerInterface $serializer, + private string $filePath, + ) { + // Available connection bundled with the Messenger component + // can be found in "Symfony\Component\Messenger\Bridge\*\Transport\Connection". + $this->connection = /* create your connection */; + } + + public function get(): iterable + { + // Receive the envelope according to your transport ($yourEnvelope here), + // in most cases, using a connection is the easiest solution. + $yourEnvelope = $this->connection->get(); + if (null === $yourEnvelope) { + return []; + } + + try { + $envelope = $this->serializer->decode([ + 'body' => $yourEnvelope['body'], + 'headers' => $yourEnvelope['headers'], + ]); + } catch (MessageDecodingFailedException $exception) { + $this->connection->reject($yourEnvelope['id']); + throw $exception; + } + + return [$envelope->with(new CustomStamp($yourEnvelope['id']))]; + } + + public function ack(Envelope $envelope): void + { + // Add information about the handled message + } + + public function reject(Envelope $envelope): void + { + // In the case of a custom connection + $id = /* get the message id thanks to information or stamps present in the envelope */; + + $this->connection->reject($id); + } + } + +Receiver and Sender on the same Bus +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To allow sending and receiving messages on the same bus and prevent an infinite +loop, the message bus will add a :class:`Symfony\\Component\\Messenger\\Stamp\\ReceivedStamp` +stamp to the message envelopes and the :class:`Symfony\\Component\\Messenger\\Middleware\\SendMessageMiddleware` +middleware will know it should not route these messages again to a transport. + +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /messenger + /messenger/* + +.. _`blog posts about command buses`: https://matthiasnoback.nl/tags/command%20bus/ +.. _`SimpleBus project`: https://docs.simplebus.io/en/latest/ diff --git a/components/mime.rst b/components/mime.rst new file mode 100644 index 00000000000..c043b342ebc --- /dev/null +++ b/components/mime.rst @@ -0,0 +1,298 @@ +The Mime Component +================== + + The Mime component allows manipulating the MIME messages used to send emails + and provides utilities related to MIME types. + +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 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

...

') + ; + +The only purpose of this component is to create the email messages. Use the +:doc:`Mailer component ` to actually send them. + +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-extra + +Now, enable the extension:: + + // ... + use Twig\Extra\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

...

', null, '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 + ), null, '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'] + + $types = $mimeTypes->getMimeTypes('js'); + // $types = ['application/javascript', 'application/x-javascript', 'text/javascript'] + $types = $mimeTypes->getMimeTypes('apk'); + // $types = ['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 add 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 type guessers must be :ref:`registered as services ` +and :doc:`tagged ` with the ``mime.mime_type_guesser`` tag. +If you're using the +:ref:`default services.yaml configuration `, +this is already done for you, thanks to :ref:`autoconfiguration `. + +.. _`MIME`: https://en.wikipedia.org/wiki/MIME +.. _`MIME types`: https://en.wikipedia.org/wiki/Media_type +.. _`fileinfo extension`: https://www.php.net/fileinfo diff --git a/components/options_resolver.rst b/components/options_resolver.rst index edee6f833e7..6f3a6751f28 100644 --- a/components/options_resolver.rst +++ b/components/options_resolver.rst @@ -1,300 +1,994 @@ -.. index:: - single: Options Resolver - single: Components; OptionsResolver - The OptionsResolver Component ============================= - The OptionsResolver Component helps you configure objects with option - arrays. It supports default values, option constraints and lazy options. + The OptionsResolver component is an improved replacement for the + :phpfunction:`array_replace` PHP function. It allows you to create an + options system with required options, defaults, validation (type, value), + normalization and more. Installation ------------ -You can install the component in several different ways: +.. code-block:: terminal + + $ composer require symfony/options-resolver -* Use the official Git repository (https://github.com/symfony/OptionsResolver -* :doc:`Install it via Composer` (``symfony/options-resolver`` on `Packagist`_) +.. include:: /components/require_autoload.rst.inc Usage ----- -Imagine you have a ``Person`` class which has 2 options: ``firstName`` and -``lastName``. These options are going to be handled by the OptionsResolver -Component. +Imagine you have a ``Mailer`` class which has four options: ``host``, +``username``, ``password`` and ``port``:: -First, create the ``Person`` class:: + class Mailer + { + protected array $options; - class Person + public function __construct(array $options = []) + { + $this->options = $options; + } + } + +When accessing the ``$options``, you need to add some boilerplate code to +check which options are set:: + + class Mailer { - protected $options; + // ... + public function sendMail($from, $to): void + { + $mail = ...; + + $mail->setHost($this->options['host'] ?? 'smtp.example.org'); + $mail->setUsername($this->options['username'] ?? 'user'); + $mail->setPassword($this->options['password'] ?? 'pa$$word'); + $mail->setPort($this->options['port'] ?? 25); + + // ... + } + } + +Also, the default values of the options are buried in the business logic of your +code. Use :phpfunction:`array_replace` to fix that:: - public function __construct(array $options = array()) + class Mailer + { + // ... + + public function __construct(array $options = []) { + $this->options = array_replace([ + 'host' => 'smtp.example.org', + 'username' => 'user', + 'password' => 'pa$$word', + 'port' => 25, + ], $options); } } -You could of course set the ``$options`` value directly on the property. Instead, -use the :class:`Symfony\\Component\\OptionsResolver\\OptionsResolver` class -and let it resolve the options by calling -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::resolve`. -The advantages of doing this will become more obvious as you continue:: +Now all four options are guaranteed to be set, but you could still make an error +like the following when using the ``Mailer`` class:: + + $mailer = new Mailer([ + 'usernme' => 'johndoe', // 'username' is wrongly spelled as 'usernme' + ]); + +No error will be shown. In the best case, the bug will appear during testing, +but the developer will spend time looking for the problem. In the worst case, +the bug might not appear until it's deployed to the live system. + +Fortunately, the :class:`Symfony\\Component\\OptionsResolver\\OptionsResolver` +class helps you to fix this problem:: use Symfony\Component\OptionsResolver\OptionsResolver; - // ... - public function __construct(array $options = array()) + class Mailer { - $resolver = new OptionsResolver(); + // ... + + public function __construct(array $options = []) + { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'host' => 'smtp.example.org', + 'username' => 'user', + 'password' => 'pa$$word', + 'port' => 25, + ]); - $this->options = $resolver->resolve($options); + $this->options = $resolver->resolve($options); + } } -The ``$options`` property is an instance of -:class:`Symfony\\Component\\OptionsResolver\\Options`, which implements -:phpclass:`ArrayAccess`, :phpclass:`Iterator` and :phpclass:`Countable`. That -means you can handle it just like a normal array:: +Like before, all options will be guaranteed to be set. Additionally, an +:class:`Symfony\\Component\\OptionsResolver\\Exception\\UndefinedOptionsException` +is thrown if an unknown option is passed:: + + $mailer = new Mailer([ + 'usernme' => 'johndoe', + ]); + + // UndefinedOptionsException: The option "usernme" does not exist. + // Defined options are: "host", "password", "port", "username" + +The rest of your code can access the values of the options without boilerplate +code:: // ... - public function getFirstName() + class Mailer { - return $this->options['firstName']; + // ... + + public function sendMail($from, $to): void + { + $mail = ...; + $mail->setHost($this->options['host']); + $mail->setUsername($this->options['username']); + $mail->setPassword($this->options['password']); + $mail->setPort($this->options['port']); + // ... + } } - public function getFullName() +It's a good practice to split the option configuration into a separate method:: + + // ... + class Mailer { - $name = $this->options['firstName']; + // ... - if (isset($this->options['lastName'])) { - $name .= ' '.$this->options['lastName']; + public function __construct(array $options = []) + { + $resolver = new OptionsResolver(); + $this->configureOptions($resolver); + + $this->options = $resolver->resolve($options); } - return $name; + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'host' => 'smtp.example.org', + 'username' => 'user', + 'password' => 'pa$$word', + 'port' => 25, + 'encryption' => null, + ]); + } } -Now, try to actually use the class:: +First, your code becomes easier to read, especially if the constructor does more +than processing options. Second, sub-classes may now override the +``configureOptions()`` method to adjust the configuration of the options:: - $person = new Person(array( - 'firstName' => 'Wouter', - 'lastName' => 'de Jong', - )); + // ... + class GoogleMailer extends Mailer + { + public function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); - echo $person->getFirstName(); + $resolver->setDefaults([ + 'host' => 'smtp.google.com', + 'encryption' => 'ssl', + ]); + } + } -Right now, you'll receive a -:class:`Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException`, -which tells you that the options ``firstName`` and ``lastName`` do not exist. -This is because you need to configure the ``OptionsResolver`` first, so it -knows which options should be resolved. +Required Options +~~~~~~~~~~~~~~~~ -.. tip:: +If an option must be set by the caller, pass that option to +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setRequired`. +For example, to make the ``host`` option required, you can do:: - To check if an option exists, you can use the - :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isKnown` - function. + // ... + class Mailer + { + // ... -A best practice is to put the configuration in a method (e.g. -``setDefaultOptions``). You call this method in the constructor to configure -the ``OptionsResolver`` class:: + public function configureOptions(OptionsResolver $resolver): void + { + // ... + $resolver->setRequired('host'); + } + } - use Symfony\Component\OptionsResolver\OptionsResolver; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; +If you omit a required option, a +:class:`Symfony\\Component\\OptionsResolver\\Exception\\MissingOptionsException` +will be thrown:: + + $mailer = new Mailer(); + + // MissingOptionsException: The required option "host" is missing. + +The :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setRequired` +method accepts a single name or an array of option names if you have more than +one required option:: - class Person + // ... + class Mailer { - protected $options; + // ... - public function __construct(array $options = array()) + public function configureOptions(OptionsResolver $resolver): void { - $resolver = new OptionsResolver(); - $this->setDefaultOptions($resolver); + // ... + $resolver->setRequired(['host', 'username', 'password']); + } + } - $this->options = $resolver->resolve($options); +Use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isRequired` to find +out if an option is required. You can use +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::getRequiredOptions` to +retrieve the names of all required options:: + + // ... + class GoogleMailer extends Mailer + { + public function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + + if ($resolver->isRequired('host')) { + // ... + } + + $requiredOptions = $resolver->getRequiredOptions(); } + } - protected function setDefaultOptions(OptionsResolverInterface $resolver) +If you want to check whether a required option is still missing from the default +options, you can use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isMissing`. +The difference between this and :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isRequired` +is that this method will return false if a required option has already +been set:: + + // ... + class Mailer + { + // ... + + public function configureOptions(OptionsResolver $resolver): void { - // ... configure the resolver, you will learn this in the sections below + // ... + $resolver->setRequired('host'); } } -Required Options ----------------- + // ... + class GoogleMailer extends Mailer + { + public function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + + $resolver->isRequired('host'); + // => true + + $resolver->isMissing('host'); + // => true + + $resolver->setDefault('host', 'smtp.google.com'); + + $resolver->isRequired('host'); + // => true + + $resolver->isMissing('host'); + // => false + } + } -Suppose the ``firstName`` option is required: the class can't work without -it. You can set the required options by calling -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setRequired`:: +The :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::getMissingOptions` method +lets you access the names of all missing options. + +Type Validation +~~~~~~~~~~~~~~~ + +You can run additional checks on the options to make sure they were passed +correctly. To validate the types of the options, call +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setAllowedTypes`:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { - $resolver->setRequired(array('firstName')); + // ... + + public function configureOptions(OptionsResolver $resolver): void + { + // ... + + // specify one allowed type + $resolver->setAllowedTypes('host', 'string'); + + // specify multiple allowed types + $resolver->setAllowedTypes('port', ['null', 'int']); + // if you prefer, you can also use the following equivalent syntax + $resolver->setAllowedTypes('port', 'int|null'); + + // check all items in an array recursively for a type + $resolver->setAllowedTypes('dates', 'DateTime[]'); + $resolver->setAllowedTypes('ports', 'int[]'); + // the following syntax means "an array of integers or an array of strings" + $resolver->setAllowedTypes('endpoints', '(int|string)[]'); + } } -You are now able to use the class without errors:: +.. versionadded:: 7.3 - $person = new Person(array( - 'firstName' => 'Wouter', - )); + Defining type unions with the ``|`` syntax was introduced in Symfony 7.3. - echo $person->getFirstName(); // 'Wouter' +You can pass any type for which an ``is_()`` function is defined in PHP. +You may also pass fully qualified class or interface names (which is checked +using ``instanceof``). Additionally, you can validate all items in an array +recursively by suffixing the type with ``[]``. -If you don't pass a required option, a -:class:`Symfony\\Component\\OptionsResolver\\Exception\\MissingOptionsException` -will be thrown. +If you pass an invalid option now, an +:class:`Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException` +is thrown:: + + $mailer = new Mailer([ + 'host' => 25, + ]); -To determine if an option is required, you can use the -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isRequired` -method. + // InvalidOptionsException: The option "host" with value "25" is + // expected to be of type "string", but is of type "int" -Optional Options ----------------- +In sub-classes, you can use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addAllowedTypes` +to add additional allowed types without erasing the ones already set. -Sometimes, an option can be optional (e.g. the ``lastName`` option in the -``Person`` class). You can configure these options by calling -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setOptional`:: +.. _optionsresolver-validate-value: + +Value Validation +~~~~~~~~~~~~~~~~ + +Some options can only take one of a fixed list of predefined values. For +example, suppose the ``Mailer`` class has a ``transport`` option which can be +one of ``sendmail``, ``mail`` and ``smtp``. Use the method +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setAllowedValues` +to verify that the passed option contains one of these values:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { // ... - $resolver->setOptional(array('lastName')); + public function configureOptions(OptionsResolver $resolver): void + { + // ... + $resolver->setDefault('transport', 'sendmail'); + $resolver->setAllowedValues('transport', ['sendmail', 'mail', 'smtp']); + } } -Set Default Values ------------------- +If you pass an invalid transport, an +:class:`Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException` +is thrown:: + + $mailer = new Mailer([ + 'transport' => 'send-mail', + ]); + + // InvalidOptionsException: The option "transport" with value "send-mail" + // is invalid. Accepted values are: "sendmail", "mail", "smtp" + +For options with more complicated validation schemes, pass a closure which +returns ``true`` for acceptable values and ``false`` for invalid values:: + + // ... + $resolver->setAllowedValues('transport', function (string $value): bool { + // return true or false + }); + +.. tip:: + + You can even use the :doc:`Validator ` component to validate the + input by using the :method:`Symfony\\Component\\Validator\\Validation::createIsValidCallable` + method:: -Most of the optional options have a default value. You can configure these -options by calling -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setDefaults`:: + use Symfony\Component\OptionsResolver\OptionsResolver; + use Symfony\Component\Validator\Constraints\Length; + use Symfony\Component\Validator\Validation; + + // ... + $resolver->setAllowedValues('transport', Validation::createIsValidCallable( + new Length(min: 10) + )); + +In sub-classes, you can use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addAllowedValues` +to add additional allowed values without erasing the ones already set. + +Option Normalization +~~~~~~~~~~~~~~~~~~~~ + +Sometimes, option values need to be normalized before you can use them. For +instance, assume that the ``host`` should always start with ``http://``. To do +that, you can write normalizers. Normalizers are executed after validating an +option. You can configure a normalizer by calling +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setNormalizer`:: + + use Symfony\Component\OptionsResolver\Options; // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { // ... - $resolver->setDefaults(array( - 'age' => 0, - )); + public function configureOptions(OptionsResolver $resolver): void + { + // ... + + $resolver->setNormalizer('host', function (Options $options, string $value): string { + if (!str_starts_with($value, 'http://')) { + $value = 'http://'.$value; + } + + return $value; + }); + } } -The default age will be ``0`` now. When the user specifies an age, it gets -replaced. You don't need to configure ``age`` as an optional option. The -``OptionsResolver`` already knows that options with a default value are -optional. +The normalizer receives the actual ``$value`` and returns the normalized form. +You see that the closure also takes an ``$options`` parameter. This is useful +if you need to use other options during normalization:: -The ``OptionsResolver`` component also has an -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::replaceDefaults` -method. This can be used to override the previous default value. The closure -that is passed has 2 parameters: + // ... + class Mailer + { + // ... + public function configureOptions(OptionsResolver $resolver): void + { + // ... + $resolver->setNormalizer('host', function (Options $options, string $value): string { + if (!str_starts_with($value, 'http://') && !str_starts_with($value, 'https://')) { + if ('ssl' === $options['encryption']) { + $value = 'https://'.$value; + } else { + $value = 'http://'.$value; + } + } -* ``$options`` (an :class:`Symfony\\Component\\OptionsResolver\\Options` - instance), with all the default options -* ``$value``, the previous set default value + return $value; + }); + } + } -Default Values that depend on another Option +To normalize a new allowed value in subclasses that are being normalized +in parent classes, use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addNormalizer` method. +This way, the ``$value`` argument will receive the previously normalized +value, otherwise you can prepend the new normalizer by passing ``true`` as +third argument. + +Default Values that Depend on another Option ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Suppose you add a ``gender`` option to the ``Person`` class, whose default -value you guess based on the first name. You can do that easily by using a -Closure as the default value:: +Suppose you want to set the default value of the ``port`` option based on the +encryption chosen by the user of the ``Mailer`` class. More precisely, you want +to set the port to ``465`` if SSL is used and to ``25`` otherwise. + +You can implement this feature by passing a closure as the default value of +the ``port`` option. The closure receives the options as arguments. Based on +these options, you can return the desired default value:: use Symfony\Component\OptionsResolver\Options; // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { // ... + public function configureOptions(OptionsResolver $resolver): void + { + // ... + $resolver->setDefault('encryption', null); - $resolver->setDefaults(array( - 'gender' => function (Options $options) { - if (GenderGuesser::isMale($options['firstName'])) { - return 'male'; + $resolver->setDefault('port', function (Options $options): int { + if ('ssl' === $options['encryption']) { + return 465; } - - return 'female'; - }, - )); + + return 25; + }); + } } -.. caution:: +.. warning:: - The first argument of the Closure must be typehinted as ``Options``, - otherwise it is considered as the value. + The argument of the callable must be type hinted as ``Options``. Otherwise, + the callable itself is considered as the default value of the option. -Configure allowed Values ------------------------- +.. note:: -Not all values are valid values for options. For instance, the ``gender`` -option can only be ``female`` or ``male``. You can configure these allowed -values by calling -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setAllowedValues`:: + The closure is only executed if the ``port`` option isn't set by the user + or overwritten in a subclass. + +A previously set default value can be accessed by adding a second argument to +the closure:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { // ... + public function configureOptions(OptionsResolver $resolver): void + { + // ... + $resolver->setDefaults([ + 'encryption' => null, + 'host' => 'example.org', + ]); + } + } - $resolver->setAllowedValues(array( - 'gender' => array('male', 'female'), - )); + class GoogleMailer extends Mailer + { + public function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + + $resolver->setDefault('host', function (Options $options, string $previousValue): string { + if ('ssl' === $options['encryption']) { + return 'secure.example.org'; + } + + // Take default value configured in the base class + return $previousValue; + }); + } } -There is also an -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addAllowedValues` -method, which you can use if you want to add an allowed value to the previous -set allowed values. +As seen in the example, this feature is mostly useful if you want to reuse the +default values set in parent classes in sub-classes. -Configure allowed Types -~~~~~~~~~~~~~~~~~~~~~~~ +Options without Default Values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can also specify allowed types. For instance, the ``firstName`` option can -be anything, but it must be a string. You can configure these types by calling -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setAllowedTypes`:: +In some cases, it is useful to define an option without setting a default value. +This is useful if you need to know whether or not the user *actually* set +an option or not. For example, if you set the default value for an option, +it's not possible to know whether the user passed this value or if it comes +from the default:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { // ... + public function configureOptions(OptionsResolver $resolver): void + { + // ... + $resolver->setDefault('port', 25); + } - $resolver->setAllowedTypes(array( - 'firstName' => 'string', - )); + // ... + public function sendMail(string $from, string $to): void + { + // Is this the default value or did the caller of the class really + // set the port to 25? + if (25 === $this->options['port']) { + // ... + } + } } -Possible types are the one associated with the ``is_*`` php functions or a -class name. You can also pass an array of types as the value. For instance, -``array('null', 'string')`` allows ``firstName`` to be ``null`` or a -``string``. +You can use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setDefined` +to define an option without setting a default value. Then the option will only +be included in the resolved options if it was actually passed to +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::resolve`:: -There is also an -:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addAllowedTypes` -method, which you can use to add an allowed type to the previous allowed types. + // ... + class Mailer + { + // ... -Normalize the Options ---------------------- + public function configureOptions(OptionsResolver $resolver): void + { + // ... + $resolver->setDefined('port'); + } + + // ... + public function sendMail(string $from, string $to): void + { + if (array_key_exists('port', $this->options)) { + echo 'Set!'; + } else { + echo 'Not Set!'; + } + } + } + + $mailer = new Mailer(); + $mailer->sendMail($from, $to); + // => Not Set! + + $mailer = new Mailer([ + 'port' => 25, + ]); + $mailer->sendMail($from, $to); + // => Set! -Some values need to be normalized before you can use them. For instance, the -``firstName`` should always start with an uppercase letter. To do that, you can -write normalizers. These Closures will be executed after all options are -passed and return the normalized value. You can configure these normalizers by -calling -:method:`Symfony\\Components\\OptionsResolver\\OptionsResolver::setNormalizers`:: +You can also pass an array of option names if you want to define multiple +options in one go:: // ... - protected function setDefaultOptions(OptionsResolverInterface $resolver) + class Mailer { // ... + public function configureOptions(OptionsResolver $resolver): void + { + // ... + $resolver->setDefined(['port', 'encryption']); + } + } - $resolver->setNormalizers(array( - 'firstName' => function (Options $options, $value) { - return ucfirst($value); - }, - )); +The methods :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::isDefined` +and :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::getDefinedOptions` +let you find out which options are defined:: + + // ... + class GoogleMailer extends Mailer + { + // ... + + public function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + + if ($resolver->isDefined('host')) { + // One of the following was called: + + // $resolver->setDefault('host', ...); + // $resolver->setRequired('host'); + // $resolver->setDefined('host'); + } + + $definedOptions = $resolver->getDefinedOptions(); + } + } + +Nested Options +~~~~~~~~~~~~~~ + +Suppose you have an option named ``spool`` which has two sub-options ``type`` +and ``path``. Instead of defining it as a simple array of values, you can pass a +closure as the default value of the ``spool`` option with a +:class:`Symfony\\Component\\OptionsResolver\\OptionsResolver` argument. Based on +this instance, you can define the options under ``spool`` and its desired +default value:: + + class Mailer + { + // ... + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setOptions('spool', function (OptionsResolver $spoolResolver): void { + $spoolResolver->setDefaults([ + 'type' => 'file', + 'path' => '/path/to/spool', + ]); + $spoolResolver->setAllowedValues('type', ['file', 'memory']); + $spoolResolver->setAllowedTypes('path', 'string'); + }); + } + + public function sendMail(string $from, string $to): void + { + if ('memory' === $this->options['spool']['type']) { + // ... + } + } } -You see that the closure also get an ``$options`` parameter. Sometimes, you -need to use the other options for normalizing. + $mailer = new Mailer([ + 'spool' => [ + 'type' => 'memory', + ], + ]); + +.. deprecated:: 7.3 + + Defining nested options via :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setDefault` + is deprecated since Symfony 7.3. Use the :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setOptions` + method instead, which also allows defining default values for prototyped options. + +.. versionadded:: 7.3 + + The ``setOptions()`` method was introduced in Symfony 7.3. + +Nested options also support required options, validation (type, value) and +normalization of their values. If the default value of a nested option depends +on another option defined in the parent level, add a second ``Options`` argument +to the closure to access to them:: + + class Mailer + { + // ... + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefault('sandbox', false); + $resolver->setOptions('spool', function (OptionsResolver $spoolResolver, Options $parent): void { + $spoolResolver->setDefaults([ + 'type' => $parent['sandbox'] ? 'memory' : 'file', + // ... + ]); + }); + } + } + +.. warning:: + + The arguments of the closure must be type hinted as ``OptionsResolver`` and + ``Options`` respectively. Otherwise, the closure itself is considered as the + default value of the option. + +In same way, parent options can access to the nested options as normal arrays:: + + class Mailer + { + // ... + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setOptions('spool', function (OptionsResolver $spoolResolver): void { + $spoolResolver->setDefaults([ + 'type' => 'file', + // ... + ]); + }); + $resolver->setOptions('profiling', function (Options $options): void { + return 'file' === $options['spool']['type']; + }); + } + } + +.. note:: + + The fact that an option is defined as nested means that you must pass + an array of values to resolve it at runtime. + +Prototype Options +~~~~~~~~~~~~~~~~~ + +There are situations where you will have to resolve and validate a set of +options that may repeat many times within another option. Let's imagine a +``connections`` option that will accept an array of database connections +with ``host``, ``database``, ``user`` and ``password`` each. + +The best way to implement this is to define the ``connections`` option as prototype:: + + $resolver->setOptions('connections', function (OptionsResolver $connResolver): void { + $connResolver + ->setPrototype(true) + ->setRequired(['host', 'database']) + ->setDefaults(['user' => 'root', 'password' => null]); + }); + +According to the prototype definition in the example above, it is possible +to have multiple connection arrays like the following:: + + $resolver->resolve([ + 'connections' => [ + 'default' => [ + 'host' => '127.0.0.1', + 'database' => 'symfony', + ], + 'test' => [ + 'host' => '127.0.0.1', + 'database' => 'symfony_test', + 'user' => 'test', + 'password' => 'test', + ], + // ... + ], + ]); + +The array keys (``default``, ``test``, etc.) of this prototype option are +validation-free and can be any arbitrary value that helps differentiate the +connections. + +.. note:: + + A prototype option can only be defined inside a nested option and + during its resolution it will expect an array of arrays. + +Deprecating the Option +~~~~~~~~~~~~~~~~~~~~~~ + +Once an option is outdated or you decided not to maintain it anymore, you can +deprecate it using the :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setDeprecated` +method:: + + $resolver + ->setDefined(['hostname', 'host']) + + // this outputs the following generic deprecation message: + // Since acme/package 1.2: The option "hostname" is deprecated. + ->setDeprecated('hostname', 'acme/package', '1.2') + + // you can also pass a custom deprecation message (%name% placeholder is available) + // %name% placeholder will be replaced by the deprecated option. + // This outputs the following deprecation message: + // Since acme/package 1.2: The option "hostname" is deprecated, use "host" instead. + ->setDeprecated( + 'hostname', + 'acme/package', + '1.2', + 'The option "%name%" is deprecated, use "host" instead.' + ) + ; + +.. note:: + + The deprecation message will be triggered only if the option is being used + somewhere, either its value is provided by the user or the option is evaluated + within closures of lazy options and normalizers. + +.. note:: + + When using an option deprecated by you in your own library, you can pass + ``false`` as the second argument of the + :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::offsetGet` method + to not trigger the deprecation warning. + +.. note:: + + All deprecation messages are displayed in the profiler logs in the "Deprecations" tab. + +Instead of passing the message, you may also pass a closure which returns +a string (the deprecation message) or an empty string to ignore the deprecation. +This closure is useful to only deprecate some of the allowed types or values of +the option:: + + $resolver + ->setDefault('encryption', null) + ->setDefault('port', null) + ->setAllowedTypes('port', ['null', 'int']) + ->setDeprecated('port', 'acme/package', '1.2', function (Options $options, ?int $value): string { + if (null === $value) { + return 'Passing "null" to option "port" is deprecated, pass an integer instead.'; + } + + // deprecation may also depend on another option + if ('ssl' === $options['encryption'] && 456 !== $value) { + return 'Passing a different port than "456" when the "encryption" option is set to "ssl" is deprecated.'; + } + + return ''; + }) + ; + +.. note:: + + Deprecation based on the value is triggered only when the option is provided + by the user. + +This closure receives as argument the value of the option after validating it +and before normalizing it when the option is being resolved. + +Ignore not defined Options +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, all options are resolved and validated, resulting in a +:class:`Symfony\\Component\\OptionsResolver\\Exception\\UndefinedOptionsException` +if an unknown option is passed. You can ignore not defined options by using the +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::ignoreUndefined` method:: + + // ... + $resolver + ->setDefined(['hostname']) + ->setIgnoreUndefined(true) + ; + + // option "version" will be ignored + $resolver->resolve([ + 'hostname' => 'acme/package', + 'version' => '1.2.3' + ]); + +Chaining Option Configurations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In many cases you may need to define multiple configurations for each option. +For example, suppose the ``InvoiceMailer`` class has an ``host`` option that is required +and a ``transport`` option which can be one of ``sendmail``, ``mail`` and ``smtp``. +You can improve the readability of the code avoiding to duplicate option name for +each configuration using the :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::define` +method:: + + // ... + class InvoiceMailer + { + // ... + public function configureOptions(OptionsResolver $resolver): void + { + // ... + $resolver->define('host') + ->required() + ->default('smtp.example.org') + ->allowedTypes('string') + ->info('The IP address or hostname'); + + $resolver->define('transport') + ->required() + ->default('transport') + ->allowedValues('sendmail', 'mail', 'smtp'); + } + } + +Performance Tweaks +~~~~~~~~~~~~~~~~~~ + +With the current implementation, the ``configureOptions()`` method will be +called for every single instance of the ``Mailer`` class. Depending on the +amount of option configuration and the number of created instances, this may add +noticeable overhead to your application. If that overhead becomes a problem, you +can change your code to do the configuration only once per class:: + + // ... + class Mailer + { + private static array $resolversByClass = []; + + protected array $options; + + public function __construct(array $options = []) + { + // What type of Mailer is this, a Mailer, a GoogleMailer, ... ? + $class = get_class($this); + + // Was configureOptions() executed before for this class? + if (!isset(self::$resolversByClass[$class])) { + self::$resolversByClass[$class] = new OptionsResolver(); + $this->configureOptions(self::$resolversByClass[$class]); + } + + $this->options = self::$resolversByClass[$class]->resolve($options); + } + + public function configureOptions(OptionsResolver $resolver): void + { + // ... + } + } + +Now the :class:`Symfony\\Component\\OptionsResolver\\OptionsResolver` instance +will be created once per class and reused from that on. Be aware that this may +lead to memory leaks in long-running applications, if the default options contain +references to objects or object graphs. If that's the case for you, implement a +method ``clearOptionsConfig()`` and call it periodically:: + + // ... + class Mailer + { + private static array $resolversByClass = []; + + public static function clearOptionsConfig(): void + { + self::$resolversByClass = []; + } + + // ... + } + +That's it! You now have all the tools and knowledge needed to process +options in your code. + +Getting More Insights +~~~~~~~~~~~~~~~~~~~~~ + +Use the ``OptionsResolverIntrospector`` to inspect the options definitions +inside an ``OptionsResolver`` instance:: + + use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector; + use Symfony\Component\OptionsResolver\OptionsResolver; + + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'host' => 'smtp.example.org', + 'port' => 25, + ]); -.. _Packagist: https://packagist.org/packages/symfony/options-resolver + $introspector = new OptionsResolverIntrospector($resolver); + $introspector->getDefault('host'); // Retrieves "smtp.example.org" diff --git a/components/phpunit_bridge.rst b/components/phpunit_bridge.rst new file mode 100644 index 00000000000..5ce4c003a11 --- /dev/null +++ b/components/phpunit_bridge.rst @@ -0,0 +1,1087 @@ +The PHPUnit Bridge +================== + + The PHPUnit Bridge provides utilities to report legacy tests and usage of + deprecated code and helpers for mocking native functions related to time, + DNS and class existence. + +It comes with the following features: + +* Sets by default a consistent locale (``C``) for your tests (if you + create locale-sensitive tests, use PHPUnit's ``setLocale()`` method); + +* Auto-register ``class_exists`` to load Doctrine annotations (when used); + +* It displays the whole list of deprecated features used in the application; + +* 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; + +* Provides a modified version of PHPUnit that allows: + + #. separating the dependencies of your app from those of phpunit to prevent any unwanted constraints to apply; + #. running tests in parallel when a test suite is split in several phpunit.xml files; + #. recording and replaying skipped tests; + +* It allows to create tests that are compatible with multiple PHPUnit versions + (because it provides polyfills for missing methods, namespaced aliases for + non-namespaced classes, etc.). + +Installation +------------ + +.. code-block:: terminal + + $ composer require --dev symfony/phpunit-bridge + +.. include:: /components/require_autoload.rst.inc + +.. note:: + + The PHPUnit bridge is designed to work with all maintained versions of + Symfony components, even across different major versions of them. You should + always use its very latest stable major version to get the most accurate + deprecation report. + +If you plan to :ref:`write assertions about deprecations ` and use the regular +PHPUnit script (not the modified PHPUnit script provided by Symfony), you have +to register a new `test listener`_ called ``SymfonyTestsListener``: + +.. code-block:: xml + + + + + + + + + + + +Usage +----- + +.. seealso:: + + This article explains how to use the PhpUnitBridge features as an independent + component in any PHP application. Read the :doc:`/testing` article to learn + about how to use it in Symfony applications. + +Once the component is installed, a ``simple-phpunit`` script is created in the +``vendor/`` directory to run tests. This script wraps the original PHPUnit binary +to provide more features: + +.. code-block:: terminal + + $ cd my-project/ + $ ./vendor/bin/simple-phpunit + +After running your PHPUnit tests, you will get a report similar to this one: + +.. code-block:: terminal + + $ ./vendor/bin/simple-phpunit + PHPUnit by Sebastian Bergmann. + + Configuration read from /phpunit.xml.dist + ................. + + Time: 1.77 seconds, Memory: 5.75Mb + + OK (17 tests, 21 assertions) + + Remaining deprecation notices (2) + + getEntityManager is deprecated since Symfony 2.1. Use getManager instead: 2x + 1x in DefaultControllerTest::testPublicUrls from App\Tests\Controller + 1x in BlogControllerTest::testIndex from App\Tests\Controller + +The summary includes: + +**Unsilenced** + Reports deprecation notices that were triggered without the recommended + `@-silencing operator`_. + +**Legacy** + Deprecation notices denote tests that explicitly test some legacy features. + +**Remaining/Other** + Deprecation notices are all other (non-legacy) notices, grouped by message, + test class and method. + +.. note:: + + If you don't want to use the ``simple-phpunit`` script, register the following + `PHPUnit event listener`_ in your PHPUnit configuration file to get the same + report about deprecations (which is created by a `PHP error handler`_ + called :class:`Symfony\\Bridge\\PhpUnit\\DeprecationErrorHandler`): + + .. code-block:: xml + + + + + + + +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 subdirectories 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 +--------------------------- + +Deprecation notices can be triggered by using ``trigger_deprecation`` from +the ``symfony/deprecation-contracts`` package:: + + // indicates something is deprecated since version 1.3 of vendor-name/packagename + trigger_deprecation('vendor-name/package-name', '1.3', 'Your deprecation message'); + + // you can also use printf format (all arguments after the message will be used) + trigger_deprecation('...', '1.3', 'Value "%s" is deprecated, use ... instead.', $value); + +Mark Tests as Legacy +-------------------- + +There are three ways to mark a test as legacy: + +* (**Recommended**) Add the ``@group legacy`` annotation to its class or method; + +* Make its class name start with the ``Legacy`` prefix; + +* Make its method name start with ``testLegacy*()`` instead of ``test*()``. + +.. note:: + + If your data provider calls code that would usually trigger a deprecation, + you can prefix its name with ``provideLegacy`` or ``getLegacy`` to silence + these deprecations. If your data provider does not execute deprecated + code, it is not required to choose a special naming just because the + test being fed by the data provider is marked as legacy. + + Also be aware that choosing one of the two legacy prefixes will not mark + tests as legacy that make use of this data provider. You still have to + mark them as legacy tests explicitly. + +Configuration +------------- + +In case you need to inspect the stack trace of a particular deprecation +triggered by your unit tests, you can set the ``SYMFONY_DEPRECATIONS_HELPER`` +`environment variable`_ to a regular expression that matches this deprecation's +message, enclosed with ``/``. For example, with: + +.. code-block:: xml + + + + + + + + + + + + +`PHPUnit`_ will stop your test suite once a deprecation notice is triggered whose +message contains the ``"foobar"`` string. + +.. _making-tests-fail: + +Making Tests Fail +~~~~~~~~~~~~~~~~~ + +By default, any non-legacy-tagged or any non-silenced (`@-silencing operator`_) +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 fail 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 a 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 +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=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 ``SYMFONY_DEPRECATIONS_HELPER=max[self]=0``, deprecations that are +triggered outside the ``vendor/`` directory will be accounted for separately, +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. | ++------------------------+-----------------------------------------------------+ + +Ignoring Deprecations +..................... + +If your application has some deprecations that you can't fix for some reasons, +you can tell Symfony to ignore them. + +You need first to create a text file where each line is a deprecation to ignore +defined as a regular expression. Lines beginning with a hash (``#``) are +considered comments: + +.. code-block:: terminal + + # This file contains patterns to be ignored while testing for use of + # deprecated code. + + %The "Symfony\\Component\\Validator\\Context\\ExecutionContextInterface::.*\(\)" method is considered internal Used by the validator engine\. (Should not be called by user\W+code\. )?It may change without further notice\. You should not extend it from "[^"]+"\.% + %The "PHPUnit\\Framework\\TestCase::addWarning\(\)" method is considered internal% + +Then, you can run the following command to use that file and ignore those deprecations: + +.. code-block:: terminal + + $ SYMFONY_DEPRECATIONS_HELPER='ignoreFile=./tests/baseline-ignore' ./vendor/bin/simple-phpunit + +Baseline Deprecations +..................... + +You can also take a snapshot of deprecations currently triggered by your application +code, and ignore those during your test runs, still reporting newly added ones. +The trick is to create a file with the allowed deprecations and define it as the +"deprecation baseline". Deprecations inside that file are ignored but the rest of +deprecations are still reported. + +First, generate the file with the allowed deprecations (run the same command +whenever you want to update the existing file): + +.. code-block:: terminal + + $ SYMFONY_DEPRECATIONS_HELPER='generateBaseline=true&baselineFile=./tests/allowed.json' ./vendor/bin/simple-phpunit + +This command stores all the deprecations reported while running tests in the +given file path and encoded in JSON. + +Then, you can run the following command to use that file and ignore those deprecations: + +.. code-block:: terminal + + $ SYMFONY_DEPRECATIONS_HELPER='baselineFile=./tests/allowed.json' ./vendor/bin/simple-phpunit + +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. + +It's also possible to change verbosity per deprecation type. For example, using +``quiet[]=indirect&quiet[]=other`` will hide details for deprecations of types +"indirect" and "other". + +The ``quiet`` option hides details for the specified deprecation types, but will +not change the outcome in terms of exit code. That's what :ref:`max ` +is for, and both settings are orthogonal. + +Disabling the Deprecation Helper +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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. + +Deprecation Notices at Autoloading Time +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the PHPUnit Bridge uses ``DebugClassLoader`` from the +`ErrorHandler component`_ to throw deprecation notices at class autoloading +time. This can be disabled with the ``debug-class-loader`` option. + +.. code-block:: xml + + + + + + + + + 0 + + + + + +Compile-time Deprecations +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use the ``debug:container`` command to list the deprecations generated during +the compiling and warming up of the container: + +.. code-block:: terminal + + $ php bin/console debug:container --deprecations + +Log Deprecations +~~~~~~~~~~~~~~~~ + +For turning the verbose output off and write it to a log file instead you can use +``SYMFONY_DEPRECATIONS_HELPER='logFile=/path/deprecations.log'``. + +Setting The Locale For Tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the PHPUnit Bridge forces the locale to ``C`` to avoid locale +issues in tests. This behavior can be changed by setting the +``SYMFONY_PHPUNIT_LOCALE`` environment variable to the desired locale: + +.. code-block:: bash + + # .env.test + SYMFONY_PHPUNIT_LOCALE="fr_FR" + +Alternatively, you can set this environment variable in the PHPUnit +configuration file: + +.. code-block:: xml + + + + + + + + + + + + +Finally, if you want to avoid the bridge to force any locale, you can set the +``SYMFONY_PHPUNIT_LOCALE`` environment variable to ``0``. + +.. _write-assertions-about-deprecations: + +Write Assertions about Deprecations +----------------------------------- + +When adding deprecations to your code, you might like writing tests that verify +that they are triggered as required. To do so, the bridge provides the +``expectDeprecation()`` method that you can use on your test methods. +It requires you to pass the expected message, given in the same format as for +the `PHPUnit's assertStringMatchesFormat()`_ method. If you expect more than one +deprecation message for a given test method, you can use the method several +times (order matters):: + + use PHPUnit\Framework\TestCase; + use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; + + class MyTest extends TestCase + { + use ExpectDeprecationTrait; + + /** + * @group legacy + */ + public function testDeprecatedCode(): void + { + // test some code that triggers the following deprecation: + // trigger_deprecation('vendor-name/package-name', '5.1', 'This "Foo" method is deprecated.'); + $this->expectDeprecation('Since vendor-name/package-name 5.1: This "%s" method is deprecated'); + + // ... + + // test some code that triggers the following deprecation: + // trigger_deprecation('vendor-name/package-name', '4.4', 'The second argument of the "Bar" method is deprecated.'); + $this->expectDeprecation('Since vendor-name/package-name 4.4: The second argument of the "%s" method is deprecated.'); + } + } + +Display the Full Stack Trace +---------------------------- + +By default, the PHPUnit Bridge displays only deprecation messages. +To show the full stack trace related to a deprecation, set the value of ``SYMFONY_DEPRECATIONS_HELPER`` +to a regular expression matching the deprecation message. + +For example, if the following deprecation notice is thrown: + +.. code-block:: bash + + 1x: Doctrine\Common\ClassLoader is deprecated. + 1x in EntityTypeTest::setUp from Symfony\Bridge\Doctrine\Tests\Form\Type + +Running the following command will display the full stack trace: + +.. code-block:: terminal + + $ SYMFONY_DEPRECATIONS_HELPER='/Doctrine\\Common\\ClassLoader is deprecated\./' ./vendor/bin/simple-phpunit + +Testing with Multiple PHPUnit Versions +-------------------------------------- + +When testing a library that has to be compatible with several versions of PHP, +the test suite cannot use the latest versions of PHPUnit because: + +* PHPUnit 8 deprecated several methods in favor of other methods which are not + available in older versions (e.g. PHPUnit 4); +* PHPUnit 8 added the ``void`` return type to the ``setUp()`` method, which is + not compatible with PHP 5.5; +* PHPUnit switched to namespaced classes starting from PHPUnit 6, so tests must + work with and without namespaces. + +Polyfills for the Unavailable Methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using the ``simple-phpunit`` script, PHPUnit Bridge injects polyfills for +most methods of the ``TestCase`` and ``Assert`` classes (e.g. ``expectException()``, +``expectExceptionMessage()``, ``assertContainsEquals()``, etc.). This allows writing +test cases using the latest best practices while still remaining compatible with +older PHPUnit versions. + +Removing the Void Return Type +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When running the ``simple-phpunit`` script with the ``SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT`` +environment variable set to ``1``, the PHPUnit bridge will alter the code of +PHPUnit to remove the return type (introduced in PHPUnit 8) from ``setUp()``, +``tearDown()``, ``setUpBeforeClass()`` and ``tearDownAfterClass()`` methods. +This allows you to write a test compatible with both PHP 5 and PHPUnit 8. + +Using Namespaced PHPUnit Classes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The PHPUnit bridge adds namespaced class aliases for most of the PHPUnit classes +declared without namespaces (e.g. ``PHPUnit_Framework_Assert``), allowing you to +always use the namespaced class declaration even when the test is executed with +PHPUnit 4. + +Time-sensitive Tests +-------------------- + +Use Case +~~~~~~~~ + +If you have this kind of time-related tests:: + + use PHPUnit\Framework\TestCase; + use Symfony\Component\Stopwatch\Stopwatch; + + class MyTest extends TestCase + { + public function testSomething(): void + { + $stopwatch = new Stopwatch(); + + $stopwatch->start('event_name'); + sleep(10); + $duration = $stopwatch->stop('event_name')->getDuration(); + + $this->assertEquals(10000, $duration); + } + } + +You calculated the duration time of your process using the Stopwatch utilities to +:ref:`profile Symfony applications `. However, depending +on the load of the server or the processes running on your local machine, the +``$duration`` could for example be ``10.000023s`` instead of ``10s``. + +This kind of tests are called transient tests: they are failing randomly +depending on spurious and external circumstances. They are often cause trouble +when using public continuous integration services like `Travis CI`_. + +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()``, ``usleep()``, ``gmdate()``, and ``hrtime()``. 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. 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', (string) 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 +PHPUnit using the ``vendor/bin/simple-phpunit`` script or when registering the +following listener in your PHPUnit configuration: + +.. code-block:: xml + + + + + + + +.. note:: + + If you don't want to use the ``@group time-sensitive`` annotation, you can + register the ``ClockMock`` class manually by calling + ``ClockMock::register(__CLASS__)`` and ``ClockMock::withClockMock(true)`` + before the test and ``ClockMock::withClockMock(false)`` after the test. + +As a result, the following is guaranteed to work and is no longer a transient +test:: + + use PHPUnit\Framework\TestCase; + use Symfony\Component\Stopwatch\Stopwatch; + + /** + * @group time-sensitive + */ + class MyTest extends TestCase + { + public function testSomething(): void + { + $stopwatch = new Stopwatch(); + + $stopwatch->start('event_name'); + sleep(10); + $duration = $stopwatch->stop('event_name')->getDuration(); + + $this->assertEquals(10000, $duration); + } + } + +And that's all! + +.. warning:: + + Time-based function mocking follows the `PHP namespace resolutions rules`_ + so "fully qualified function calls" (e.g ``\time()``) cannot be mocked. + +The ``@group time-sensitive`` annotation is equivalent to calling +``ClockMock::register(MyTest::class)``. If you want to mock a function used in a +different class, do it explicitly using ``ClockMock::register(MyClass::class)``:: + + // the class that uses the time() function to be mocked + namespace App; + + class MyClass + { + public function getTimeInHours(): void + { + return time() / 3600; + } + } + + // the test that mocks the external time() function explicitly + namespace App\Tests; + + use App\MyClass; + use PHPUnit\Framework\TestCase; + use Symfony\Bridge\PhpUnit\ClockMock; + + /** + * @group time-sensitive + */ + class MyTest extends TestCase + { + public function testGetTimeInHours(): void + { + ClockMock::register(MyClass::class); + + $my = new MyClass(); + $result = $my->getTimeInHours(); + + $this->assertEquals(time() / 3600, $result); + } + } + +.. tip:: + + An added bonus of using the ``ClockMock`` class is that time passes + instantly. Using PHP's ``sleep(10)`` will make your test wait for 10 + actual seconds (more or less). In contrast, the ``ClockMock`` class + advances the internal clock the given number of seconds without actually + waiting that time, so your test will execute 10 seconds faster. + +DNS-sensitive Tests +------------------- + +Tests that make network connections, for example to check the validity of a DNS +record, can be slow to execute and unreliable due to the conditions of the +network. For that reason, this component also provides mocks for these PHP +functions: + +* :phpfunction:`checkdnsrr` +* :phpfunction:`dns_check_record` +* :phpfunction:`getmxrr` +* :phpfunction:`dns_get_mx` +* :phpfunction:`gethostbyaddr` +* :phpfunction:`gethostbyname` +* :phpfunction:`gethostbynamel` +* :phpfunction:`dns_get_record` + +Use Case +~~~~~~~~ + +Consider the following example that tests a custom class called ``DomainValidator`` +which defines a ``checkDnsRecord`` option to also validate that a domain is +associated to a valid host:: + + use App\Validator\DomainValidator; + use PHPUnit\Framework\TestCase; + + class MyTest extends TestCase + { + public function testEmail(): void + { + $validator = new DomainValidator(['checkDnsRecord' => true]); + $isValid = $validator->validate('example.com'); + + // ... + } + } + +In order to avoid making a real network connection, add the ``@group dns-sensitive`` +annotation to the class and use the ``DnsMock::withMockedHosts()`` to configure +the data you expect to get for the given hosts:: + + use App\Validator\DomainValidator; + use PHPUnit\Framework\TestCase; + use Symfony\Bridge\PhpUnit\DnsMock; + + /** + * @group dns-sensitive + */ + class DomainValidatorTest extends TestCase + { + public function testEmails(): void + { + DnsMock::withMockedHosts([ + 'example.com' => [['type' => 'A', 'ip' => '1.2.3.4']], + ]); + + $validator = new DomainValidator(['checkDnsRecord' => true]); + $isValid = $validator->validate('example.com'); + + // ... + } + } + +The ``withMockedHosts()`` method configuration is defined as an array. The keys +are the mocked hosts and the values are arrays of DNS records in the same format +returned by :phpfunction:`dns_get_record`, so you can simulate diverse network +conditions:: + + DnsMock::withMockedHosts([ + 'example.com' => [ + [ + 'type' => 'A', + 'ip' => '1.2.3.4', + ], + [ + 'type' => 'AAAA', + 'ipv6' => '::12', + ], + ], + ]); + +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` +* :phpfunction:`enum_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 behavior.'; + } + + 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(): void + { + $class = new MyClass(); + $result = $class->hello(); // "The dependency behavior." + + // ... + } + } + +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(): void + { + ClassExistsMock::register(MyClass::class); + ClassExistsMock::withMockedClasses([DependencyClass::class => false]); + + $class = new MyClass(); + $result = $class->hello(); // "The default behavior." + + // ... + } + } + +Note that mocking a class with ``ClassExistsMock::withMockedClasses()`` +will make :phpfunction:`class_exists`, :phpfunction:`interface_exists` +and :phpfunction:`trait_exists` return true. + +To register an enumeration and mock :phpfunction:`enum_exists`, +``ClassExistsMock::withMockedEnums()`` must be used. Note that, like in +PHP 8.1 and later, calling ``class_exists`` on a enum will return ``true``. +That's why calling ``ClassExistsMock::withMockedEnums()`` will also register the enum +as a mocked class. + +Troubleshooting +--------------- + +The ``@group time-sensitive`` and ``@group dns-sensitive`` annotations work +"by convention" and assume that the namespace of the tested class can be +obtained just by removing the ``Tests\`` part from the test namespace. I.e. +if your test cases fully-qualified class name (FQCN) is +``App\Tests\Watch\DummyWatchTest``, it assumes the tested class namespace +is ``App\Watch``. + +If this convention doesn't work for your application, configure the mocked +namespaces in the ``phpunit.xml`` file, as done for example in the +:doc:`HttpKernel Component `: + +.. code-block:: xml + + + + + + + + + + + Symfony\Component\HttpFoundation + + + + + + +Under the hood, a PHPUnit listener injects the mocked functions in the tested +classes' namespace. In order to work as expected, the listener has to run before +the tested class ever runs. + +By default, the mocked functions are created when the annotation are found and +the corresponding tests are run. Depending on how your tests are constructed, +this might be too late. + +You can either: + +* Declare the namespaces of the tested classes in your ``phpunit.xml.dist``; +* Register the namespaces at the end of the ``config/bootstrap.php`` file. + +.. code-block:: xml + + + + + + + + Acme\MyClassTest + + + + + +:: + + // config/bootstrap.php + use Symfony\Bridge\PhpUnit\ClockMock; + + // ... + if ('test' === $_SERVER['APP_ENV']) { + ClockMock::register('Acme\\MyClassTest\\'); + } + +Modified PHPUnit script +----------------------- + +This bridge provides a modified version of PHPUnit that you can call by using +its ``bin/simple-phpunit`` command. It has the following features: + +* Works with a standalone vendor directory that doesn't conflict with yours; +* Does not embed ``prophecy`` to prevent any conflicts with its dependencies; +* Collects and replays skipped tests when the ``SYMFONY_PHPUNIT_SKIPPED_TESTS`` + env var is defined: the env var should specify a file name that will be used for + storing skipped tests on a first run, and replay them on the second run; +* Parallelizes test suites execution when given a directory as argument, scanning + this directory for ``phpunit.xml.dist`` files up to ``SYMFONY_PHPUNIT_MAX_DEPTH`` + levels (specified as an env var, defaults to ``3``); + +The script writes the modified PHPUnit it builds in a directory that can be +configured by the ``SYMFONY_PHPUNIT_DIR`` env var, or in the same directory as +the ``simple-phpunit`` if it is not provided. It's also possible to set this +env var in the ``phpunit.xml.dist`` file. + +If you have installed the bridge through Composer, you can run it by calling e.g.: + +.. code-block:: terminal + + $ vendor/bin/simple-phpunit + +.. tip:: + + It's possible to change the PHPUnit version by setting the + ``SYMFONY_PHPUNIT_VERSION`` env var in the ``phpunit.xml.dist`` file (e.g. + ````). This is the + preferred method as it can be committed to your version control repository. + + It's also possible to set ``SYMFONY_PHPUNIT_VERSION`` as a real env var + (not defined in a :ref:`dotenv file `). + + In the same way, ``SYMFONY_MAX_PHPUNIT_VERSION`` will set the maximum version + of PHPUnit to be considered. This is useful when testing a framework that does + not support the latest version(s) of PHPUnit. + +.. tip:: + + If you still need to use ``prophecy`` (but not ``symfony/yaml``), + then set the ``SYMFONY_PHPUNIT_REMOVE`` env var to ``symfony/yaml``. + + It's also possible to set this env var in the ``phpunit.xml.dist`` file. + +.. tip:: + + It is also possible to require additional packages that will be installed along + with the rest of the needed PHPUnit packages using the ``SYMFONY_PHPUNIT_REQUIRE`` + env variable. This is specially useful for installing PHPUnit plugins without + having to add them to your main ``composer.json`` file. The required packages + need to be separated with a space. + + .. code-block:: xml + + + + + + + +Code Coverage Listener +---------------------- + +By default, the code coverage is computed with the following rule: if a line of +code is executed, then it is marked as covered. The test which executes a +line of code is therefore marked as "covering the line of code". This can be +misleading. + +Consider the following example:: + + class Bar + { + public function barMethod(): string + { + return 'bar'; + } + } + + class Foo + { + public function __construct( + private Bar $bar, + ) { + } + + public function fooMethod(): string + { + $this->bar->barMethod(); + + return 'bar'; + } + } + + class FooTest extends PHPUnit\Framework\TestCase + { + public function test(): void + { + $bar = new Bar(); + $foo = new Foo($bar); + + $this->assertSame('bar', $foo->fooMethod()); + } + } + +The ``FooTest::test`` method executes every single line of code of both ``Foo`` +and ``Bar`` classes, but ``Bar`` is not truly tested. The ``CoverageListener`` +aims to fix this behavior by adding the appropriate `@covers`_ annotation on +each test class. + +If a test class already defines the ``@covers`` annotation, this listener does +nothing. Otherwise, it tries to find the code related to the test by removing +the ``Test`` part of the classname: ``My\Namespace\Tests\FooTest`` -> +``My\Namespace\Foo``. + +Installation +~~~~~~~~~~~~ + +Add the following configuration to the ``phpunit.xml.dist`` file: + +.. code-block:: xml + + + + + + + + + + + +If the logic used to find the related code is too simple or doesn't work for +your application, you can use your own SUT (System Under Test) solver: + +.. code-block:: xml + + + + + My\Namespace\SutSolver::solve + + + + +The ``My\Namespace\SutSolver::solve`` can be any PHP callable and receives the +current test as its first argument. + +Finally, the listener can also display warning messages when the SUT solver does +not find the SUT: + +.. code-block:: xml + + + + + + true + + + + +.. _`PHPUnit`: https://phpunit.de +.. _`PHPUnit event listener`: https://docs.phpunit.de/en/10.0/extending-phpunit.html#phpunit-s-event-system +.. _`ErrorHandler component`: https://github.com/symfony/error-handler +.. _`PHPUnit's assertStringMatchesFormat()`: https://docs.phpunit.de/en/9.6/assertions.html#assertstringmatchesformat +.. _`PHP error handler`: https://www.php.net/manual/en/book.errorfunc.php +.. _`environment variable`: https://docs.phpunit.de/en/9.6/configuration.html#the-env-element +.. _`@-silencing operator`: https://www.php.net/manual/en/language.operators.errorcontrol.php +.. _`Travis CI`: https://travis-ci.org/ +.. _`test listener`: https://docs.phpunit.de/en/9.6/configuration.html#the-extensions-element +.. _`@covers`: https://docs.phpunit.de/en/9.6/annotations.html#covers +.. _`PHP namespace resolutions rules`: https://www.php.net/manual/en/language.namespaces.rules.php diff --git a/components/process.rst b/components/process.rst index b109ea6d0d7..7552537e82e 100644 --- a/components/process.rst +++ b/components/process.rst @@ -1,137 +1,466 @@ -.. index:: - single: Process - single: Components; Process - The Process Component ===================== - The Process Component executes commands in sub-processes. + The Process component executes commands in sub-processes. Installation ------------ -You can install the component in many different ways: +.. code-block:: terminal + + $ composer require symfony/process -* Use the official Git repository (https://github.com/symfony/Process); -* :doc:`Install it via Composer` (``symfony/process`` on `Packagist`_). +.. include:: /components/require_autoload.rst.inc Usage ----- -The :class:`Symfony\\Component\\Process\\Process` class allows you to execute -a command in a sub-process:: +The :class:`Symfony\\Component\\Process\\Process` class executes a command in a +sub-process, taking care of the differences between operating system and +escaping arguments to prevent security issues. It replaces PHP functions like +:phpfunction:`exec`, :phpfunction:`passthru`, :phpfunction:`shell_exec` and +:phpfunction:`system`:: + use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; - $process = new Process('ls -lsa'); + $process = new Process(['ls', '-lsa']); $process->run(); // executes after the command finishes if (!$process->isSuccessful()) { - throw new \RuntimeException($process->getErrorOutput()); + throw new ProcessFailedException($process); } - print $process->getOutput(); - -The component takes care of the subtle differences between the different platforms -when executing the command. - -.. versionadded:: 2.2 - The ``getIncrementalOutput()`` and ``getIncrementalErrorOutput()`` methods were added in Symfony 2.2. + echo $process->getOutput(); -The ``getOutput()`` method always return the whole content of the standard +The ``getOutput()`` method always returns the whole content of the standard output of the command and ``getErrorOutput()`` the content of the error output. Alternatively, the :method:`Symfony\\Component\\Process\\Process::getIncrementalOutput` and :method:`Symfony\\Component\\Process\\Process::getIncrementalErrorOutput` -methods returns the new outputs since the last call. +methods return the new output since the last call. + +The :method:`Symfony\\Component\\Process\\Process::clearOutput` method clears +the contents of the output and +:method:`Symfony\\Component\\Process\\Process::clearErrorOutput` clears +the contents of the error output. + +You can also use the :class:`Symfony\\Component\\Process\\Process` class with the +for each construct to get the output while it is generated. By default, the loop waits +for new output before going to the next iteration:: + + $process = new Process(['ls', '-lsa']); + $process->start(); + + foreach ($process as $type => $data) { + if ($process::OUT === $type) { + echo "\nRead from stdout: ".$data; + } else { // $process::ERR === $type + echo "\nRead from stderr: ".$data; + } + } + +.. tip:: + + The Process component internally uses a PHP iterator to get the output while + it is generated. That iterator is exposed via the ``getIterator()`` method + to allow customizing its behavior:: + + $process = new Process(['ls', '-lsa']); + $process->start(); + $iterator = $process->getIterator($process::ITER_SKIP_ERR | $process::ITER_KEEP_OUTPUT); + foreach ($iterator as $data) { + echo $data."\n"; + } + +The ``mustRun()`` method is identical to ``run()``, except that it will throw +a :class:`Symfony\\Component\\Process\\Exception\\ProcessFailedException` +if the process couldn't be executed successfully (i.e. the process exited +with a non-zero code):: + + use Symfony\Component\Process\Exception\ProcessFailedException; + use Symfony\Component\Process\Process; + + $process = new Process(['ls', '-lsa']); + + try { + $process->mustRun(); + + echo $process->getOutput(); + } catch (ProcessFailedException $exception) { + echo $exception->getMessage(); + } + +.. tip:: + + You can get the last output time in seconds by using the + :method:`Symfony\\Component\\Process\\Process::getLastOutputTime` method. + This method returns ``null`` if the process wasn't started! + +Configuring Process Options +--------------------------- + +Symfony uses the PHP :phpfunction:`proc_open` function to run the processes. +You can configure the options passed to the ``other_options`` argument of +``proc_open()`` using the ``setOptions()`` method:: + + $process = new Process(['...', '...', '...']); + // this option allows a subprocess to continue running after the main script exited + $process->setOptions(['create_new_console' => true]); + +.. warning:: + + Most of the options defined by ``proc_open()`` (such as ``create_new_console`` + and ``suppress_errors``) are only supported on Windows operating systems. + Check out the `PHP documentation for proc_open()`_ before using them. + +.. _process-using-features-from-the-os-shell: + +Using Features From the OS Shell +-------------------------------- + +Using an 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 while they run):: + + $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 +commands as strings using the +:method:`Symfony\\Component\\Process\\Process::fromShellCommandline` static +factory. + +Each operating system provides a different syntax for their command-lines, +so it becomes your responsibility to deal with escaping and portability. + +When using strings to define commands, variable arguments are passed as +environment variables using the second argument of the ``run()``, +``mustRun()`` or ``start()`` methods. Referencing them is also OS-dependent:: + + // On Unix-like OSes (Linux, macOS) + $process = Process::fromShellCommandline('echo "$MESSAGE"'); + + // On Windows + $process = Process::fromShellCommandline('echo "!MESSAGE!"'); + + // On both Unix-like and Windows + $process->run(null, ['MESSAGE' => 'Something to output']); + +If you prefer to create portable commands that are independent from the +operating system, you can write the above command as follows:: + + // works the same on Windows , Linux and macOS + $process = Process::fromShellCommandline('echo "${:MESSAGE}"'); + +Portable commands require using a syntax that is specific to the component: when +enclosing a variable name into ``"${:`` and ``}"`` exactly, the process object +will replace it with its escaped value, or will fail if the variable is not +found in the list of environment variables attached to the command. + +Setting Environment Variables for Processes +------------------------------------------- + +The constructor of the :class:`Symfony\\Component\\Process\\Process` class and +all of its methods related to executing processes (``run()``, ``mustRun()``, +``start()``, etc.) allow passing an array of environment variables to set while +running the process:: -When executing a long running command (like rsync-ing files to a remote + $process = new Process(['...'], null, ['ENV_VAR_NAME' => 'value']); + $process = Process::fromShellCommandline('...', null, ['ENV_VAR_NAME' => 'value']); + $process->run(null, ['ENV_VAR_NAME' => 'value']); + +In addition to the env vars passed explicitly, processes inherit all the env +vars defined in your system. You can prevent this by setting to ``false`` the +env vars you want to remove:: + + $process = new Process(['...'], null, [ + 'APP_ENV' => false, + 'SYMFONY_DOTENV_VARS' => false, + ]); + +Getting real-time Process Output +-------------------------------- + +When executing a long running command (like ``rsync`` to a remote server), you can give feedback to the end user in real-time by passing an anonymous function to the :method:`Symfony\\Component\\Process\\Process::run` method:: use Symfony\Component\Process\Process; - $process = new Process('ls -lsa'); - $process->run(function ($type, $buffer) { - if ('err' === $type) { + $process = new Process(['ls', '-lsa']); + $process->run(function ($type, $buffer): void { + if (Process::ERR === $type) { echo 'ERR > '.$buffer; } else { echo 'OUT > '.$buffer; } }); - -.. versionadded:: 2.1 - The non-blocking feature was added in 2.1. - + +.. 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 +-------------------------------- + You can also start the subprocess and then let it run asynchronously, retrieving -output and the status in your main process whenever you need it. Use the +output and the status in your main process whenever you need it. Use the :method:`Symfony\\Component\\Process\\Process::start` method to start an asynchronous process, the :method:`Symfony\\Component\\Process\\Process::isRunning` method to check if the process is done and the :method:`Symfony\\Component\\Process\\Process::getOutput` method to get the output:: - $process = new Process('ls -lsa'); + $process = new Process(['ls', '-lsa']); $process->start(); - + while ($process->isRunning()) { // waiting for process to finish } echo $process->getOutput(); - + You can also wait for a process to end if you started it asynchronously and are done doing other stuff:: - $process = new Process('ls -lsa'); + $process = new Process(['ls', '-lsa']); $process->start(); - + // ... do other things - - $process->wait(function ($type, $buffer) { - if ('err' === $type) { + + $process->wait(); + + // ... do things after the process has finished + +.. note:: + + The :method:`Symfony\\Component\\Process\\Process::wait` method is blocking, + which means that your code will halt at this line until the external + process is completed. + +.. note:: + + If a ``Response`` is sent **before** a child process had a chance to complete, + the server process will be killed (depending on your OS). It means that + your task will be stopped right away. Running an asynchronous process + is not the same as running a process that survives its parent process. + + If you want your process to survive the request/response cycle, you can + take advantage of the ``kernel.terminate`` event, and run your command + **synchronously** inside this event. Be aware that ``kernel.terminate`` + is called only if you use PHP-FPM. + +.. danger:: + + Beware also that if you do that, the said PHP-FPM process will not be + available to serve any new request until the subprocess is finished. This + means you can quickly block your FPM pool if you're not careful enough. + That is why it's generally way better not to do any fancy things even + after the request is sent, but to use a job queue instead. + +:method:`Symfony\\Component\\Process\\Process::wait` takes one optional argument: +a callback that is called repeatedly whilst the process is still running, passing +in the output and its type:: + + $process = new Process(['ls', '-lsa']); + $process->start(); + + $process->wait(function ($type, $buffer): void { + if (Process::ERR === $type) { echo 'ERR > '.$buffer; } else { echo 'OUT > '.$buffer; } }); +Instead of waiting until the process has finished, you can use the +:method:`Symfony\\Component\\Process\\Process::waitUntil` method to keep or stop +waiting based on some PHP logic. The following example starts a long running +process and checks its output to wait until its fully initialized:: + + $process = new Process(['/usr/bin/php', 'slow-starting-server.php']); + $process->start(); + + // ... do other things + + // waits until the given anonymous function returns true + $process->waitUntil(function ($type, $output): bool { + return $output === 'Ready. Waiting for commands...'; + }); + + // ... do things after the process is ready + +Streaming to the Standard Input of a Process +-------------------------------------------- + +Before a process is started, you can specify its standard input using either the +:method:`Symfony\\Component\\Process\\Process::setInput` method or the 4th argument +of the constructor. The provided input can be a string, a stream resource or a +``Traversable`` object:: + + $process = new Process(['cat']); + $process->setInput('foobar'); + $process->run(); + +When this input is fully written to the subprocess standard input, the corresponding +pipe is closed. + +In order to write to a subprocess standard input while it is running, the component +provides the :class:`Symfony\\Component\\Process\\InputStream` class:: + + $input = new InputStream(); + $input->write('foo'); + + $process = new Process(['cat']); + $process->setInput($input); + $process->start(); + + // ... read process output or do other things + + $input->write('bar'); + $input->close(); + + $process->wait(); + + // will echo: foobar + echo $process->getOutput(); + +The :method:`Symfony\\Component\\Process\\InputStream::write` method accepts scalars, +stream resources or ``Traversable`` objects as arguments. As shown in the above example, +you need to explicitly call the :method:`Symfony\\Component\\Process\\InputStream::close` +method when you are done writing to the standard input of the subprocess. + +Using PHP Streams as the Standard Input of a Process +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The input of a process can also be defined using `PHP streams`_:: + + $stream = fopen('php://temporary', 'w+'); + + $process = new Process(['cat']); + $process->setInput($stream); + $process->start(); + + fwrite($stream, 'foo'); + + // ... read process output or do other things + + fwrite($stream, 'bar'); + fclose($stream); + + $process->wait(); + + // will echo: 'foobar' + echo $process->getOutput(); + +Using TTY and PTY Modes +----------------------- + +All examples above show that your program has control over the input of a +process (using ``setInput()``) and the output from that process (using +``getOutput()``). The Process component has two special modes that tweak +the relationship between your program and the process: teletype (tty) and +pseudo-teletype (pty). + +In TTY mode, you connect the input and output of the process to the input +and output of your program. This allows for instance to open an editor like +Vim or Nano as a process. You enable TTY mode by calling +:method:`Symfony\\Component\\Process\\Process::setTty`:: + + $process = new Process(['vim']); + $process->setTty(true); + $process->run(); + + // As the output is connected to the terminal, it is no longer possible + // to read or modify the output from the process! + dump($process->getOutput()); // null + +In PTY mode, your program behaves as a terminal for the process instead of +a plain input and output. Some programs behave differently when +interacting with a real terminal instead of another program. For instance, +some programs prompt for a password when talking with a terminal. Use +:method:`Symfony\\Component\\Process\\Process::setPty` to enable this +mode. + +Stopping a Process +------------------ + +Any asynchronous process can be stopped at any time with the +:method:`Symfony\\Component\\Process\\Process::stop` method. This method takes +two arguments: a timeout and a signal. Once the timeout is reached, the signal +is sent to the running process. The default signal sent to a process is ``SIGKILL``. +Please read the :ref:`signal documentation below ` +to find out more about signal handling in the Process component:: + + $process = new Process(['ls', '-lsa']); + $process->start(); + + // ... do other things + + $process->stop(3, SIGINT); + +Executing PHP Code in Isolation +------------------------------- + If you want to execute some PHP code in isolation, use the ``PhpProcess`` instead:: use Symfony\Component\Process\PhpProcess; $process = new PhpProcess(<< + EOF ); $process->run(); -.. versionadded:: 2.1 - The ``ProcessBuilder`` class was added in Symfony 2.1. +Executing a PHP Child Process with the Same Configuration +--------------------------------------------------------- -To make your code work better on all platforms, you might want to use the -:class:`Symfony\\Component\\Process\\ProcessBuilder` class instead:: +When you start a PHP process, it uses the default configuration defined in +your ``php.ini`` file. You can bypass these options with the ``-d`` command line +option. For example, if ``memory_limit`` is set to ``256M``, you can disable this +memory limit when running some command like this: +``php -d memory_limit=-1 bin/console app:my-command``. - use Symfony\Component\Process\ProcessBuilder; +However, if you run the command via the Symfony ``Process`` class, PHP will use +the settings defined in the ``php.ini`` file. You can solve this issue by using +the :class:`Symfony\\Component\\Process\\PhpSubprocess` class to run the command:: - $builder = new ProcessBuilder(array('ls', '-lsa')); - $builder->getProcess()->run(); + use Symfony\Component\Process\Process; + + class MyCommand extends Command + { + protected function execute(InputInterface $input, OutputInterface $output): int + { + // the memory_limit (and any other config option) of this command is + // the one defined in php.ini instead of the new values (optionally) + // passed via the '-d' command option + $childProcess = new Process(['bin/console', 'cache:pool:prune']); + + // the memory_limit (and any other config option) of this command takes + // into account the values (optionally) passed via the '-d' command option + $childProcess = new PhpSubprocess(['bin/console', 'cache:pool:prune']); + } + } Process Timeout --------------- -You can limit the amount of time a process takes to complete by setting a -timeout (in seconds):: +By default processes have a timeout of 60 seconds, but you can change it passing +a different timeout (in seconds) to the ``setTimeout()`` method:: use Symfony\Component\Process\Process; - $process = new Process('ls -lsa'); + $process = new Process(['ls', '-lsa']); $process->setTimeout(3600); $process->run(); If the timeout is reached, a -:class:`Symfony\\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:: @@ -148,4 +477,141 @@ check regularly:: usleep(200000); } -.. _Packagist: https://packagist.org/packages/symfony/process +.. tip:: + + You can get the process start time using the ``getStartTime()`` method. + +.. _reference-process-signal: + +Process Idle Timeout +-------------------- + +In contrast to the timeout of the previous paragraph, the idle timeout only +considers the time since the last output was produced by the process:: + + use Symfony\Component\Process\Process; + + $process = new Process(['something-with-variable-runtime']); + $process->setTimeout(3600); + $process->setIdleTimeout(60); + $process->run(); + +In the case above, a process is considered timed out, when either the total runtime +exceeds 3600 seconds, or the process does not produce any output for 60 seconds. + +Process Signals +--------------- + +When running a program asynchronously, you can send it POSIX signals with the +:method:`Symfony\\Component\\Process\\Process::signal` method:: + + use Symfony\Component\Process\Process; + + $process = new Process(['find', '/', '-name', 'rabbit']); + $process->start(); + + // will send a SIGKILL to the process + $process->signal(SIGKILL); + +You can make the process ignore signals by using the +:method:`Symfony\\Component\\Process\\Process::setIgnoredSignals` +method. The given signals won't be propagated to the child process:: + + use Symfony\Component\Process\Process; + + $process = new Process(['find', '/', '-name', 'rabbit']); + $process->setIgnoredSignals([SIGKILL, SIGUSR1]); + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\Process\\Process::setIgnoredSignals` + method was introduced in Symfony 7.1. + +Process Pid +----------- + +You can access the `pid`_ of a running process with the +:method:`Symfony\\Component\\Process\\Process::getPid` method:: + + use Symfony\Component\Process\Process; + + $process = new Process(['/usr/bin/php', 'worker.php']); + $process->start(); + + $pid = $process->getPid(); + +Disabling Output +---------------- + +As standard output and error output are always fetched from the underlying process, +it might be convenient to disable output in some cases to save memory. +Use :method:`Symfony\\Component\\Process\\Process::disableOutput` and +:method:`Symfony\\Component\\Process\\Process::enableOutput` to toggle this feature:: + + use Symfony\Component\Process\Process; + + $process = new Process(['/usr/bin/php', 'worker.php']); + $process->disableOutput(); + $process->run(); + +.. warning:: + + You cannot enable or disable the output while the process is running. + + If you disable the output, you cannot access ``getOutput()``, + ``getIncrementalOutput()``, ``getErrorOutput()``, ``getIncrementalErrorOutput()`` or + ``setIdleTimeout()``. + + However, it is possible to pass a callback to the ``start``, ``run`` or ``mustRun`` + methods to handle process output in a streaming fashion. + +Finding an Executable +--------------------- + +The Process component provides a utility class called +:class:`Symfony\\Component\\Process\\ExecutableFinder` which finds +and returns the absolute path of an executable:: + + use Symfony\Component\Process\ExecutableFinder; + + $executableFinder = new ExecutableFinder(); + $chromedriverPath = $executableFinder->find('chromedriver'); + // $chromedriverPath = '/usr/local/bin/chromedriver' (the result will be different on your computer) + +The :method:`Symfony\\Component\\Process\\ExecutableFinder::find` method also takes extra parameters to specify a default value +to return and extra directories where to look for the executable:: + + use Symfony\Component\Process\ExecutableFinder; + + $executableFinder = new ExecutableFinder(); + $chromedriverPath = $executableFinder->find('chromedriver', '/path/to/chromedriver', ['local-bin/']); + +Finding the Executable PHP Binary +--------------------------------- + +This component also provides a special utility class called +:class:`Symfony\\Component\\Process\\PhpExecutableFinder` which returns the +absolute path of the executable PHP binary available on your server:: + + use Symfony\Component\Process\PhpExecutableFinder; + + $phpBinaryFinder = new PhpExecutableFinder(); + $phpBinaryPath = $phpBinaryFinder->find(); + // $phpBinaryPath = '/usr/local/bin/php' (the result will be different on your computer) + +Checking for TTY Support +------------------------ + +Another utility provided by this component is a method called +:method:`Symfony\\Component\\Process\\Process::isTtySupported` which returns +whether `TTY`_ is supported on the current operating system:: + + use Symfony\Component\Process\Process; + + $process = (new Process())->setTty(Process::isTtySupported()); + +.. _`pid`: https://en.wikipedia.org/wiki/Process_identifier +.. _`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) +.. _`PHP documentation for proc_open()`: https://www.php.net/manual/en/function.proc-open.php diff --git a/components/property_access.rst b/components/property_access.rst new file mode 100644 index 00000000000..f608640fa9b --- /dev/null +++ b/components/property_access.rst @@ -0,0 +1,589 @@ +The PropertyAccess Component +============================ + + The PropertyAccess component provides functions to read and write from/to an + object or array using a simple string notation. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/property-access + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +The entry point of this component is the +:method:`Symfony\\Component\\PropertyAccess\\PropertyAccess::createPropertyAccessor` +factory. This factory will create a new instance of the +:class:`Symfony\\Component\\PropertyAccess\\PropertyAccessor` class with the +default configuration:: + + use Symfony\Component\PropertyAccess\PropertyAccess; + + $propertyAccessor = PropertyAccess::createPropertyAccessor(); + +.. _property-access-reading-arrays: + +Reading from Arrays +------------------- + +You can read an array with the +:method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::getValue` method. +This is done using the index notation that is used in PHP:: + + // ... + $person = [ + 'first_name' => 'Wouter', + ]; + + var_dump($propertyAccessor->getValue($person, '[first_name]')); // 'Wouter' + var_dump($propertyAccessor->getValue($person, '[age]')); // null + +As you can see, the method will return ``null`` if the index does not exist. +But you can change this behavior with the +:method:`Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder::enableExceptionOnInvalidIndex` +method:: + + // ... + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() + ->enableExceptionOnInvalidIndex() + ->getPropertyAccessor(); + + $person = [ + 'first_name' => 'Wouter', + ]; + + // instead of returning null, the code now throws an exception of type + // Symfony\Component\PropertyAccess\Exception\NoSuchIndexException + $value = $propertyAccessor->getValue($person, '[age]'); + + // You can avoid the exception by adding the nullsafe operator + $value = $propertyAccessor->getValue($person, '[age?]'); + +You can also use multi dimensional arrays:: + + // ... + $persons = [ + [ + 'first_name' => 'Wouter', + ], + [ + 'first_name' => 'Ryan', + ], + ]; + + var_dump($propertyAccessor->getValue($persons, '[0][first_name]')); // 'Wouter' + var_dump($propertyAccessor->getValue($persons, '[1][first_name]')); // 'Ryan' + +.. tip:: + + If the key of the array contains a dot ``.`` or a left square bracket ``[``, + you must escape those characters with a backslash. In the above example, + if the array key was ``first.name`` instead of ``first_name``, you should + access its value as follows:: + + var_dump($propertyAccessor->getValue($persons, '[0][first\.name]')); // 'Wouter' + var_dump($propertyAccessor->getValue($persons, '[1][first\.name]')); // 'Ryan' + + Right square brackets ``]`` don't need to be escaped in array keys. + +Reading from Objects +-------------------- + +The ``getValue()`` method is a very robust method, and you can see all of its +features when working with objects. + +Accessing public Properties +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To read from properties, use the "dot" notation:: + + // ... + $person = new Person(); + $person->firstName = 'Wouter'; + + var_dump($propertyAccessor->getValue($person, 'firstName')); // 'Wouter' + + $child = new Person(); + $child->firstName = 'Bar'; + $person->children = [$child]; + + var_dump($propertyAccessor->getValue($person, 'children[0].firstName')); // 'Bar' + +.. warning:: + + Accessing public properties is the last option used by ``PropertyAccessor``. + It tries to access the value using the below methods first before using + the property directly. For example, if you have a public property that + has a getter method, it will use the getter. + +Using Getters +~~~~~~~~~~~~~ + +The ``getValue()`` method also supports reading using getters. The method will +be created using common naming conventions for getters. It transforms the +property name to camelCase (``first_name`` becomes ``FirstName``) and prefixes +it with ``get``. So the actual method becomes ``getFirstName()``:: + + // ... + class Person + { + private string $firstName = 'Wouter'; + + public function getFirstName(): string + { + return $this->firstName; + } + } + + $person = new Person(); + + var_dump($propertyAccessor->getValue($person, 'first_name')); // 'Wouter' + +Using Hassers/Issers +~~~~~~~~~~~~~~~~~~~~ + +And it doesn't even stop there. If there is no getter found, the accessor will +look for an isser or hasser. This method is created using the same way as +getters, this means that you can do something like this:: + + // ... + class Person + { + private bool $author = true; + private array $children = []; + + public function isAuthor(): bool + { + return $this->author; + } + + public function hasChildren(): bool + { + return 0 !== count($this->children); + } + } + + $person = new Person(); + + if ($propertyAccessor->getValue($person, 'author')) { + var_dump('This person is an author'); + } + if ($propertyAccessor->getValue($person, 'children')) { + var_dump('This person has children'); + } + +This will produce: ``This person is an author`` + +Accessing a non Existing Property Path +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default a :class:`Symfony\\Component\\PropertyAccess\\Exception\\NoSuchPropertyException` +is thrown if the property path passed to :method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::getValue` +does not exist. You can change this behavior using the +:method:`Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder::disableExceptionOnInvalidPropertyPath` +method:: + + // ... + class Person + { + public string $name; + } + + $person = new Person(); + + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() + ->disableExceptionOnInvalidPropertyPath() + ->getPropertyAccessor(); + + // instead of throwing an exception the following code returns null + $value = $propertyAccessor->getValue($person, 'birthday'); + +Accessing Nullable Property Paths +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Consider the following PHP code:: + + class Person + { + } + + class Comment + { + public ?Person $person = null; + public string $message; + } + + $comment = new Comment(); + $comment->message = 'test'; + +Given that ``$person`` is nullable, an object graph like ``comment.person.profile`` +will trigger an exception when the ``$person`` property is ``null``. The solution +is to mark all nullable properties with the nullsafe operator (``?``):: + + // This code throws an exception of type + // Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException + var_dump($propertyAccessor->getValue($comment, 'person.firstname')); + + // If a property marked with the nullsafe operator is null, the expression is + // no longer evaluated and null is returned immediately without throwing an exception + var_dump($propertyAccessor->getValue($comment, 'person?.firstname')); // null + +.. _components-property-access-magic-get: + +Magic ``__get()`` Method +~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``getValue()`` method can also use the magic ``__get()`` method:: + + // ... + class Person + { + private array $children = [ + 'Wouter' => [...], + ]; + + public function __get($id): mixed + { + return $this->children[$id]; + } + + public function __isset($id): bool + { + return isset($this->children[$id]); + } + } + + $person = new Person(); + + var_dump($propertyAccessor->getValue($person, 'Wouter')); // [...] + +.. warning:: + + When implementing the magic ``__get()`` method, you also need to implement + ``__isset()``. + +.. _components-property-access-magic-call: + +Magic ``__call()`` Method +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Lastly, ``getValue()`` can use the magic ``__call()`` method, but you need to +enable this feature by using :class:`Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder`:: + + // ... + class Person + { + private array $children = [ + 'wouter' => [...], + ]; + + public function __call($name, $args): mixed + { + $property = lcfirst(substr($name, 3)); + if ('get' === substr($name, 0, 3)) { + return $this->children[$property] ?? null; + } elseif ('set' === substr($name, 0, 3)) { + $value = 1 == count($args) ? $args[0] : null; + $this->children[$property] = $value; + } + } + } + + $person = new Person(); + + // enables PHP __call() magic method + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() + ->enableMagicCall() + ->getPropertyAccessor(); + + var_dump($propertyAccessor->getValue($person, 'wouter')); // [...] + +.. warning:: + + The ``__call()`` feature is disabled by default, you can enable it by calling + :method:`Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder::enableMagicCall` + see `Enable other Features`_. + +Writing to Arrays +----------------- + +The ``PropertyAccessor`` class can do more than just read an array, it can +also write to an array. This can be achieved using the +:method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::setValue` method:: + + // ... + $person = []; + + $propertyAccessor->setValue($person, '[first_name]', 'Wouter'); + + var_dump($propertyAccessor->getValue($person, '[first_name]')); // 'Wouter' + // or + // var_dump($person['first_name']); // 'Wouter' + +.. _components-property-access-writing-to-objects: + +Writing to Objects +------------------ + +The ``setValue()`` method has the same features as the ``getValue()`` method. You +can use setters, the magic ``__set()`` method or properties to set values:: + + // ... + class Person + { + public string $firstName; + private string $lastName; + private array $children = []; + + public function setLastName($name): void + { + $this->lastName = $name; + } + + public function getLastName(): string + { + return $this->lastName; + } + + public function getChildren(): array + { + return $this->children; + } + + public function __set($property, $value): void + { + $this->$property = $value; + } + } + + $person = new Person(); + + $propertyAccessor->setValue($person, 'firstName', 'Wouter'); + $propertyAccessor->setValue($person, 'lastName', 'de Jong'); // setLastName is called + $propertyAccessor->setValue($person, 'children', [new Person()]); // __set is called + + var_dump($person->firstName); // 'Wouter' + var_dump($person->getLastName()); // 'de Jong' + var_dump($person->getChildren()); // [Person()]; + +You can also use ``__call()`` to set values but you need to enable the feature, +see `Enable other Features`_:: + + // ... + class Person + { + private array $children = []; + + public function __call($name, $args): mixed + { + $property = lcfirst(substr($name, 3)); + if ('get' === substr($name, 0, 3)) { + return $this->children[$property] ?? null; + } elseif ('set' === substr($name, 0, 3)) { + $value = 1 == count($args) ? $args[0] : null; + $this->children[$property] = $value; + } + } + + } + + $person = new Person(); + + // Enable magic __call + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() + ->enableMagicCall() + ->getPropertyAccessor(); + + $propertyAccessor->setValue($person, 'wouter', [...]); + + var_dump($person->getWouter()); // [...] + +.. note:: + + The ``__set()`` method support is enabled by default. + See `Enable other Features`_ if you want to disable it. + +Writing to Array Properties +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``PropertyAccessor`` class allows to update the content of arrays stored in +properties through *adder* and *remover* methods:: + + // ... + class Person + { + /** + * @var string[] + */ + private array $children = []; + + public function getChildren(): array + { + return $this->children; + } + + public function addChild(string $name): void + { + $this->children[$name] = $name; + } + + public function removeChild(string $name): void + { + unset($this->children[$name]); + } + } + + $person = new Person(); + $propertyAccessor->setValue($person, 'children', ['kevin', 'wouter']); + + var_dump($person->getChildren()); // ['kevin', 'wouter'] + +The PropertyAccess component checks for methods called ``add()`` +and ``remove()``. Both methods must be defined. +For instance, in the previous example, the component looks for the ``addChild()`` +and ``removeChild()`` methods to access the ``children`` property. +`The String component`_ inflector is used to find the singular of a property name. + +If available, *adder* and *remover* methods have priority over a *setter* method. + +Using non-standard adder/remover methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes, adder and remover methods don't use the standard ``add`` or ``remove`` prefix, like in this example:: + + // ... + class Team + { + // ... + + public function joinTeam(string $person): void + { + $this->team[] = $person; + } + + public function leaveTeam(string $person): void + { + foreach ($this->team as $id => $item) { + if ($person === $item) { + unset($this->team[$id]); + + break; + } + } + } + } + + use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; + use Symfony\Component\PropertyAccess\PropertyAccessor; + + $list = new Team(); + $reflectionExtractor = new ReflectionExtractor(null, null, ['join', 'leave']); + $propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH, null, $reflectionExtractor, $reflectionExtractor); + $propertyAccessor->setValue($person, 'team', ['kevin', 'wouter']); + + var_dump($person->getTeam()); // ['kevin', 'wouter'] + +Instead of calling ``add()`` and ``remove()``, the PropertyAccess +component will call ``join()`` and ``leave()`` methods. + +Checking Property Paths +----------------------- + +When you want to check whether +:method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::getValue` can +safely be called without actually calling that method, you can use +:method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::isReadable` instead:: + + $person = new Person(); + + if ($propertyAccessor->isReadable($person, 'firstName')) { + // ... + } + +The same is possible for :method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::setValue`: +Call the :method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::isWritable` +method to find out whether a property path can be updated:: + + $person = new Person(); + + if ($propertyAccessor->isWritable($person, 'firstName')) { + // ... + } + +Mixing Objects and Arrays +------------------------- + +You can also mix objects and arrays:: + + // ... + class Person + { + public string $firstName; + private array $children = []; + + public function setChildren($children): void + { + $this->children = $children; + } + + public function getChildren(): array + { + return $this->children; + } + } + + $person = new Person(); + + $propertyAccessor->setValue($person, 'children[0]', new Person); + // equal to $person->getChildren()[0] = new Person() + + $propertyAccessor->setValue($person, 'children[0].firstName', 'Wouter'); + // equal to $person->getChildren()[0]->firstName = 'Wouter' + + var_dump('Hello '.$propertyAccessor->getValue($person, 'children[0].firstName')); // 'Wouter' + // equal to $person->getChildren()[0]->firstName + +Enable other Features +~~~~~~~~~~~~~~~~~~~~~ + +The :class:`Symfony\\Component\\PropertyAccess\\PropertyAccessor` can be +configured to enable extra features. To do that you could use the +:class:`Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder`:: + + // ... + $propertyAccessorBuilder = PropertyAccess::createPropertyAccessorBuilder(); + + $propertyAccessorBuilder->enableMagicCall(); // enables magic __call + $propertyAccessorBuilder->enableMagicGet(); // enables magic __get + $propertyAccessorBuilder->enableMagicSet(); // enables magic __set + $propertyAccessorBuilder->enableMagicMethods(); // enables magic __get, __set and __call + + $propertyAccessorBuilder->disableMagicCall(); // disables magic __call + $propertyAccessorBuilder->disableMagicGet(); // disables magic __get + $propertyAccessorBuilder->disableMagicSet(); // disables magic __set + $propertyAccessorBuilder->disableMagicMethods(); // disables magic __get, __set and __call + + // checks if magic __call, __get or __set handling are enabled + $propertyAccessorBuilder->isMagicCallEnabled(); // true or false + $propertyAccessorBuilder->isMagicGetEnabled(); // true or false + $propertyAccessorBuilder->isMagicSetEnabled(); // true or false + + // At the end get the configured property accessor + $propertyAccessor = $propertyAccessorBuilder->getPropertyAccessor(); + + // Or all in one + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() + ->enableMagicCall() + ->getPropertyAccessor(); + +Or you can pass parameters directly to the constructor (not the recommended way):: + + // enable handling of magic __call, __set but not __get: + $propertyAccessor = new PropertyAccessor(PropertyAccessor::MAGIC_CALL | PropertyAccessor::MAGIC_SET); + +.. _`The String component`: https://github.com/symfony/string diff --git a/components/property_access/index.rst b/components/property_access/index.rst deleted file mode 100644 index c40373aaac1..00000000000 --- a/components/property_access/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Property Access -=============== - -.. toctree:: - :maxdepth: 2 - - introduction diff --git a/components/property_access/introduction.rst b/components/property_access/introduction.rst deleted file mode 100644 index f4d4de5cb23..00000000000 --- a/components/property_access/introduction.rst +++ /dev/null @@ -1,264 +0,0 @@ -.. index:: - single: PropertyAccess - single: Components; PropertyAccess - -The PropertyAccess Component -============================ - - The PropertyAccess component provides function to read and write from/to an - object or array using a simple string notation. - -.. versionadded:: 2.2 - The PropertyAccess Component is new to Symfony 2.2. Previously, the - ``PropertyPath`` class was located in the ``Form`` component. - -Installation ------------- - -You can install the component in two different ways: - -* Use the official Git repository (https://github.com/symfony/PropertyAccess); -* :doc:`Install it via Composer` (``symfony/property-access`` on `Packagist`_). - -Usage ------ - -The entry point of this component is the -:method:`PropertyAccess::getPropertyAccessor` -factory. This factory will create a new instance of the -:class:`Symfony\\Component\\PropertyAccess\\PropertyAccessor` class with the -default configuration:: - - use Symfony\Component\PropertyAccess\PropertyAccess; - - $accessor = PropertyAccess::getPropertyAccessor(); - -Reading from Arrays -------------------- - -You can read an array with the -:method:`PropertyAccessor::getValue` -method. This is done using the index notation that is used in PHP:: - - // ... - $person = array( - 'first_name' => 'Wouter', - ); - - echo $accessor->getValue($person, '[first_name]'); // 'Wouter' - echo $accessor->getValue($person, '[age]'); // null - -As you can see, the method will return ``null`` if the index does not exists. - -You can also use multi dimensional arrays:: - - // ... - $persons = array( - array( - 'first_name' => 'Wouter', - ), - array( - 'first_name' => 'Ryan', - ) - ); - - echo $accessor->getValue($persons, '[0][first_name]'); // 'Wouter' - echo $accessor->getValue($persons, '[1][first_name]'); // 'Ryan' - -Reading from Objects --------------------- - -The ``getValue`` method is a very robust method, and you can see all of its -features when working with objects. - -Accessing public Properties -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To read from properties, use the "dot" notation:: - - // ... - $person = new Person(); - $person->firstName = 'Wouter'; - - echo $accessor->getValue($person, 'firstName'); // 'Wouter' - - $child = new Person(); - $child->firstName = 'Bar'; - $person->children = array($child); - - echo $accessor->getValue($person, 'children[0].firstName'); // 'Bar' - -.. caution:: - - Accessing public properties is the last option used by ``PropertyAccessor``. - It tries to access the value using the below methods first before using - the property directly. For example, if you have a public property that - has a getter method, it will use the getter. - -Using Getters -~~~~~~~~~~~~~ - -The ``getValue`` method also supports reading using getters. The method will -be created using common naming conventions for getters. It camelizes the -property name (``first_name`` becomes ``FirstName``) and prefixes it with -``get``. So the actual method becomes ``getFirstName``:: - - // ... - class Person - { - private $firstName = 'Wouter'; - - public function getFirstName() - { - return $this->firstName; - } - } - - $person = new Person(); - - echo $accessor->getValue($person, 'first_name'); // 'Wouter' - -Using Hassers/Issers -~~~~~~~~~~~~~~~~~~~~ - -And it doesn't even stop there. If there is no getter found, the accessor will -look for an isser or hasser. This method is created using the same way as -getters, this means that you can do something like this:: - - // ... - class Person - { - private $author = true; - private $children = array(); - - public function isAuthor() - { - return $this->author; - } - - public function hasChildren() - { - return 0 !== count($this->children); - } - } - - $person = new Person(); - - if ($accessor->getValue($person, 'author')) { - echo 'He is an author'; - } - if ($accessor->getValue($person, 'children')) { - echo 'He has children'; - } - -This will produce: ``He is an author`` - -Magic Methods -~~~~~~~~~~~~~ - -At last, ``getValue`` can use the magic ``__get`` method too:: - - // ... - class Person - { - private $children = array( - 'wouter' => array(...), - ); - - public function __get($id) - { - return $this->children[$id]; - } - } - - $person = new Person(); - - echo $accessor->getValue($person, 'Wouter'); // array(...) - -Writing to Arrays ------------------ - -The ``PropertyAccessor`` class can do more than just read an array, it can -also write to an array. This can be achieved using the -:method:`PropertyAccessor::setValue` -method:: - - // ... - $person = array(); - - $accessor->setValue($person, '[first_name]', 'Wouter'); - - echo $accessor->getValue($person, '[first_name]'); // 'Wouter' - // or - // echo $person['first_name']; // 'Wouter' - -Writing to Objects ------------------- - -The ``setValue`` method has the same features as the ``getValue`` method. You -can use setters, the magic ``__set`` or properties to set values:: - - // ... - class Person - { - public $firstName; - private $lastName; - private $children = array(); - - public function setLastName($name) - { - $this->lastName = $name; - } - - public function __set($property, $value) - { - $this->$property = $value; - } - - // ... - } - - $person = new Person(); - - $accessor->setValue($person, 'firstName', 'Wouter'); - $accessor->setValue($person, 'lastName', 'de Jong'); - $accessor->setValue($person, 'children', array(new Person())); - - echo $person->firstName; // 'Wouter' - echo $person->getLastName(); // 'de Jong' - echo $person->children; // array(Person()); - -Mixing Objects and Arrays -------------------------- - -You can also mix objects and arrays:: - - // ... - class Person - { - public $firstName; - private $children = array(); - - public function setChildren($children) - { - return $this->children; - } - - public function getChildren() - { - return $this->children; - } - } - - $person = new Person(); - - $accessor->setValue($person, 'children[0]', new Person); - // equal to $person->getChildren()[0] = new Person() - - $accessor->setValue($person, 'children[0].firstName', 'Wouter'); - // equal to $person->getChildren()[0]->firstName = 'Wouter' - - echo 'Hello '.$accessor->getValue($person, 'children[0].firstName'); // 'Wouter' - // equal to $person->getChildren()[0]->firstName - -.. _Packagist: https://packagist.org/packages/symfony/property-access diff --git a/components/property_info.rst b/components/property_info.rst new file mode 100644 index 00000000000..39019657ced --- /dev/null +++ b/components/property_info.rst @@ -0,0 +1,607 @@ +The PropertyInfo Component +========================== + + The PropertyInfo component allows you to get information + about class properties by using different sources of metadata. + +While the :doc:`PropertyAccess component ` +allows you to read and write values to/from objects and arrays, the PropertyInfo +component works solely with class definitions to provide information about the +data type and visibility - including via getter or setter methods - of the properties +within that class. + +.. _`components-property-information-installation`: + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/property-info + +.. include:: /components/require_autoload.rst.inc + +Additional dependencies may be required for some of the +:ref:`extractors provided with this component `. + +.. _`components-property-information-usage`: + +Usage +----- + +To use this component, create a new +:class:`Symfony\\Component\\PropertyInfo\\PropertyInfoExtractor` instance and +provide it with a set of information extractors:: + + use Example\Namespace\YourAwesomeCoolClass; + use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; + use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; + use Symfony\Component\PropertyInfo\PropertyInfoExtractor; + + // a full list of extractors is shown further below + $phpDocExtractor = new PhpDocExtractor(); + $reflectionExtractor = new ReflectionExtractor(); + + // list of PropertyListExtractorInterface (any iterable) + $listExtractors = [$reflectionExtractor]; + + // list of PropertyTypeExtractorInterface (any iterable) + $typeExtractors = [$phpDocExtractor, $reflectionExtractor]; + + // list of PropertyDescriptionExtractorInterface (any iterable) + $descriptionExtractors = [$phpDocExtractor]; + + // list of PropertyAccessExtractorInterface (any iterable) + $accessExtractors = [$reflectionExtractor]; + + // list of PropertyInitializableExtractorInterface (any iterable) + $propertyInitializableExtractors = [$reflectionExtractor]; + + $propertyInfo = new PropertyInfoExtractor( + $listExtractors, + $typeExtractors, + $descriptionExtractors, + $accessExtractors, + $propertyInitializableExtractors + ); + + // see below for more examples + $class = YourAwesomeCoolClass::class; + $properties = $propertyInfo->getProperties($class); + +Extractor Ordering +~~~~~~~~~~~~~~~~~~ + +The order of extractor instances within an array matters: the first non-null +result will be returned. That is why you must provide each category of extractors +as a separate array, even if an extractor provides information for more than +one category. + +For example, while the :class:`Symfony\\Component\\PropertyInfo\\Extractor\\ReflectionExtractor` +and :class:`Symfony\\Bridge\\Doctrine\\PropertyInfo\\DoctrineExtractor` +both provide list and type information it is probably better that: + +* The :class:`Symfony\\Component\\PropertyInfo\\Extractor\\ReflectionExtractor` + has priority for list information so that all properties in a class (not + just mapped properties) are returned. +* The :class:`Symfony\\Bridge\\Doctrine\\PropertyInfo\\DoctrineExtractor` + has priority for type information so that entity metadata is used instead + of type-hinting to provide more accurate type information:: + + use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor; + use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; + use Symfony\Component\PropertyInfo\PropertyInfoExtractor; + + $reflectionExtractor = new ReflectionExtractor(); + $doctrineExtractor = new DoctrineExtractor(/* ... */); + + $propertyInfo = new PropertyInfoExtractor( + // List extractors + [ + $reflectionExtractor, + $doctrineExtractor + ], + // Type extractors + [ + $doctrineExtractor, + $reflectionExtractor + ] + ); + +.. _`components-property-information-extractable-information`: + +Extractable Information +----------------------- + +The :class:`Symfony\\Component\\PropertyInfo\\PropertyInfoExtractor` +class exposes public methods to extract several types of information: + +* :ref:`List of properties `: :method:`Symfony\\Component\\PropertyInfo\\PropertyListExtractorInterface::getProperties` +* :ref:`Property type `: :method:`Symfony\\Component\\PropertyInfo\\PropertyTypeExtractorInterface::getTypes` + (including typed properties) +* :ref:`Property description `: :method:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface::getShortDescription` and :method:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface::getLongDescription` +* :ref:`Property access details `: :method:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface::isReadable` and :method:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface::isWritable` +* :ref:`Property initializable through the constructor `: :method:`Symfony\\Component\\PropertyInfo\\PropertyInitializableExtractorInterface::isInitializable` + +.. note:: + + Be sure to pass a *class* name, not an object to the extractor methods:: + + // bad! It may work, but not with all extractors + $propertyInfo->getProperties($awesomeObject); + + // Good! + $propertyInfo->getProperties(get_class($awesomeObject)); + $propertyInfo->getProperties('Example\Namespace\YourAwesomeClass'); + $propertyInfo->getProperties(YourAwesomeClass::class); + +.. _property-info-list: + +List Information +~~~~~~~~~~~~~~~~ + +Extractors that implement :class:`Symfony\\Component\\PropertyInfo\\PropertyListExtractorInterface` +provide the list of properties that are available on a class as an array +containing each property name as a string:: + + $properties = $propertyInfo->getProperties($class); + /* + Example Result + -------------- + array(3) { + [0] => string(8) "username" + [1] => string(8) "password" + [2] => string(6) "active" + } + */ + +.. _property-info-type: + +Type Information +~~~~~~~~~~~~~~~~ + +Extractors that implement :class:`Symfony\\Component\\PropertyInfo\\PropertyTypeExtractorInterface` +provide :ref:`extensive data type information ` +for a property:: + + $types = $propertyInfo->getTypes($class, $property); + /* + Example Result + -------------- + array(1) { + [0] => + class Symfony\Component\PropertyInfo\Type (6) { + private $builtinType => string(6) "string" + private $nullable => bool(false) + private $class => NULL + private $collection => bool(false) + private $collectionKeyType => NULL + private $collectionValueType => NULL + } + } + */ + +See :ref:`components-property-info-type` for info about the ``Type`` class. + +Documentation Block +~~~~~~~~~~~~~~~~~~~ + +Extractors that implement :class:`Symfony\\Component\\PropertyInfo\\PropertyDocBlockExtractorInterface` +can provide the full documentation block for a property as a string:: + + $docBlock = $propertyInfo->getDocBlock($class, $property); + /* + Example Result + -------------- + string(79): + This is the subsequent paragraph in the DocComment. + It can span multiple lines. + */ + +.. versionadded:: 7.1 + + The :class:`Symfony\\Component\\PropertyInfo\\PropertyDocBlockExtractorInterface` + interface was introduced in Symfony 7.1. + +.. _property-info-description: + +Description Information +~~~~~~~~~~~~~~~~~~~~~~~ + +Extractors that implement :class:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface` +provide long and short descriptions from a properties annotations as +strings:: + + $title = $propertyInfo->getShortDescription($class, $property); + /* + Example Result + -------------- + string(41) "This is the first line of the DocComment." + */ + + $paragraph = $propertyInfo->getLongDescription($class, $property); + /* + Example Result + -------------- + string(79): + This is the subsequent paragraph in the DocComment. + It can span multiple lines. + */ + +.. _property-info-access: + +Access Information +~~~~~~~~~~~~~~~~~~ + +Extractors that implement :class:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface` +provide whether properties are readable or writable as booleans:: + + $propertyInfo->isReadable($class, $property); + // Example Result: bool(true) + + $propertyInfo->isWritable($class, $property); + // Example Result: bool(false) + +The :class:`Symfony\\Component\\PropertyInfo\\Extractor\\ReflectionExtractor` looks +for getter/isser/setter/hasser method in addition to whether or not a property is public +to determine if it's accessible. This based on how the :doc:`PropertyAccess ` +works. It assumes camel case style method names following `PSR-1`_. For example, +both ``myProperty`` and ``my_property`` properties are readable if there's a +``getMyProperty()`` method and writable if there's a ``setMyProperty()`` method. + +.. _property-info-initializable: + +Property Initializable Information +---------------------------------- + +Extractors that implement :class:`Symfony\\Component\\PropertyInfo\\PropertyInitializableExtractorInterface` +provide whether properties are initializable through the class's constructor as booleans:: + + $propertyInfo->isInitializable($class, $property); + // Example Result: bool(true) + +:method:`Symfony\\Component\\PropertyInfo\\Extractor\\ReflectionExtractor::isInitializable` +returns ``true`` if a constructor's parameter of the given class matches the +given property name. + +.. tip:: + + The main :class:`Symfony\\Component\\PropertyInfo\\PropertyInfoExtractor` + class implements all interfaces, delegating the extraction of property + information to the extractors that have been registered with it. + + This means that any method available on each of the extractors is also + available on the main :class:`Symfony\\Component\\PropertyInfo\\PropertyInfoExtractor` + class. + +.. _`components-property-info-type`: + +Type Objects +------------ + +Compared to the other extractors, type information extractors provide much +more information than can be represented as simple scalar values. Because +of this, type extractors return an array of :class:`Symfony\\Component\\PropertyInfo\\Type` +objects for each type that the property supports. + +For example, if a property supports both ``integer`` and ``string`` (via +the ``@return int|string`` annotation), +:method:`PropertyInfoExtractor::getTypes() ` +will return an array containing **two** instances of the :class:`Symfony\\Component\\PropertyInfo\\Type` +class. + +.. note:: + + Most extractors will return only one :class:`Symfony\\Component\\PropertyInfo\\Type` + instance. The :class:`Symfony\\Component\\PropertyInfo\\Extractor\\PhpDocExtractor` + is currently the only extractor that returns multiple instances in the array. + +Each object will provide 6 attributes, available in the 6 methods: + +.. _`components-property-info-type-builtin`: + +``Type::getBuiltInType()`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :method:`Type::getBuiltinType() ` +method returns the built-in PHP data type, which can be one of these +string values: ``array``, ``bool``, ``callable``, ``float``, ``int``, +``iterable``, ``null``, ``object``, ``resource`` or ``string``. + +Constants inside the :class:`Symfony\\Component\\PropertyInfo\\Type` +class, in the form ``Type::BUILTIN_TYPE_*``, are provided for convenience. + +``Type::isNullable()`` +~~~~~~~~~~~~~~~~~~~~~~ + +The :method:`Type::isNullable() ` +method will return a boolean value indicating whether the property parameter +can be set to ``null``. + +``Type::getClassName()`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +If the :ref:`built-in PHP data type ` +is ``object``, the :method:`Type::getClassName() ` +method will return the fully-qualified class or interface name accepted. + +``Type::isCollection()`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +The :method:`Type::isCollection() ` +method will return a boolean value indicating if the property parameter is +a collection - a non-scalar value capable of containing other values. Currently +this returns ``true`` if: + +* The :ref:`built-in PHP data type ` + is ``array``; +* The mutator method the property is derived from has a prefix of ``add`` + or ``remove`` (which are defined as the list of array mutator prefixes); +* The `phpDocumentor`_ annotation is of type "collection" (e.g. + ``@var SomeClass``, ``@var SomeClass``, + ``@var Doctrine\Common\Collections\Collection``, etc.) + +``Type::getCollectionKeyTypes()`` & ``Type::getCollectionValueTypes()`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If the property is a collection, additional type objects may be returned +for both the key and value types of the collection (if the information is +available), via the :method:`Type::getCollectionKeyTypes() ` +and :method:`Type::getCollectionValueTypes() ` +methods. + +.. note:: + + The ``list`` pseudo type is returned by the PropertyInfo component as an + array with integer as the key type. + +.. _`components-property-info-extractors`: + +Extractors +---------- + +The extraction of property information is performed by *extractor classes*. +An extraction class can provide one or more types of property information +by implementing the correct interface(s). + +The :class:`Symfony\\Component\\PropertyInfo\\PropertyInfoExtractor` will +iterate over the relevant extractor classes in the order they were set, call +the appropriate method and return the first result that is not ``null``. + +.. _`components-property-information-extractors-available`: + +While you can create your own extractors, the following are already available +to cover most use-cases: + +ReflectionExtractor +~~~~~~~~~~~~~~~~~~~ + +Using PHP reflection, the :class:`Symfony\\Component\\PropertyInfo\\Extractor\\ReflectionExtractor` +provides list, type and access information from setter and accessor methods. +It can also give the type of a property (even extracting it from the constructor +arguments), and if it is initializable through the constructor. It supports +return and scalar types:: + + use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; + + $reflectionExtractor = new ReflectionExtractor(); + + // List information. + $reflectionExtractor->getProperties($class); + + // Type information. + $reflectionExtractor->getTypes($class, $property); + + // Access information. + $reflectionExtractor->isReadable($class, $property); + $reflectionExtractor->isWritable($class, $property); + + // Initializable information + $reflectionExtractor->isInitializable($class, $property); + +.. note:: + + When using the Symfony framework, this service is automatically registered + when the ``property_info`` feature is enabled: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + property_info: + enabled: true + +PhpDocExtractor +~~~~~~~~~~~~~~~ + +.. note:: + + This extractor depends on the `phpdocumentor/reflection-docblock`_ library. + +Using `phpDocumentor Reflection`_ to parse property and method annotations, +the :class:`Symfony\\Component\\PropertyInfo\\Extractor\\PhpDocExtractor` +provides type and description information. This extractor is automatically +registered with the ``property_info`` in the Symfony Framework *if* the dependent +library is present:: + + use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; + + $phpDocExtractor = new PhpDocExtractor(); + + // Type information. + $phpDocExtractor->getTypes($class, $property); + // Description information. + $phpDocExtractor->getShortDescription($class, $property); + $phpDocExtractor->getLongDescription($class, $property); + $phpDocExtractor->getDocBlock($class, $property); + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\PropertyInfo\\Extractor\\PhpDocExtractor::getDocBlock` + method was introduced in Symfony 7.1. + +PhpStanExtractor +~~~~~~~~~~~~~~~~ + +.. note:: + + This extractor depends on the `phpstan/phpdoc-parser`_ and + `phpdocumentor/reflection-docblock`_ libraries. + +This extractor fetches information thanks to the PHPStan parser. It gathers +information from annotations of properties and methods, such as ``@var``, +``@param`` or ``@return``:: + + // src/Domain/Foo.php + class Foo + { + /** + * @param string $bar + */ + public function __construct( + private string $bar, + ) { + } + } + + // Extraction.php + use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; + use App\Domain\Foo; + + $phpStanExtractor = new PhpStanExtractor(); + + // Type information. + $phpStanExtractor->getTypesFromConstructor(Foo::class, 'bar'); + // Description information. + $phpStanExtractor->getShortDescription($class, 'bar'); + $phpStanExtractor->getLongDescription($class, 'bar'); + +.. versionadded:: 7.3 + + The :method:`Symfony\\Component\\PropertyInfo\\Extractor\\PhpStanExtractor::getShortDescription` + and :method:`Symfony\\Component\\PropertyInfo\\Extractor\\PhpStanExtractor::getLongDescription` + methods were introduced in Symfony 7.3. + +SerializerExtractor +~~~~~~~~~~~~~~~~~~~ + +.. note:: + + This extractor depends on the `symfony/serializer`_ library. + +Using :ref:`groups metadata ` from the +:doc:`Serializer component `, the +:class:`Symfony\\Component\\PropertyInfo\\Extractor\\SerializerExtractor` +provides list information. This extractor is *not* registered automatically +with the ``property_info`` service in the Symfony Framework:: + + use Symfony\Component\PropertyInfo\Extractor\SerializerExtractor; + use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; + use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; + + $serializerClassMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $serializerExtractor = new SerializerExtractor($serializerClassMetadataFactory); + + // the `serializer_groups` option must be configured (may be set to null) + $serializerExtractor->getProperties($class, ['serializer_groups' => ['mygroup']]); + +If ``serializer_groups`` is set to ``null``, serializer groups metadata won't be +checked but you will get only the properties considered by the Serializer +Component (notably the ``#[Ignore]`` attribute is taken into account). + +DoctrineExtractor +~~~~~~~~~~~~~~~~~ + +.. note:: + + This extractor depends on the `symfony/doctrine-bridge`_ and `doctrine/orm`_ + libraries. + +Using entity mapping data from `Doctrine ORM`_, the +:class:`Symfony\\Bridge\\Doctrine\\PropertyInfo\\DoctrineExtractor` +provides list and type information. This extractor is not registered automatically +with the ``property_info`` service in the Symfony Framework:: + + use Doctrine\ORM\EntityManager; + use Doctrine\ORM\Tools\Setup; + use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor; + + $config = Setup::createAnnotationMetadataConfiguration([__DIR__], true); + $entityManager = EntityManager::create([ + 'driver' => 'pdo_sqlite', + // ... + ], $config); + $doctrineExtractor = new DoctrineExtractor($entityManager); + + // List information. + $doctrineExtractor->getProperties($class); + // Type information. + $doctrineExtractor->getTypes($class, $property); + +.. _components-property-information-constructor-extractor: + +ConstructorExtractor +~~~~~~~~~~~~~~~~~~~~ + +The :class:`Symfony\\Component\\PropertyInfo\\Extractor\\ConstructorExtractor` +tries to extract properties information by using either the +:class:`Symfony\\Component\\PropertyInfo\\Extractor\\PhpStanExtractor` or +the :class:`Symfony\\Component\\PropertyInfo\\Extractor\\ReflectionExtractor` +on the constructor arguments:: + + // src/Domain/Foo.php + class Foo + { + public function __construct( + private string $bar, + ) { + } + } + + // Extraction.php + use App\Domain\Foo; + use Symfony\Component\PropertyInfo\Extractor\ConstructorExtractor; + + $constructorExtractor = new ConstructorExtractor([new ReflectionExtractor()]); + $constructorExtractor->getTypes(Foo::class, 'bar')[0]->getBuiltinType(); // returns 'string' + +.. _`components-property-information-extractors-creation`: + +Creating Your Own Extractors +---------------------------- + +You can create your own property information extractors by creating a +class that implements one or more of the following interfaces: +:class:`Symfony\\Component\\PropertyInfo\\Extractor\\ConstructorArgumentTypeExtractorInterface`, +:class:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface`, +:class:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface`, +:class:`Symfony\\Component\\PropertyInfo\\PropertyListExtractorInterface`, +:class:`Symfony\\Component\\PropertyInfo\\PropertyTypeExtractorInterface` and +:class:`Symfony\\Component\\PropertyInfo\\PropertyInitializableExtractorInterface`. + +If you have enabled the PropertyInfo component with the FrameworkBundle, +you can automatically register your extractor class with the ``property_info`` +service by defining it as a service with one or more of the following +:doc:`tags `: + +* ``property_info.list_extractor`` if it provides list information. +* ``property_info.type_extractor`` if it provides type information. +* ``property_info.description_extractor`` if it provides description information. +* ``property_info.access_extractor`` if it provides access information. +* ``property_info.initializable_extractor`` if it provides initializable information + (it checks if a property can be initialized through the constructor). +* ``property_info.constructor_extractor`` if it provides type information from the constructor argument. + + .. versionadded:: 7.3 + + The ``property_info.constructor_extractor`` tag was introduced in Symfony 7.3. + +.. _`PSR-1`: https://www.php-fig.org/psr/psr-1/ +.. _`phpDocumentor Reflection`: https://github.com/phpDocumentor/ReflectionDocBlock +.. _`phpdocumentor/reflection-docblock`: https://packagist.org/packages/phpdocumentor/reflection-docblock +.. _`phpstan/phpdoc-parser`: https://packagist.org/packages/phpstan/phpdoc-parser +.. _`Doctrine ORM`: https://www.doctrine-project.org/projects/orm.html +.. _`symfony/serializer`: https://packagist.org/packages/symfony/serializer +.. _`symfony/doctrine-bridge`: https://packagist.org/packages/symfony/doctrine-bridge +.. _`doctrine/orm`: https://packagist.org/packages/doctrine/orm +.. _`phpDocumentor`: https://www.phpdoc.org/ diff --git a/components/psr7.rst b/components/psr7.rst new file mode 100644 index 00000000000..04a3b9148b5 --- /dev/null +++ b/components/psr7.rst @@ -0,0 +1,97 @@ +The PSR-7 Bridge +================ + + The PSR-7 bridge converts :doc:`HttpFoundation ` + objects from and to objects implementing HTTP message interfaces defined + by the `PSR-7`_. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/psr-http-message-bridge + +.. include:: /components/require_autoload.rst.inc + +The bridge also needs a PSR-7 and `PSR-17`_ implementation to convert +HttpFoundation objects to PSR-7 objects. The following command installs the +``nyholm/psr7`` library, a lightweight and fast PSR-7 implementation, but you +can use any of the `libraries that implement psr/http-factory-implementation`_: + +.. code-block:: terminal + + $ composer require nyholm/psr7 + +Usage +----- + +Converting from HttpFoundation Objects to PSR-7 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The bridge provides an interface of a factory called +`HttpMessageFactoryInterface`_ that builds objects implementing PSR-7 +interfaces from HttpFoundation objects. + +The following code snippet explains how to convert a :class:`Symfony\\Component\\HttpFoundation\\Request` +to a ``Nyholm\Psr7\ServerRequest`` class implementing the +``Psr\Http\Message\ServerRequestInterface`` interface:: + + use Nyholm\Psr7\Factory\Psr17Factory; + use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; + use Symfony\Component\HttpFoundation\Request; + + $symfonyRequest = new Request([], [], [], [], [], ['HTTP_HOST' => 'dunglas.fr'], 'Content'); + // The HTTP_HOST server key must be set to avoid an unexpected error + + $psr17Factory = new Psr17Factory(); + $psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); + $psrRequest = $psrHttpFactory->createRequest($symfonyRequest); + +And now from a :class:`Symfony\\Component\\HttpFoundation\\Response` to a +``Nyholm\Psr7\Response`` class implementing the +``Psr\Http\Message\ResponseInterface`` interface:: + + use Nyholm\Psr7\Factory\Psr17Factory; + use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; + use Symfony\Component\HttpFoundation\Response; + + $symfonyResponse = new Response('Content'); + + $psr17Factory = new Psr17Factory(); + $psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); + $psrResponse = $psrHttpFactory->createResponse($symfonyResponse); + +Converting Objects implementing PSR-7 Interfaces to HttpFoundation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +On the other hand, the bridge provide a factory interface called +`HttpFoundationFactoryInterface`_ that builds HttpFoundation objects from +objects implementing PSR-7 interfaces. + +The next snippet explain how to convert an object implementing the +``Psr\Http\Message\ServerRequestInterface`` interface to a +:class:`Symfony\\Component\\HttpFoundation\\Request` instance:: + + use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory; + + // $psrRequest is an instance of Psr\Http\Message\ServerRequestInterface + + $httpFoundationFactory = new HttpFoundationFactory(); + $symfonyRequest = $httpFoundationFactory->createRequest($psrRequest); + +From an object implementing the ``Psr\Http\Message\ResponseInterface`` +to a :class:`Symfony\\Component\\HttpFoundation\\Response` instance:: + + use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory; + + // $psrResponse is an instance of Psr\Http\Message\ResponseInterface + + $httpFoundationFactory = new HttpFoundationFactory(); + $symfonyResponse = $httpFoundationFactory->createResponse($psrResponse); + +.. _`PSR-7`: https://www.php-fig.org/psr/psr-7/ +.. _`PSR-17`: https://www.php-fig.org/psr/psr-17/ +.. _`libraries that implement psr/http-factory-implementation`: https://packagist.org/providers/psr/http-factory-implementation +.. _`HttpMessageFactoryInterface`: https://github.com/symfony/psr-http-message-bridge/blob/main/HttpMessageFactoryInterface.php +.. _`HttpFoundationFactoryInterface`: https://github.com/symfony/psr-http-message-bridge/blob/main/HttpFoundationFactoryInterface.php diff --git a/components/require_autoload.rst.inc b/components/require_autoload.rst.inc new file mode 100644 index 00000000000..9d47bd7ffca --- /dev/null +++ b/components/require_autoload.rst.inc @@ -0,0 +1,6 @@ +.. note:: + + If you install this component outside of a Symfony application, you must + require the ``vendor/autoload.php`` file in your code to enable the class + autoloading mechanism provided by Composer. Read + :doc:`this article ` for more details. diff --git a/components/routing/hostname_pattern.rst b/components/routing/hostname_pattern.rst deleted file mode 100644 index 38bc0f143eb..00000000000 --- a/components/routing/hostname_pattern.rst +++ /dev/null @@ -1,165 +0,0 @@ -.. index:: - single: Routing; Matching on Hostname - -How to match a route based on the Host -====================================== - -.. versionadded:: 2.2 - Host matching support was added in Symfony 2.2 - -You can also match on the HTTP *host* of the incoming request. - -.. configuration-block:: - - .. code-block:: yaml - - mobile_homepage: - path: / - host: m.example.com - defaults: { _controller: AcmeDemoBundle:Main:mobileHomepage } - - homepage: - path: / - defaults: { _controller: AcmeDemoBundle:Main:homepage } - - .. code-block:: xml - - - - - - - AcmeDemoBundle:Main:mobileHomepage - - - - AcmeDemoBundle:Main:homepage - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('mobile_homepage', new Route('/', array( - '_controller' => 'AcmeDemoBundle:Main:mobileHomepage', - ), array(), array(), 'm.example.com')); - - $collection->add('homepage', new Route('/', array( - '_controller' => 'AcmeDemoBundle:Main:homepage', - ))); - - return $collection; - -Both routes match the same path ``/``, however the first one will match -only if the host is ``m.example.com``. - -Placeholders and Requirements in Hostname Patterns --------------------------------------------------- - -If you're using the :doc:`DependencyInjection Component` -(or the full Symfony2 Framework), then you can use -:ref:`service container parameters` as -variables anywhere in your routes. - -You can avoid hardcoding the domain name by using a placeholder and a requirement. -The ``%domain%`` in requirements is replaced by the value of the ``domain`` -dependency injection container parameter. - -.. configuration-block:: - - .. code-block:: yaml - - mobile_homepage: - path: / - host: m.{domain} - defaults: { _controller: AcmeDemoBundle:Main:mobileHomepage } - requirements: - domain: %domain% - - homepage: - path: / - defaults: { _controller: AcmeDemoBundle:Main:homepage } - - .. code-block:: xml - - - - - - - AcmeDemoBundle:Main:mobileHomepage - %domain% - - - - AcmeDemoBundle:Main:homepage - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('mobile_homepage', new Route('/', array( - '_controller' => 'AcmeDemoBundle:Main:mobileHomepage', - ), array( - 'domain' => '%domain%', - ), array(), 'm.{domain}')); - - $collection->add('homepage', new Route('/', array( - '_controller' => 'AcmeDemoBundle:Main:homepage', - ))); - - return $collection; - -.. _component-routing-host-imported: - -Adding a Host Regex to Imported Routes --------------------------------------------- - -You can set a host regex on imported routes: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - acme_hello: - resource: "@AcmeHelloBundle/Resources/config/routing.yml" - host: "hello.example.com" - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\RouteCollection; - - $collection = new RouteCollection(); - $collection->addCollection($loader->import("@AcmeHelloBundle/Resources/config/routing.php"), '', array(), array(), array(), 'hello.example.com'); - - return $collection; - -The host ``hello.example.com`` will be set on each route loaded from the new -routing resource. diff --git a/components/routing/index.rst b/components/routing/index.rst deleted file mode 100644 index b7f4d40386b..00000000000 --- a/components/routing/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Routing -======= - -.. toctree:: - :maxdepth: 2 - - introduction - hostname_pattern diff --git a/components/routing/introduction.rst b/components/routing/introduction.rst deleted file mode 100644 index 8be2af7af92..00000000000 --- a/components/routing/introduction.rst +++ /dev/null @@ -1,344 +0,0 @@ -.. index:: - single: Routing - single: Components; Routing - -The Routing Component -===================== - - The Routing Component maps an HTTP request to a set of configuration - variables. - -Installation ------------- - -You can install the component in many different ways: - -* Use the official Git repository (https://github.com/symfony/Routing); -* :doc:`Install it via Composer` (``symfony/routing`` on `Packagist`_). - -Usage ------ - -In order to set up a basic routing system you need 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 request to a single route - -Let's see a quick example. Notice that this assumes that you've already configured -your autoloader to load the Routing component:: - - use Symfony\Component\Routing\Matcher\UrlMatcher; - use Symfony\Component\Routing\RequestContext; - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $route = new Route('/foo', array('controller' => 'MyController')); - $routes = new RouteCollection(); - $routes->add('route_name', $route); - - $context = new RequestContext($_SERVER['REQUEST_URI']); - - $matcher = new UrlMatcher($routes, $context); - - $parameters = $matcher->match('/foo'); - // array('controller' => 'MyController', '_route' => 'route_name') - -.. note:: - - Be careful when using ``$_SERVER['REQUEST_URI']``, as it may include - any query parameters on the URL, which will cause problems with route - matching. An easy way to solve this is 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`. - -The :method:`RouteCollection::add()` -method takes two arguments. The first is the name of the route. The second -is a :class:`Symfony\\Component\\Routing\\Route` object, which expects a -URL path and some array of custom variables in its constructor. This array -of custom variables can be *anything* that's significant to your application, -and is returned when that route is matched. - -If no matching route can be found a -:class:`Symfony\\Component\\Routing\\Exception\\ResourceNotFoundException` will be thrown. - -In addition to your array of custom variables, a ``_route`` key is added, -which holds the name of the matched route. - -Defining routes -~~~~~~~~~~~~~~~ - -A full route definition can contain up to seven parts: - -1. The URL path route. This is matched against the URL passed to the `RequestContext`, -and can contain named wildcard placeholders (e.g. ``{placeholders}``) -to match dynamic parts in the URL. - -2. An array of default values. This contains an array of arbitrary values -that will be returned when the request matches the route. - -3. An array of requirements. These define constraints for the values of the -placeholders as regular expressions. - -4. An array of options. These contain internal settings for the route and -are the least commonly needed. - -5. A host. This is matched against the host of the request. See - :doc:`/components/routing/hostname_pattern` for more details. - -6. An array of schemes. These enforce a certain HTTP scheme (``http``, ``https``). - -7. An array of methods. These enforce a certain HTTP request method (``HEAD``, - ``GET``, ``POST``, ...). - -.. versionadded:: 2.2 - Host matching support was added in Symfony 2.2 - -Take the following route, which combines several of these ideas:: - - $route = new Route( - '/archive/{month}', // path - array('controller' => 'showArchive'), // default values - array('month' => '[0-9]{4}-[0-9]{2}', 'subdomain' => 'www|m'), // requirements - array(), // options - '{subdomain}.example.com', // host - array(), // schemes - array() // methods - ); - - // ... - - $parameters = $matcher->match('/archive/2012-01'); - // array( - // 'controller' => 'showArchive', - // 'month' => '2012-01', - // 'subdomain' => 'www', - // '_route' => ... - // ) - - $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. - -.. tip:: - - If you want to match all urls which start with a certain path and end in an - arbitrary suffix you can use the following route definition:: - - $route = new Route( - '/start/{suffix}', - array('suffix' => ''), - array('suffix' => '.*') - ); - -Using Prefixes -~~~~~~~~~~~~~~ - -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, -default requirements, default options and host to all routes of a subtree with -the :method:`Symfony\\Component\\Routing\\RouteCollection::addPrefix` method:: - - $rootCollection = new RouteCollection(); - - $subCollection = new RouteCollection(); - $subCollection->add(...); - $subCollection->add(...); - $subCollection->addPrefix( - '/prefix', // prefix - array(), // requirements - array(), // options - 'admin.example.com', // host - array('https') // schemes - ); - - $rootCollection->addCollection($subCollection); - -.. versionadded:: 2.2 - The ``addPrefix`` method is added in Symfony2.2. This was part of the - ``addCollection`` method in older versions. - -Set the Request Parameters -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\Routing\\RequestContext` provides information -about the current request. You can define all parameters of an HTTP request -with this class via its constructor:: - - public function __construct( - $baseUrl = '', - $method = 'GET', - $host = 'localhost', - $scheme = 'http', - $httpPort = 80, - $httpsPort = 443 - ) - -.. _components-routing-http-foundation: - -Normally you can pass the values from the ``$_SERVER`` variable to populate the -:class:`Symfony\\Component\\Routing\\RequestContext`. But If you use the -:doc:`HttpFoundation` component, you can use its -:class:`Symfony\\Component\\HttpFoundation\\Request` class to feed the -:class:`Symfony\\Component\\Routing\\RequestContext` in a shortcut:: - - use Symfony\Component\HttpFoundation\Request; - - $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:: - - use Symfony\Component\Routing\Generator\UrlGenerator; - - $routes = new RouteCollection(); - $routes->add('show_post', new Route('/show/{slug}')); - - $context = new RequestContext($_SERVER['REQUEST_URI']); - - $generator = new UrlGenerator($routes, $context); - - $url = $generator->generate('show_post', array( - 'slug' => 'my-blog-post', - )); - // /show/my-blog-post - -.. 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. - -Load Routes from a File -~~~~~~~~~~~~~~~~~~~~~~~ - -You've already seen how you can easily 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. -If the file is found, the loader returns a :class:`Symfony\\Component\\Routing\\RouteCollection`. - -If you're using the ``YamlFileLoader``, then route definitions look like this: - -.. code-block:: yaml - - # routes.yml - route1: - path: /foo - defaults: { _controller: 'MyController::fooAction' } - - route2: - path: /foo/bar - defaults: { _controller: 'MyController::foobarAction' } - -To load this file, you can use the following code. This assumes that your -``routes.yml`` file is in the same directory as the below code:: - - use Symfony\Component\Config\FileLocator; - use Symfony\Component\Routing\Loader\YamlFileLoader; - - // look inside *this* directory - $locator = new FileLocator(array(__DIR__)); - $loader = new YamlFileLoader($locator); - $collection = $loader->load('routes.yml'); - -Besides :class:`Symfony\\Component\\Routing\\Loader\\YamlFileLoader` there are two -other loaders that work the same way: - -* :class:`Symfony\\Component\\Routing\\Loader\\XmlFileLoader` -* :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 :class:`Symfony\\Component\\Routing\\RouteCollection`:: - - // RouteProvider.php - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add( - 'route_name', - new Route('/foo', array('controller' => 'ExampleController')) - ); - // ... - - return $collection; - -Routes as Closures -.................. - -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`:: - - use Symfony\Component\Routing\Loader\ClosureLoader; - - $closure = function() { - return new RouteCollection(); - }; - - $loader = new ClosureLoader(); - $collection = $loader->load($closure); - -Routes as Annotations -..................... - -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. - -The all-in-one Router -~~~~~~~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\Routing\\Router` class is a all-in-one package -to quickly use the Routing component. The constructor expects a loader instance, -a path to the main route definition and some other settings:: - - public function __construct( - LoaderInterface $loader, - $resource, - array $options = array(), - RequestContext $context = null, - array $defaults = array() - ); - -With the ``cache_dir`` option you can enable route caching (if you provide a -path) or disable caching (if it's set to ``null``). The caching is done -automatically in the background if you want to use it. A basic example of the -:class:`Symfony\\Component\\Routing\\Router` class would look like:: - - $locator = new FileLocator(array(__DIR__)); - $requestContext = new RequestContext($_SERVER['REQUEST_URI']); - - $router = new Router( - new YamlFileLoader($locator), - 'routes.yml', - array('cache_dir' => __DIR__.'/cache'), - $requestContext - ); - $router->match('/foo/bar'); - -.. note:: - - If you use caching, the Routing component will compile new classes which - are saved in the ``cache_dir``. This means your script must have write - permissions for that location. - -.. _Packagist: https://packagist.org/packages/symfony/routing diff --git a/components/runtime.rst b/components/runtime.rst new file mode 100644 index 00000000000..4eb75de2a75 --- /dev/null +++ b/components/runtime.rst @@ -0,0 +1,493 @@ +The Runtime Component +===================== + + The Runtime Component decouples the bootstrapping logic from any global state + to make sure the application can run with runtimes like `PHP-PM`_, `ReactPHP`_, + `Swoole`_, `FrankenPHP`_ etc. without any changes. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/runtime + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +The Runtime component abstracts most bootstrapping logic as so-called +*runtimes*, allowing you to write front-controllers in a generic way. +For instance, the Runtime component allows Symfony's ``public/index.php`` +to look like this:: + + // public/index.php + use App\Kernel; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return function (array $context): Kernel { + return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); + }; + +So how does this front-controller work? At first, the special +``autoload_runtime.php`` file is automatically created by the Composer plugin in +the component. This file runs the following logic: + +#. It instantiates a :class:`Symfony\\Component\\Runtime\\RuntimeInterface`; +#. The callable (returned by ``public/index.php``) is passed to the Runtime, whose job + is to resolve the arguments (in this example: ``array $context``); +#. Then, this callable is called to get the application (``App\Kernel``); +#. At last, the Runtime is used to run the application (i.e. calling + ``$kernel->handle(Request::createFromGlobals())->send()``). + +.. warning:: + + If you use the Composer ``--no-plugins`` option, the ``autoload_runtime.php`` + file won't be created. + + If you use the Composer ``--no-scripts`` option, make sure your Composer version + is ``>=2.1.3``; otherwise the ``autoload_runtime.php`` file won't be created. + +To make a console application, the bootstrap code would look like:: + + #!/usr/bin/env php + setCode(static function (InputInterface $input, OutputInterface $output): void { + $output->write('Hello World'); + }); + + return $command; + }; + +:class:`Symfony\\Component\\Console\\Application` + Useful with console applications with more than one command. This will use the + :class:`Symfony\\Component\\Runtime\\Runner\\Symfony\\ConsoleApplicationRunner`:: + + use Symfony\Component\Console\Application; + use Symfony\Component\Console\Command\Command; + use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Output\OutputInterface; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return static function (array $context): Application { + $command = new Command('hello'); + $command->setCode(static function (InputInterface $input, OutputInterface $output): void { + $output->write('Hello World'); + }); + + $app = new Application(); + $app->add($command); + $app->setDefaultCommand('hello', true); + + return $app; + }; + +The ``GenericRuntime`` and ``SymfonyRuntime`` also support these generic +applications: + +:class:`Symfony\\Component\\Runtime\\RunnerInterface` + The ``RunnerInterface`` is a way to use a custom application with the + generic Runtime:: + + // public/index.php + use Symfony\Component\Runtime\RunnerInterface; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return static function (): RunnerInterface { + return new class implements RunnerInterface { + public function run(): int + { + echo 'Hello World'; + + return 0; + } + }; + }; + +``callable`` + Your "application" can also be a ``callable``. The first callable will return + the "application" and the second callable is the "application" itself:: + + // public/index.php + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return static function (): callable { + $app = static function(): int { + echo 'Hello World'; + + return 0; + }; + + return $app; + }; + +``void`` + If the callable doesn't return anything, the ``SymfonyRuntime`` will assume + everything is fine:: + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return function (): void { + echo 'Hello world'; + }; + +Using Options +~~~~~~~~~~~~~ + +Some behavior of the Runtimes can be modified through runtime options. They +can be set using the ``APP_RUNTIME_OPTIONS`` environment variable:: + + $_SERVER['APP_RUNTIME_OPTIONS'] = [ + 'project_dir' => '/var/task', + ]; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + // ... + +You can also configure ``extra.runtime`` in ``composer.json``: + +.. code-block:: json + + { + "require": { + "...": "..." + }, + "extra": { + "runtime": { + "project_dir": "/var/task" + } + } + } + +Then, update your Composer files (running ``composer dump-autoload``, for instance), +so that the ``vendor/autoload_runtime.php`` files gets regenerated with the new option. + +The following options are supported by the ``SymfonyRuntime``: + +``env`` (default: ``APP_ENV`` environment variable, or ``"dev"``) + To define the name of the environment the app runs in. +``disable_dotenv`` (default: ``false``) + To disable looking for ``.env`` files. +``dotenv_path`` (default: ``.env``) + To define the path of dot-env files. +``dotenv_overload`` (default: ``false``) + To tell Dotenv whether to override ``.env`` vars with ``.env.local`` (or other ``.env.*`` files) +``use_putenv`` + To tell Dotenv to set env vars using ``putenv()`` (NOT RECOMMENDED). +``prod_envs`` (default: ``["prod"]``) + To define the names of the production envs. +``test_envs`` (default: ``["test"]``) + To define the names of the test envs. + +Besides these, the ``GenericRuntime`` and ``SymfonyRuntime`` also support +these options: + +``debug`` (default: the value of the env var defined by ``debug_var_name`` option + (usually, ``APP_DEBUG``), or ``true`` if such env var is not defined) + Toggles the :ref:`debug mode ` of Symfony applications (e.g. to + display errors) +``runtimes`` + Maps "application types" to a ``GenericRuntime`` implementation that + knows how to deal with each of them. +``error_handler`` (default: :class:`Symfony\\Component\\Runtime\\Internal\\BasicErrorHandler` or :class:`Symfony\\Component\\Runtime\\Internal\\SymfonyErrorHandler` for ``SymfonyRuntime``) + Defines the class to use to handle PHP errors. +``env_var_name`` (default: ``"APP_ENV"``) + Defines the name of the env var that stores the name of the + :ref:`configuration environment ` + to use when running the application. +``debug_var_name`` (default: ``"APP_DEBUG"``) + Defines the name of the env var that stores the value of the + :ref:`debug mode ` flag to use when running the application. + +Create Your Own Runtime +----------------------- + +This is an advanced topic that describes the internals of the Runtime component. + +Using the Runtime component will benefit maintainers because the bootstrap +logic could be versioned as a part of a normal package. If the application +author decides to use this component, the package maintainer of the Runtime +class will have more control and can fix bugs and add features. + +The Runtime component is designed to be totally generic and able to run any +application outside of the global state in 6 steps: + +#. The main entry point returns a *callable* (the "app") that wraps the application; +#. The *app callable* is passed to ``RuntimeInterface::getResolver()``, which returns + a :class:`Symfony\\Component\\Runtime\\ResolverInterface`. This resolver returns + an array with the app callable (or something that decorates this callable) at + index 0 and all its resolved arguments at index 1. +#. The *app callable* is invoked with its arguments, it will return an object that + represents the application. +#. This *application object* is passed to ``RuntimeInterface::getRunner()``, which + returns a :class:`Symfony\\Component\\Runtime\\RunnerInterface`: an instance + that knows how to "run" the application object. +#. The ``RunnerInterface::run(object $application)`` is called and it returns the + exit status code as ``int``. +#. The PHP engine is terminated with this status code. + +When creating a new runtime, there are two things to consider: First, what arguments +will the end user use? Second, what will the user's application look like? + +For instance, imagine you want to create a runtime for `ReactPHP`_: + +**What arguments will the end user use?** + +For a generic ReactPHP application, no special arguments are +typically required. This means that you can use the +:class:`Symfony\\Component\\Runtime\\GenericRuntime`. + +**What will the user's application look like?** + +There is also no typical React application, so you might want to rely on +the `PSR-15`_ interfaces for HTTP request handling. + +However, a ReactPHP application will need some special logic to *run*. That logic +is added in a new class implementing :class:`Symfony\\Component\\Runtime\\RunnerInterface`:: + + use Psr\Http\Message\ResponseInterface; + use Psr\Http\Message\ServerRequestInterface; + use Psr\Http\Server\RequestHandlerInterface; + use React\EventLoop\Factory as ReactFactory; + use React\Http\Server as ReactHttpServer; + use React\Socket\Server as ReactSocketServer; + use Symfony\Component\Runtime\RunnerInterface; + + class ReactPHPRunner implements RunnerInterface + { + public function __construct( + private RequestHandlerInterface $application, + private int $port, + ) { + } + + public function run(): int + { + $application = $this->application; + $loop = ReactFactory::create(); + + // configure ReactPHP to correctly handle the PSR-15 application + $server = new ReactHttpServer( + $loop, + function (ServerRequestInterface $request) use ($application): ResponseInterface { + return $application->handle($request); + } + ); + + // start the ReactPHP server + $socket = new ReactSocketServer($this->port, $loop); + $server->listen($socket); + + $loop->run(); + + return 0; + } + } + +By extending the ``GenericRuntime``, you make sure that the application is +always using this ``ReactPHPRunner``:: + + use Symfony\Component\Runtime\GenericRuntime; + use Symfony\Component\Runtime\RunnerInterface; + + class ReactPHPRuntime extends GenericRuntime + { + private int $port; + + public function __construct(array $options) + { + $this->port = $options['port'] ?? 8080; + parent::__construct($options); + } + + public function getRunner(?object $application): RunnerInterface + { + if ($application instanceof RequestHandlerInterface) { + return new ReactPHPRunner($application, $this->port); + } + + // if it's not a PSR-15 application, use the GenericRuntime to + // run the application (see "Resolvable Applications" above) + return parent::getRunner($application); + } + } + +The end user will now be able to create front controller like:: + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return function (array $context): SomeCustomPsr15Application { + return new SomeCustomPsr15Application(); + }; + +.. _PHP-PM: https://github.com/php-pm/php-pm +.. _Swoole: https://openswoole.com/ +.. _FrankenPHP: https://frankenphp.dev/ +.. _ReactPHP: https://reactphp.org/ +.. _`PSR-15`: https://www.php-fig.org/psr/psr-15/ +.. _`runtime template file`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Runtime/Internal/autoload_runtime.template diff --git a/components/security/authentication.rst b/components/security/authentication.rst deleted file mode 100644 index 48154ebb5c5..00000000000 --- a/components/security/authentication.rst +++ /dev/null @@ -1,215 +0,0 @@ -.. index:: - single: Security, Authentication - -Authentication -============== - -When a request points to a secured area, and one of the listeners from the -firewall map is able to extract the user's credentials from the current -:class:`Symfony\\Component\\HttpFoundation\\Request` object, it should create -a token, containing these credentials. The next thing the listener should -do is ask the authentication manager to validate the given token, and return -an *authenticated* token if the supplied credentials were found to be valid. -The listener should then store the authenticated token in the security context:: - - use Symfony\Component\Security\Http\Firewall\ListenerInterface; - use Symfony\Component\Security\Core\SecurityContextInterface; - use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; - use Symfony\Component\HttpKernel\Event\GetResponseEvent; - use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; - - class SomeAuthenticationListener implements ListenerInterface - { - /** - * @var SecurityContextInterface - */ - private $securityContext; - - /** - * @var AuthenticationManagerInterface - */ - private $authenticationManager; - - /** - * @var string Uniquely identifies the secured area - */ - private $providerKey; - - // ... - - public function handle(GetResponseEvent $event) - { - $request = $event->getRequest(); - - $username = ...; - $password = ...; - - $unauthenticatedToken = new UsernamePasswordToken( - $username, - $password, - $this->providerKey - ); - - $authenticatedToken = $this - ->authenticationManager - ->authenticate($unauthenticatedToken); - - $this->securityContext->setToken($authenticatedToken); - } - } - -.. note:: - - A token can be of any class, as long as it implements - :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface`. - -The Authentication Manager --------------------------- - -The default authentication manager is an instance of -:class:`Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationProviderManager`:: - - use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager; - - // instances of Symfony\Component\Security\Core\Authentication\AuthenticationProviderInterface - $providers = array(...); - - $authenticationManager = new AuthenticationProviderManager($providers); - - try { - $authenticatedToken = $authenticationManager - ->authenticate($unauthenticatedToken); - } catch (AuthenticationException $failed) { - // authentication failed - } - -The ``AuthenticationProviderManager``, when instantiated, receives several -authentication providers, each supporting a different type of token. - -.. note:: - - You may of course write your own authentication manager, it only has - to implement :class:`Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationManagerInterface`. - -.. _authentication_providers: - -Authentication providers ------------------------- - -Each provider (since it implements -:class:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface`) -has a method :method:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface::supports` -by which the ``AuthenticationProviderManager`` -can determine if it supports the given token. If this is the case, the -manager then calls the provider's method :class:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface::authenticate`. -This method should return an authenticated token or throw an -:class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException` -(or any other exception extending it). - -Authenticating Users by their Username and Password -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An authentication provider will attempt to authenticate a user based on -the credentials he provided. Usually these are a username and a password. -Most web applications store their user's username and a hash of the user's -password combined with a randomly generated salt. This means that the average -authentication would consist of fetching the salt and the hashed password -from the user data storage, hash the password the user has just provided -(e.g. using a login form) with the salt and compare both to determine if -the given password is valid. - -This functionality is offered by the :class:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\DaoAuthenticationProvider`. -It fetches the user's data from a :class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface``, -uses a :class:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface` -to create a hash of the password and returns an authenticated token if the -password was valid:: - - use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider; - use Symfony\Component\Security\Core\User\UserChecker; - use Symfony\Component\Security\Core\User\InMemoryUserProvider; - use Symfony\Component\Security\Core\Encoder\EncoderFactory; - - $userProvider = new InMemoryUserProvider( - array( - 'admin' => array( - // password is "foo" - 'password' => '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg==', - 'roles' => array('ROLE_ADMIN'), - ), - ) - ); - - // for some extra checks: is account enabled, locked, expired, etc.? - $userChecker = new UserChecker(); - - // an array of password encoders (see below) - $encoderFactory = new EncoderFactory(...); - - $provider = new DaoAuthenticationProvider( - $userProvider, - $userChecker, - 'secured_area', - $encoderFactory - ); - - $provider->authenticate($unauthenticatedToken); - -.. note:: - - The example above demonstrates the use of the "in-memory" user provider, - but you may use any user provider, as long as it implements - :class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`. - It is also possible to let multiple user providers try to find the user's - data, using the :class:`Symfony\\Component\\Security\\Core\\User\\ChainUserProvider`. - -The Password encoder Factory -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\DaoAuthenticationProvider` -uses an encoder factory to create a password encoder for a given type of -user. This allows you to use different encoding strategies for different -types of users. The default :class:`Symfony\\Component\\Security\\Core\\Encoder\\EncoderFactory` -receives an array of encoders:: - - use Symfony\Component\Security\Core\Encoder\EncoderFactory; - use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder; - - $defaultEncoder = new MessageDigestPasswordEncoder('sha512', true, 5000); - $weakEncoder = new MessageDigestPasswordEncoder('md5', true, 1); - - $encoders = array( - 'Symfony\\Component\\Security\\Core\\User\\User' => $defaultEncoder, - 'Acme\\Entity\\LegacyUser' => $weakEncoder, - - // ... - ); - - $encoderFactory = new EncoderFactory($encoders); - -Each encoder should implement :class:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface` -or be an array with a ``class`` and an ``arguments`` key, which allows the -encoder factory to construct the encoder only when it is needed. - -Password Encoders -~~~~~~~~~~~~~~~~~ - -When the :method:`Symfony\\Component\\Security\\Core\\Encoder\\EncoderFactory::getEncoder` -method of the password encoder factory is called with the user object as -its first argument, it will return an encoder of type :class:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface` -which should be used to encode this user's password:: - - // fetch a user of type Acme\Entity\LegacyUser - $user = ... - - $encoder = $encoderFactory->getEncoder($user); - - // will return $weakEncoder (see above) - - $encodedPassword = $encoder->encodePassword($password, $user->getSalt()); - - // check if the password is valid: - - $validPassword = $encoder->isPasswordValid( - $user->getPassword(), - $password, - $user->getSalt()); diff --git a/components/security/authorization.rst b/components/security/authorization.rst deleted file mode 100644 index 7dc0433fd8e..00000000000 --- a/components/security/authorization.rst +++ /dev/null @@ -1,242 +0,0 @@ -.. index:: - single: Security, Authorization - -Authorization -============= - -When any of the authentication providers (see :ref:`authentication_providers`) -has verified the still-unauthenticated token, an authenticated token will -be returned. The authentication listener should set this token directly -in the :class:`Symfony\\Component\\Security\\Core\\SecurityContextInterface` -using its :method:`Symfony\\Component\\Security\\Core\\SecurityContextInterface::setToken` -method. - -From then on, the user is authenticated, i.e. identified. Now, other parts -of the application can use the token to decide whether or not the user may -request a certain URI, or modify a certain object. This decision will be made -by an instance of :class:`Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManagerInterface`. - -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` - 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 - Each attribute stands for a certain right the user should have, e.g. - ``ROLE_ADMIN`` to make sure the user is an administrator. -* An object (optional) - Any object on which for which access control needs to be checked, like - an article or a comment object. - -Access Decision Manager ------------------------ - -Since deciding whether or not a user is authorized to perform a certain -action can be a complicated process, the standard :class:`Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager` -itself depends on multiple voters, and makes a final verdict based on all -the votes (either positive, negative or neutral) it has received. It -recognizes several strategies: - -* ``affirmative`` (default) - grant access as soon as any voter returns an affirmative response; - -* ``consensus`` - grant access if there are more voters granting access than there are denying; - -* ``unanimous`` - only grant access if none of the voters has denied access; - -.. code-block:: php - - use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; - - // instances of Symfony\Component\Security\Core\Authorization\Voter\VoterInterface - $voters = array(...); - - // one of "affirmative", "consensus", "unanimous" - $strategy = ...; - - // whether or not to grant access when all voters abstain - $allowIfAllAbstainDecisions = ...; - - // whether or not to grant access when there is no majority (applies only to the "consensus" strategy) - $allowIfEqualGrantedDeniedDecisions = ...; - - $accessDecisionManager = new AccessDecisionManager( - $voters, - $strategy, - $allowIfAllAbstainDecisions, - $allowIfEqualGrantedDeniedDecisions - ); - -Voters ------- - -Voters are instances -of :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`, -which means they have to implement a few methods which allows the decision -manager to use them: - -* ``supportsAttribute($attribute)`` - will be used to check if the voter knows how to handle the given attribute; - -* ``supportsClass($class)`` - will be used to check if the voter is able to grant or deny access for - an object of the given class; - -* ``vote(TokenInterface $token, $object, array $attributes)`` - this method will do the actual voting and return a value equal to one - of the class constants of :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`, - i.e. ``VoterInterface::ACCESS_GRANTED``, ``VoterInterface::ACCESS_DENIED`` - or ``VoterInterface::ACCESS_ABSTAIN``; - -The security component contains some standard voters which cover many use -cases: - -AuthenticatedVoter -~~~~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AuthenticatedVoter` -voter supports the attributes ``IS_AUTHENTICATED_FULLY``, ``IS_AUTHENTICATED_REMEMBERED``, -and ``IS_AUTHENTICATED_ANONYMOUSLY`` and grants access based on the current -level of authentication, i.e. is the user fully authenticated, or only based -on a "remember-me" cookie, or even authenticated anonymously? - -.. code-block:: php - - use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver; - - $anonymousClass = 'Symfony\Component\Security\Core\Authentication\Token\AnonymousToken'; - $rememberMeClass = 'Symfony\Component\Security\Core\Authentication\Token\RememberMeToken'; - - $trustResolver = new AuthenticationTrustResolver($anonymousClass, $rememberMeClass); - - $authenticatedVoter = new AuthenticatedVoter($trustResolver); - - // instance of Symfony\Component\Security\Core\Authentication\Token\TokenInterface - $token = ...; - - // any object - $object = ...; - - $vote = $authenticatedVoter->vote($token, $object, array('IS_AUTHENTICATED_FULLY'); - -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` -method:: - - use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter; - - $roleVoter = new RoleVoter('ROLE_'); - - $roleVoter->vote($token, $object, 'ROLE_ADMIN'); - -RoleHierarchyVoter -~~~~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\RoleHierarchyVoter` -extends :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\RoleVoter` -and provides some additional functionality: it knows how to handle a -hierarchy of roles. For instance, a ``ROLE_SUPER_ADMIN`` role may have subroles -``ROLE_ADMIN`` and ``ROLE_USER``, so that when a certain object requires the -user to have the ``ROLE_ADMIN`` role, it grants access to users who in fact -have the ``ROLE_ADMIN`` role, but also to users having the ``ROLE_SUPER_ADMIN`` -role:: - - use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter; - use Symfony\Component\Security\Core\Role\RoleHierarchy; - - $hierarchy = array( - 'ROLE_SUPER_ADMIN' => array('ROLE_ADMIN', 'ROLE_USER'), - ); - - $roleHierarchy = new RoleHierarchy($hierarchy); - - $roleHierarchyVoter = new RoleHierarchyVoter($roleHierarchy); - -.. note:: - - When you make your own voter, you may of course use its constructor - to inject any dependencies it needs to come to a decision. - -Roles ------ - -Roles are objects that give expression to a certain right the user has. -The only requirement is that they implement :class:`Symfony\\Component\\Security\\Core\\Role\\RoleInterface`, -which means they should also have a :method:`Symfony\\Component\\Security\\Core\\Role\\Role\\RoleInterface::getRole` -method that returns a string representation of the role itself. The default -:class:`Symfony\\Component\\Security\\Core\\Role\\Role` simply returns its -first constructor argument:: - - use Symfony\Component\Security\Core\Role\Role; - - $role = new Role('ROLE_ADMIN'); - - // will echo 'ROLE_ADMIN' - echo $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. - -Using the decision manager --------------------------- - -The Access Listener -~~~~~~~~~~~~~~~~~~~ - -The access decision manager can be used at any point in a request to decide whether -or not the current user is entitled to access a given resource. One optional, -but useful, method for restricting access based on a URL pattern is the -:class:`Symfony\\Component\\Security\\Http\\Firewall\\AccessListener`, -which is one of the firewall listeners (see :ref:`firewall_listeners`) that -is triggered for each request matching the firewall map (see :ref:`firewall`). - -It uses an access map (which should be an instance of :class:`Symfony\\Component\\Security\\Http\\AccessMapInterface`) -which contains request matchers and a corresponding set of attributes that -are required for the current user to get access to the application:: - - use Symfony\Component\Security\Http\AccessMap; - use Symfony\Component\HttpFoundation\RequestMatcher; - use Symfony\Component\Security\Http\Firewall\AccessListener; - - $accessMap = new AccessMap(); - $requestMatcher = new RequestMatcher('^/admin'); - $accessMap->add($requestMatcher, array('ROLE_ADMIN')); - - $accessListener = new AccessListener( - $securityContext, - $accessDecisionManager, - $accessMap, - $authenticationManager - ); - -Security context -~~~~~~~~~~~~~~~~ - -The access decision manager is also available to other parts of the application -via the :method:`Symfony\\Component\\Security\\Core\\SecurityContext::isGranted` -method of the :class:`Symfony\\Component\\Security\\Core\\SecurityContext`. -A call to this method will directly delegate the question to the access -decision manager:: - - use Symfony\Component\Security\SecurityContext; - use Symfony\Component\Security\Core\Exception\AccessDeniedException; - - $securityContext = new SecurityContext( - $authenticationManager, - $accessDecisionManager - ); - - if (!$securityContext->isGranted('ROLE_ADMIN')) { - throw new AccessDeniedException(); - } diff --git a/components/security/firewall.rst b/components/security/firewall.rst deleted file mode 100644 index 1fce747905e..00000000000 --- a/components/security/firewall.rst +++ /dev/null @@ -1,131 +0,0 @@ -.. index:: - single: Security, Firewall - -The Firewall and Security Context -================================= - -Central to the Security Component is the security context, which is an instance -of :class:`Symfony\\Component\\Security\\Core\\SecurityContextInterface`. When all -steps in the process of authenticating the user have been taken successfully, -you can ask the security context if the authenticated user has access to a -certain action or resource of the application:: - - use Symfony\Component\Security\SecurityContext; - use Symfony\Component\Security\Core\Exception\AccessDeniedException; - - $securityContext = new SecurityContext(); - - // ... authenticate the user - - if (!$securityContext->isGranted('ROLE_ADMIN')) { - throw new AccessDeniedException(); - } - -.. _firewall: - -A Firewall for HTTP Requests ----------------------------- - -Authenticating a user is done by the firewall. An application may have -multiple secured areas, so the firewall is configured using a map of these -secured areas. For each of these areas, the map contains a request matcher -and a collection of listeners. The request matcher gives the firewall the -ability to find out if the current request points to a secured area. -The listeners are then asked if the current request can be used to authenticate -the user:: - - use Symfony\Component\Security\Http\FirewallMap; - use Symfony\Component\HttpFoundation\RequestMatcher; - use Symfony\Component\Security\Http\Firewall\ExceptionListener; - - $map = new FirewallMap(); - - $requestMatcher = new RequestMatcher('^/secured-area/'); - - // instances of Symfony\Component\Security\Http\Firewall\ListenerInterface - $listeners = array(...); - - $exceptionListener = new ExceptionListener(...); - - $map->add($requestMatcher, $listeners, $exceptionListener); - -The firewall map will be given to the firewall as its first argument, together -with the event dispatcher that is used by the :class:`Symfony\\Component\\HttpKernel\\HttpKernel`:: - - use Symfony\Component\Security\Http\Firewall; - use Symfony\Component\HttpKernel\KernelEvents; - - // the EventDispatcher used by the HttpKernel - $dispatcher = ...; - - $firewall = new Firewall($map, $dispatcher); - - $dispatcher->addListener(KernelEvents::REQUEST, array($firewall, 'onKernelRequest'); - -The firewall is registered to listen to the ``kernel.request`` event that -will be dispatched by the ``HttpKernel`` at the beginning of each request -it processes. This way, the firewall may prevent the user from going any -further than allowed. - -.. _firewall_listeners: - -Firewall listeners -~~~~~~~~~~~~~~~~~~ - -When the firewall gets notified of the ``kernel.request`` event, it asks -the firewall map if the request matches one of the secured areas. The first -secured area that matches the request will return a set of corresponding -firewall listeners (which each implement :class:`Symfony\\Component\\Security\\Http\\Firewall\\ListenerInterface`). -These listeners will all be asked to handle the current request. This basically -means: find out if the current request contains any information by which -the user might be authenticated (for instance the Basic HTTP authentication -listener checks if the request has a header called ``PHP_AUTH_USER``). - -Exception listener -~~~~~~~~~~~~~~~~~~ - -If any of the listeners throws an :class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException`, -the exception listener that was provided when adding secured areas to the -firewall map will jump in. - -The exception listener determines what happens next, based on the arguments -it received when it was created. It may start the authentication procedure, -perhaps ask the user to supply his credentials again (when he has only been -authenticated based on a "remember-me" cookie), or transform the exception -into an :class:`Symfony\\Component\\HttpKernel\\Exception\\AccessDeniedHttpException`, -which will eventually result in an "HTTP/1.1 403: Access Denied" response. - -Entry points -~~~~~~~~~~~~ - -When the user is not authenticated at all (i.e. when the security context -has no token yet), the firewall's entry point will be called to "start" -the authentication process. An entry point should implement -:class:`Symfony\\Component\\Security\\Http\\EntryPoint\\AuthenticationEntryPointInterface`, -which has only one method: :method:`Symfony\\Component\\Security\\Http\\EntryPoint\\AuthenticationEntryPointInterface::start`. -This method receives the current :class:`Symfony\\Component\\HttpFoundation\\Request` -object and the exception by which the exception listener was triggered. -The method should return a :class:`Symfony\\Component\\HttpFoundation\\Response` -object. This could be, for instance, the page containing the login form or, -in the case of Basic HTTP authentication, a response with a ``WWW-Authenticate`` -header, which will prompt the user to supply his username and password. - -Flow: Firewall, Authentication, Authorization ---------------------------------------------- - -Hopefully you can now see a little bit about how the "flow" of the security -context works: - -#. the Firewall is registered as a listener on the ``kernel.request`` event; -#. at the beginning of the request, the Firewall checks the firewall map - to see if any firewall should be active for this URL; -#. If a firewall is found in the map for this URL, its listeners are notified -#. each listener checks to see if the current request contains any authentication - information - a listener may (a) authenticate a user, (b) throw an - ``AuthenticationException``, or (c) do nothing (because there is no - authentication information on the request); -#. Once a user is authenticated, you'll use :doc:`/components/security/authorization` - to deny access to certain resources. - -Read the next sections to find out more about :doc:`/components/security/authentication` -and :doc:`/components/security/authorization`. \ No newline at end of file diff --git a/components/security/index.rst b/components/security/index.rst deleted file mode 100644 index c735740690d..00000000000 --- a/components/security/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Security -======== - -.. toctree:: - :maxdepth: 2 - - introduction - firewall - authentication - authorization \ No newline at end of file diff --git a/components/security/introduction.rst b/components/security/introduction.rst deleted file mode 100644 index a6849f15c4f..00000000000 --- a/components/security/introduction.rst +++ /dev/null @@ -1,32 +0,0 @@ -.. index:: - single: Security - -The Security Component -====================== - -Introduction ------------- - -The Security Component provides a complete security system for your web -application. It ships with facilities for authenticating using HTTP basic -or digest authentication, interactive form login or X.509 certificate login, -but also allows you to implement your own authentication strategies. -Furthermore, the component provides ways to authorize authenticated users -based on their roles, and it contains an advanced ACL system. - -Installation ------------- - -You can install the component in many different ways: - -* Use the official Git repository (https://github.com/symfony/Security); -* :doc:`Install it via Composer` (``symfony/security`` on Packagist_). - -Sections --------- - -* :doc:`/components/security/firewall` -* :doc:`/components/security/authentication` -* :doc:`/components/security/authorization` - -.. _Packagist: https://packagist.org/packages/symfony/security \ No newline at end of file diff --git a/components/semaphore.rst b/components/semaphore.rst new file mode 100644 index 00000000000..5715b426053 --- /dev/null +++ b/components/semaphore.rst @@ -0,0 +1,73 @@ +The Semaphore Component +======================= + + The Semaphore Component manages `semaphores`_, a mechanism to provide + exclusive access to a shared resource. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/semaphore + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +In computer science, a semaphore is a variable or abstract data type used to +control access to a common resource by multiple processes in a concurrent +system such as a multitasking operating system. The main difference +with :doc:`locks ` is that semaphores allow more than one process to +access a resource, whereas locks only allow one process. + +Create semaphores with the :class:`Symfony\\Component\\Semaphore\\SemaphoreFactory` +class, which in turn requires another class to manage the storage:: + + use Symfony\Component\Semaphore\SemaphoreFactory; + use Symfony\Component\Semaphore\Store\RedisStore; + + $redis = new Redis(); + $redis->connect('172.17.0.2'); + + $store = new RedisStore($redis); + $factory = new SemaphoreFactory($store); + +The semaphore is created by calling the +:method:`Symfony\\Component\\Semaphore\\SemaphoreFactory::createSemaphore` +method. Its first argument is an arbitrary string that represents the locked +resource. Its second argument is the maximum number of processes allowed. Then, a +call to the :method:`Symfony\\Component\\Semaphore\\SemaphoreInterface::acquire` +method will try to acquire the semaphore:: + + // ... + $semaphore = $factory->createSemaphore('pdf-invoice-generation', 2); + + if ($semaphore->acquire()) { + // The resource "pdf-invoice-generation" is locked. + // Here you can safely compute and generate the invoice. + + $semaphore->release(); + } + +If the semaphore can not be acquired, the method returns ``false``. The +``acquire()`` method can be safely called repeatedly, even if the semaphore is +already acquired. + +.. note:: + + Unlike other implementations, the Semaphore component distinguishes + semaphores instances even when they are created for the same resource. If a + semaphore has to be used by several services, they should share the same + ``Semaphore`` instance returned by the ``SemaphoreFactory::createSemaphore`` + method. + +.. tip:: + + If you don't release the semaphore explicitly, it will be released + automatically on instance destruction. In some cases, it can be useful to + lock a resource across several requests. To disable the automatic release + behavior, set the fifth argument of the ``createSemaphore()`` method to ``false``. + +.. _`semaphores`: https://en.wikipedia.org/wiki/Semaphore_(programming) diff --git a/components/serializer.rst b/components/serializer.rst deleted file mode 100644 index 877bc396639..00000000000 --- a/components/serializer.rst +++ /dev/null @@ -1,194 +0,0 @@ -.. index:: - single: Serializer - single: Components; Serializer - -The Serializer Component -======================== - - The Serializer Component is meant to be used to turn objects into a - specific format (XML, JSON, Yaml, ...) and the other way around. - -In order to do so, the Serializer Component follows the following -simple schema. - -.. _component-serializer-encoders: -.. _component-serializer-normalizers: - -.. image:: /images/components/serializer/serializer_workflow.png - -As you can see in the picture above, an array is used as a man in -the middle. This way, Encoders will only deal with turning specific -**formats** into **arrays** and vice versa. The same way, Normalizers -will deal with turning specific **objects** into **arrays** and vice versa. - -Serialization is a complicated topic, and while this component may not work -in all cases, it can be a useful tool while developing tools to serialize -and deserialize your objects. - -Installation ------------- - -You can install the component in many different ways: - -* Use the official Git repository (https://github.com/symfony/Serializer); -* :doc:`Install it via Composer` (``symfony/serializer`` on `Packagist`_). - -Usage ------ - -Using the Serializer component is really simple. You just need to set up -the :class:`Symfony\\Component\\Serializer\\Serializer` specifying -which Encoders and Normalizer are going to be available:: - - use Symfony\Component\Serializer\Serializer; - use Symfony\Component\Serializer\Encoder\XmlEncoder; - use Symfony\Component\Serializer\Encoder\JsonEncoder; - use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; - - $encoders = array(new XmlEncoder(), new JsonEncoder()); - $normalizers = array(new GetSetMethodNormalizer()); - - $serializer = new Serializer($normalizers, $encoders); - -Serializing an Object ---------------------- - -For the sake of this example, assume the following class already -exists in your project:: - - namespace Acme; - - class Person - { - private $age; - private $name; - - // Getters - public function getName() - { - return $this->name; - } - - public function getAge() - { - return $this->age; - } - - // Setters - public function setName($name) - { - $this->name = $name; - } - - public function setAge($age) - { - $this->age = $age; - } - } - -Now, if you want to serialize this object into JSON, you only need to -use the Serializer service created before:: - - $person = new Acme\Person(); - $person->setName('foo'); - $person->setAge(99); - - $jsonContent = $serializer->serialize($person, 'json'); - - // $jsonContent contains {"name":"foo","age":99} - - echo $jsonContent; // or return it in a Response - -The first parameter of the :method:`Symfony\\Component\\Serializer\\Serializer::serialize` -is the object to be serialized and the second is used to choose the proper encoder, -in this case :class:`Symfony\\Component\\Serializer\\Encoder\\JsonEncoder`. - -Ignoring Attributes when Serializing -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.3 - The :method:`GetSetMethodNormalizer::setIgnoredAttributes` - method was added in Symfony 2.3. - -As an option, there's a way to ignore attributes from the origin object when -serializing. To remove those attributes use the -:method:`Symfony\\Component\\Serializer\\Normalizer\\GetSetMethodNormalizer::setIgnoredAttributes` -method on the normalizer definition:: - - use Symfony\Component\Serializer\Serializer; - use Symfony\Component\Serializer\Encoder\JsonEncoder; - use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; - - $normalizer = new GetSetMethodNormalizer(); - $normalizer->setIgnoredAttributes(array('age')); - $encoder = new JsonEncoder(); - - $serializer = new Serializer(array($normalizer), array($encoder)); - $serializer->serialize($person, 'json'); // Output: {"name":"foo"} - -Deserializing an Object ------------------------ - -Let's see now how to do the exactly the opposite. This time, the information -of the `People` class would be encoded in XML format:: - - $data = << - foo - 99 - - EOF; - - $person = $serializer->deserialize($data,'Acme\Person','xml'); - -In this case, :method:`Symfony\\Component\\Serializer\\Serializer::deserialize` -needs three parameters: - -1. The information to be decoded -2. The name of the class this information will be decoded to -3. The encoder used to convert that information into an array - -Using Camelized Method Names for Underscored Attributes -------------------------------------------------------- - -.. versionadded:: 2.3 - The :method:`GetSetMethodNormalizer::setCamelizedAttributes` - method was added in Symfony 2.3. - -Sometimes property names from the serialized content are underscored (e.g. -``first_name``). Normally, these attributes will use get/set methods like -``getFirst_name``, when ``getFirstName`` method is what you really want. To -change that behavior use the -:method:`Symfony\\Component\\Serializer\\Normalizer\\GetSetMethodNormalizer::setCamelizedAttributes` -method on the normalizer definition:: - - $encoder = new JsonEncoder(); - $normalizer = new GetSetMethodNormalizer(); - $normalizer->setCamelizedAttributes(array('first_name')); - - $serializer = new Serializer(array($normalizer), array($encoder)); - - $json = <<deserialize($json, 'Acme\Person', 'json'); - -As a final result, the deserializer uses the ``first_name`` attribute as if -it were ``firstName`` and uses the ``getFirstName`` and ``setFirstName`` methods. - -JMSSerializer -------------- - -A popular third-party library, `JMS serializer`_, provides a more -sophisticated albeit more complex solution. This library includes the -ability to configure how your objects should be serialize/deserialized via -annotations (as well as YML, XML and PHP), integration with the Doctrine ORM, -and handling of other complex cases (e.g. circular references). - -.. _`JMS serializer`: https://github.com/schmittjoh/serializer -.. _Packagist: https://packagist.org/packages/symfony/serializer diff --git a/components/stopwatch.rst b/components/stopwatch.rst deleted file mode 100644 index af5f98e823b..00000000000 --- a/components/stopwatch.rst +++ /dev/null @@ -1,101 +0,0 @@ -.. index:: - single: Stopwatch - single: Components; Stopwatch - -The Stopwatch Component -======================= - - Stopwatch component provides a way to profile code. - -.. versionadded:: 2.2 - The Stopwatch Component is new to Symfony 2.2. Previously, the ``Stopwatch`` - class was located in the ``HttpKernel`` component (and was new in 2.1). - -Installation ------------- - -You can install the component in two different ways: - -* Use the official Git repository (https://github.com/symfony/Stopwatch); -* :doc:`Install it via Composer` (``symfony/stopwatch`` on `Packagist`_). - -Usage ------ - -The Stopwatch component provides an easy and consistent way to measure execution -time of certain parts of code so that you don't constantly have to parse -microtime by yourself. Instead, use the simple -:class:`Symfony\\Component\\Stopwatch\\Stopwatch` class:: - - use Symfony\Component\Stopwatch\Stopwatch; - - $stopwatch = new Stopwatch(); - // Start event named 'eventName' - $stopwatch->start('eventName'); - // ... some code goes here - $event = $stopwatch->stop('eventName'); - -You can also provide a category name to an event:: - - $stopwatch->start('eventName', 'categoryName'); - -You can consider categories as a way of tagging events. For example, the -Symfony Profiler tool uses categories to nicely color-code different events. - -Periods -------- - -As you know from the real world, all stopwatches come with two buttons: -one to start and stop the stopwatch, and another to measure the lap time. -This is exactly what the :method:`Symfony\\Component\\Stopwatch\\Stopwatch::lap`` -method does:: - - $stopwatch = new Stopwatch(); - // Start event named 'foo' - $stopwatch->start('foo'); - // ... some code goes here - $stopwatch->lap('foo'); - // ... some code goes here - $stopwatch->lap('foo'); - // ... some other code goes here - $event = $stopwatch->stop('foo'); - -Lap information is stored as "periods" within the event. To get lap information -call:: - - $event->getPeriods(); - -In addition to periods, you can get other useful information from the event object. -For example:: - - $event->getCategory(); // Returns the category the event was started in - $event->getOrigin(); // Returns the event start time in milliseconds - $event->ensureStopped(); // Stops all periods not already stopped - $event->getStartTime(); // Returns the start time of the very first period - $event->getEndTime(); // Returns the end time of the very last period - $event->getDuration(); // Returns the event duration, including all periods - $event->getMemory(); // Returns the max memory usage of all periods - -Sections --------- - -Sections are a way to logically split the timeline into groups. You can see -how Symfony uses sections to nicely visualize the framework lifecycle in the -Symfony Profiler tool. Here is a basic usage example using sections:: - - $stopwatch = new Stopwatch(); - - $stopwatch->openSection(); - $stopwatch->start('parsing_config_file', 'filesystem_operations'); - $stopwatch->stopSection('routing'); - - $events = $stopwatch->getSectionEvents('routing'); - -You can reopen a closed section by calling the :method:`Symfony\\Component\\Stopwatch\\Stopwatch::openSection`` -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 deleted file mode 100644 index cd4b8a0bf0a..00000000000 --- a/components/templating.rst +++ /dev/null @@ -1,113 +0,0 @@ -.. index:: - single: Templating - single: Components; Templating - -The Templating Component -======================== - - Templating provides all the tools needed to build any kind of template - system. - - It provides an infrastructure to load template files and optionally monitor - them for changes. It also provides a concrete template engine implementation - using PHP with additional tools for escaping and separating templates into - blocks and layouts. - -Installation ------------- - -You can install the component in many different ways: - -* Use the official Git repository (https://github.com/symfony/Templating); -* :doc:`Install it via Composer` (``symfony/templating`` on `Packagist`_). - -Usage ------ - -The :class:`Symfony\\Component\\Templating\\PhpEngine` class is the entry point -of the component. It needs a template name parser -(:class:`Symfony\\Component\\Templating\\TemplateNameParserInterface`) to -convert a template name to a template reference and template loader -(:class:`Symfony\\Component\\Templating\\Loader\\LoaderInterface`) to find the -template associated to a reference:: - - use Symfony\Component\Templating\PhpEngine; - use Symfony\Component\Templating\TemplateNameParser; - use Symfony\Component\Templating\Loader\FilesystemLoader; - - $loader = new FilesystemLoader(__DIR__ . '/views/%name%'); - - $view = new PhpEngine(new TemplateNameParser(), $loader); - - echo $view->render('hello.php', array('firstname' => 'Fabien')); - -The :method:`Symfony\\Component\\Templating\\PhpEngine::render` method executes -the file `views/hello.php` and returns the output text. - -.. code-block:: html+php - - - Hello, ! - -Template Inheritance with Slots -------------------------------- - -The template inheritance is designed to share layouts with many templates. - -.. code-block:: html+php - - - - - <?php $view['slots']->output('title', 'Default title') ?> - - - output('_content') ?> - - - -The :method:`Symfony\\Component\\Templating\\PhpEngine::extend` method is called in the -sub-template to set its parent template. - -.. code-block:: html+php - - - extend('layout.php') ?> - - set('title', $page->title) ?> - -

- title ?> -

-

- body ?> -

- -To use template inheritance, the :class:`Symfony\\Component\\Templating\\Helper\\SlotsHelper` -helper must be registered:: - - use Symfony\Component\Templating\Helper\SlotsHelper; - - $view->set(new SlotsHelper()); - - // Retrieve page object - $page = ...; - - echo $view->render('page.php', array('page' => $page)); - -.. note:: - - Multiple levels of inheritance is possible: a layout can extend an other - layout. - -Output Escaping ---------------- - -This documentation is still being written. - -The Asset Helper ----------------- - -This documentation is still being written. - -.. _Packagist: https://packagist.org/packages/symfony/templating \ No newline at end of file diff --git a/components/type_info.rst b/components/type_info.rst new file mode 100644 index 00000000000..817c7f1d61a --- /dev/null +++ b/components/type_info.rst @@ -0,0 +1,202 @@ +The TypeInfo Component +====================== + +The TypeInfo component extracts type information from PHP elements like properties, +arguments and return types. + +This component provides: + +* A powerful ``Type`` definition that can handle unions, intersections, and generics + (and can be extended to support more types in the future); +* A way to get types from PHP elements such as properties, method arguments, + return types, and raw strings. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/type-info + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +This component gives you a :class:`Symfony\\Component\\TypeInfo\\Type` object that +represents the PHP type of anything you built or asked to resolve. + +There are two ways to use this component. First one is to create a type manually thanks +to the :class:`Symfony\\Component\\TypeInfo\\Type` static methods as following:: + + use Symfony\Component\TypeInfo\Type; + + Type::int(); + Type::nullable(Type::string()); + Type::generic(Type::object(Collection::class), Type::int()); + Type::list(Type::bool()); + Type::intersection(Type::object(\Stringable::class), Type::object(\Iterator::class)); + +Many others methods are available and can be found +in :class:`Symfony\\Component\\TypeInfo\\TypeFactoryTrait`. + +You can also use a generic method that detects the type automatically:: + + Type::fromValue(1.1); // same as Type::float() + Type::fromValue('...'); // same as Type::string() + Type::fromValue(false); // same as Type::false() + +.. versionadded:: 7.3 + + The ``fromValue()`` method was introduced in Symfony 7.3. + +Resolvers +~~~~~~~~~ + +The second way to use the component is by using ``TypeInfo`` to resolve a type +based on reflection or a simple string. This approach is designed for libraries +that need a simple way to describe a class or anything with a type:: + + use Symfony\Component\TypeInfo\Type; + use Symfony\Component\TypeInfo\TypeResolver\TypeResolver; + + class Dummy + { + public function __construct( + public int $id, + ) { + } + } + + // Instantiate a new resolver + $typeResolver = TypeResolver::create(); + + // Then resolve types for any subject + $typeResolver->resolve(new \ReflectionProperty(Dummy::class, 'id')); // returns an "int" Type instance + $typeResolver->resolve('bool'); // returns a "bool" Type instance + + // Types can be instantiated thanks to static factories + $type = Type::list(Type::nullable(Type::bool())); + + // Type instances have several helper methods + + // for collections, it returns the type of the item used as the key; + // in this example, the collection is a list, so it returns an "int" Type instance + $keyType = $type->getCollectionKeyType(); + + // you can chain the utility methods (e.g. to introspect the values of the collection) + // the following code will return true + $isValueNullable = $type->getCollectionValueType()->isNullable(); + +Each of these calls will return you a ``Type`` instance that corresponds to the +static method used. You can also resolve types from a string (as shown in the +``bool`` parameter of the previous example) + +PHPDoc Parsing +~~~~~~~~~~~~~~ + +In many cases, you may not have cleanly typed properties or may need more precise +type definitions provided by advanced PHPDoc. To achieve this, you can use a string +resolver based on the PHPDoc annotations. + +First, run the command ``composer require phpstan/phpdoc-parser`` to install the +PHP package required for string resolving. Then, follow these steps:: + + use Symfony\Component\TypeInfo\TypeResolver\TypeResolver; + + class Dummy + { + public function __construct( + public int $id, + /** @var string[] $tags */ + public array $tags, + ) { + } + } + + $typeResolver = TypeResolver::create(); + $typeResolver->resolve(new \ReflectionProperty(Dummy::class, 'id')); // returns an "int" Type + $typeResolver->resolve(new \ReflectionProperty(Dummy::class, 'tags')); // returns a collection with "int" as key and "string" as values Type + +Advanced Usages +~~~~~~~~~~~~~~~ + +The TypeInfo component provides various methods to manipulate and check types, +depending on your needs. + +**Identify** a type:: + + // define a simple integer type + $type = Type::int(); + // check if the type matches a specific identifier + $type->isIdentifiedBy(TypeIdentifier::INT); // true + $type->isIdentifiedBy(TypeIdentifier::STRING); // false + + // define a union type (equivalent to PHP's int|string) + $type = Type::union(Type::string(), Type::int()); + // now the second check is true because the union type contains the string type + $type->isIdentifiedBy(TypeIdentifier::INT); // true + $type->isIdentifiedBy(TypeIdentifier::STRING); // true + + class DummyParent {} + class Dummy extends DummyParent implements DummyInterface {} + + // define an object type + $type = Type::object(Dummy::class); + + // check if the type is an object or matches a specific class + $type->isIdentifiedBy(TypeIdentifier::OBJECT); // true + $type->isIdentifiedBy(Dummy::class); // true + // check if it inherits/implements something + $type->isIdentifiedBy(DummyParent::class); // true + $type->isIdentifiedBy(DummyInterface::class); // true + +Checking if a type **accepts a value**:: + + $type = Type::int(); + // check if the type accepts a given value + $type->accepts(123); // true + $type->accepts('z'); // false + + $type = Type::union(Type::string(), Type::int()); + // now the second check is true because the union type accepts either an int or a string value + $type->accepts(123); // true + $type->accepts('z'); // true + +.. versionadded:: 7.3 + + The :method:`Symfony\\Component\\TypeInfo\\Type::accepts` + method was introduced in Symfony 7.3. + +Using callables for **complex checks**:: + + class Foo + { + private int $integer; + private string $string; + private ?float $float; + } + + $reflClass = new \ReflectionClass(Foo::class); + + $resolver = TypeResolver::create(); + $integerType = $resolver->resolve($reflClass->getProperty('integer')); + $stringType = $resolver->resolve($reflClass->getProperty('string')); + $floatType = $resolver->resolve($reflClass->getProperty('float')); + + // define a callable to validate non-nullable number types + $isNonNullableNumber = function (Type $type): bool { + if ($type->isNullable()) { + return false; + } + + if ($type->isIdentifiedBy(TypeIdentifier::INT) || $type->isIdentifiedBy(TypeIdentifier::FLOAT)) { + return true; + } + + return false; + }; + + $integerType->isSatisfiedBy($isNonNullableNumber); // true + $stringType->isSatisfiedBy($isNonNullableNumber); // false + $floatType->isSatisfiedBy($isNonNullableNumber); // false diff --git a/components/uid.rst b/components/uid.rst new file mode 100644 index 00000000000..6c92fff0af9 --- /dev/null +++ b/components/uid.rst @@ -0,0 +1,723 @@ +The UID Component +================= + + The UID component provides utilities to work with `unique identifiers`_ (UIDs) + such as UUIDs and ULIDs. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/uid + +.. include:: /components/require_autoload.rst.inc + +.. _uuid: + +UUIDs +----- + +`UUIDs`_ (*universally unique identifiers*) are one of the most popular UIDs in +the software industry. UUIDs are 128-bit numbers usually represented as five +groups of hexadecimal characters: ``xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx`` +(the ``M`` digit is the UUID version and the ``N`` digit is the UUID variant). + +Generating UUIDs +~~~~~~~~~~~~~~~~ + +Use the named constructors of the ``Uuid`` class or any of the specific classes +to create each type of UUID: + +**UUID v1** (time-based) + +Generates the UUID using a timestamp and the MAC address of your device +(`read UUIDv1 spec `__). +Both are obtained automatically, so you don't have to pass any constructor argument:: + + use Symfony\Component\Uid\Uuid; + + // $uuid is an instance of Symfony\Component\Uid\UuidV1 + $uuid = Uuid::v1(); + +.. tip:: + + It's recommended to use UUIDv7 instead of UUIDv1 because it provides + better entropy. + +**UUID v2** (DCE security) + +Similar to UUIDv1 but with a very high likelihood of ID collision +(`read UUIDv2 spec `__). +It's part of the authentication mechanism of DCE (Distributed Computing Environment) +and the UUID includes the POSIX UIDs (user/group ID) of the user who generated it. +This UUID variant is **not implemented** by the Uid component. + +**UUID v3** (name-based, MD5) + +Generates UUIDs from names that belong, and are unique within, some given namespace +(`read UUIDv3 spec `__). +This variant is useful to generate deterministic UUIDs from arbitrary strings. +It works by populating the UUID contents with the``md5`` hash of concatenating +the namespace and the name:: + + use Symfony\Component\Uid\Uuid; + + // you can use any of the predefined namespaces... + $namespace = Uuid::fromString(Uuid::NAMESPACE_OID); + // ...or use a random namespace: + // $namespace = Uuid::v4(); + + // $name can be any arbitrary string + // $uuid is an instance of Symfony\Component\Uid\UuidV3 + $uuid = Uuid::v3($namespace, $name); + +These are the default namespaces defined by the standard: + +* ``Uuid::NAMESPACE_DNS`` if you are generating UUIDs for `DNS entries `__ +* ``Uuid::NAMESPACE_URL`` if you are generating UUIDs for `URLs `__ +* ``Uuid::NAMESPACE_OID`` if you are generating UUIDs for `OIDs (object identifiers) `__ +* ``Uuid::NAMESPACE_X500`` if you are generating UUIDs for `X500 DNs (distinguished names) `__ + +**UUID v4** (random) + +Generates a random UUID (`read UUIDv4 spec `__). +Because of its randomness, it ensures uniqueness across distributed systems +without the need for a central coordinating entity. It's privacy-friendly +because it doesn't contain any information about where and when it was generated:: + + use Symfony\Component\Uid\Uuid; + + // $uuid is an instance of Symfony\Component\Uid\UuidV4 + $uuid = Uuid::v4(); + +**UUID v5** (name-based, SHA-1) + +It's the same as UUIDv3 (explained above) but it uses ``sha1`` instead of +``md5`` to hash the given namespace and name (`read UUIDv5 spec `__). +This makes it more secure and less prone to hash collisions. + +.. _uid-uuid-v6: + +**UUID v6** (reordered time-based) + +It rearranges the time-based fields of the UUIDv1 to make it lexicographically +sortable (like :ref:`ULIDs `). It's more efficient for database indexing +(`read UUIDv6 spec `__):: + + use Symfony\Component\Uid\Uuid; + + // $uuid is an instance of Symfony\Component\Uid\UuidV6 + $uuid = Uuid::v6(); + +.. tip:: + + It's recommended to use UUIDv7 instead of UUIDv6 because it provides + better entropy. + +.. _uid-uuid-v7: + +**UUID v7** (UNIX timestamp) + +Generates time-ordered UUIDs based on a high-resolution Unix Epoch timestamp +source (the number of milliseconds since midnight 1 Jan 1970 UTC, leap seconds excluded) +(`read UUIDv7 spec `__). +It's recommended to use this version over UUIDv1 and UUIDv6 because it provides +better entropy (and a more strict chronological order of UUID generation):: + + use Symfony\Component\Uid\Uuid; + + // $uuid is an instance of Symfony\Component\Uid\UuidV7 + $uuid = Uuid::v7(); + +**UUID v8** (custom) + +Provides an RFC-compatible format for experimental or vendor-specific use cases +(`read UUIDv8 spec `__). +The only requirement is to set the variant and version bits of the UUID. The rest +of the UUID value is specific to each implementation and no format should be assumed:: + + use Symfony\Component\Uid\Uuid; + + // $uuid is an instance of Symfony\Component\Uid\UuidV8 + $uuid = Uuid::v8(); + +If your UUID value is already generated in another format, use any of the +following methods to create a ``Uuid`` object from it:: + + // all the following examples would generate the same Uuid object + $uuid = Uuid::fromString('d9e7a184-5d5b-11ea-a62a-3499710062d0'); + $uuid = Uuid::fromBinary("\xd9\xe7\xa1\x84\x5d\x5b\x11\xea\xa6\x2a\x34\x99\x71\x00\x62\xd0"); + $uuid = Uuid::fromBase32('6SWYGR8QAV27NACAHMK5RG0RPG'); + $uuid = Uuid::fromBase58('TuetYWNHhmuSQ3xPoVLv9M'); + $uuid = Uuid::fromRfc4122('d9e7a184-5d5b-11ea-a62a-3499710062d0'); + +You can also use the ``UuidFactory`` to generate UUIDs. First, you may +configure the behavior of the factory using configuration files:: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/uid.yaml + framework: + uid: + default_uuid_version: 7 + name_based_uuid_version: 5 + name_based_uuid_namespace: 6ba7b810-9dad-11d1-80b4-00c04fd430c8 + time_based_uuid_version: 7 + time_based_uuid_node: 121212121212 + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/uid.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return static function (ContainerConfigurator $container): void { + $services = $container->services() + ->defaults() + ->autowire() + ->autoconfigure(); + + $container->extension('framework', [ + 'uid' => [ + 'default_uuid_version' => 7, + 'name_based_uuid_version' => 5, + 'name_based_uuid_namespace' => '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + 'time_based_uuid_version' => 7, + 'time_based_uuid_node' => 121212121212, + ], + ]); + }; + +Then, you can inject the factory in your services and use it to generate UUIDs based +on the configuration you defined:: + + namespace App\Service; + + use Symfony\Component\Uid\Factory\UuidFactory; + + class FooService + { + public function __construct( + private UuidFactory $uuidFactory, + ) { + } + + public function generate(): void + { + // This creates a UUID of the version given in the configuration file (v7 by default) + $uuid = $this->uuidFactory->create(); + + $nameBasedUuid = $this->uuidFactory->nameBased(/** ... */); + $randomBasedUuid = $this->uuidFactory->randomBased(); + $timestampBased = $this->uuidFactory->timeBased(); + + // ... + } + } + +Converting UUIDs +~~~~~~~~~~~~~~~~ + +Use these methods to transform the UUID object into different bases:: + + $uuid = Uuid::fromString('d9e7a184-5d5b-11ea-a62a-3499710062d0'); + + $uuid->toBinary(); // string(16) "\xd9\xe7\xa1\x84\x5d\x5b\x11\xea\xa6\x2a\x34\x99\x71\x00\x62\xd0" + $uuid->toBase32(); // string(26) "6SWYGR8QAV27NACAHMK5RG0RPG" + $uuid->toBase58(); // string(22) "TuetYWNHhmuSQ3xPoVLv9M" + $uuid->toRfc4122(); // string(36) "d9e7a184-5d5b-11ea-a62a-3499710062d0" + $uuid->toHex(); // string(34) "0xd9e7a1845d5b11eaa62a3499710062d0" + $uuid->toString(); // string(36) "d9e7a184-5d5b-11ea-a62a-3499710062d0" + +.. versionadded:: 7.1 + + The ``toString()`` method was introduced in Symfony 7.1. + +You can also convert some UUID versions to others:: + + // convert V1 to V6 or V7 + $uuid = Uuid::v1(); + + $uuid->toV6(); // returns a Symfony\Component\Uid\UuidV6 instance + $uuid->toV7(); // returns a Symfony\Component\Uid\UuidV7 instance + + // convert V6 to V7 + $uuid = Uuid::v6(); + + $uuid->toV7(); // returns a Symfony\Component\Uid\UuidV7 instance + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\Uid\\UuidV1::toV6`, + :method:`Symfony\\Component\\Uid\\UuidV1::toV7` and + :method:`Symfony\\Component\\Uid\\UuidV6::toV7` + methods were introduced in Symfony 7.1. + +Working with UUIDs +~~~~~~~~~~~~~~~~~~ + +UUID objects created with the ``Uuid`` class can use the following methods +(which are equivalent to the ``uuid_*()`` method of the PHP extension):: + + use Symfony\Component\Uid\NilUuid; + use Symfony\Component\Uid\Uuid; + + // checking if the UUID is null (note that the class is called + // NilUuid instead of NullUuid to follow the UUID standard notation) + $uuid = Uuid::v4(); + $uuid instanceof NilUuid; // false + + // checking the type of UUID + use Symfony\Component\Uid\UuidV4; + $uuid = Uuid::v4(); + $uuid instanceof UuidV4; // true + + // getting the UUID datetime (it's only available in certain UUID types) + $uuid = Uuid::v1(); + $uuid->getDateTime(); // returns a \DateTimeImmutable instance + + // checking if a given value is valid as UUID + $isValid = Uuid::isValid($uuid); // true or false + + // comparing UUIDs and checking for equality + $uuid1 = Uuid::v1(); + $uuid4 = Uuid::v4(); + $uuid1->equals($uuid4); // false + + // this method returns: + // * int(0) if $uuid1 and $uuid4 are equal + // * int > 0 if $uuid1 is greater than $uuid4 + // * int < 0 if $uuid1 is less than $uuid4 + $uuid1->compare($uuid4); // e.g. int(4) + +If you're working with different UUIDs format and want to validate them, +you can use the ``$format`` parameter of the :method:`Symfony\\Component\\Uid\\Uuid::isValid` +method to specify the UUID format you're expecting:: + + use Symfony\Component\Uid\Uuid; + + $isValid = Uuid::isValid('90067ce4-f083-47d2-a0f4-c47359de0f97', Uuid::FORMAT_RFC_4122); // accept only RFC 4122 UUIDs + $isValid = Uuid::isValid('3aJ7CNpDMfXPZrCsn4Cgey', Uuid::FORMAT_BASE_32 | Uuid::FORMAT_BASE_58); // accept multiple formats + +The following constants are available: + +* ``Uuid::FORMAT_BINARY`` +* ``Uuid::FORMAT_BASE_32`` +* ``Uuid::FORMAT_BASE_58`` +* ``Uuid::FORMAT_RFC_4122`` +* ``Uuid::FORMAT_RFC_9562`` (equivalent to ``Uuid::FORMAT_RFC_4122``) + +You can also use the ``Uuid::FORMAT_ALL`` constant to accept any UUID format. +By default, only the RFC 4122 format is accepted. + +.. versionadded:: 7.2 + + The ``$format`` parameter of the :method:`Symfony\\Component\\Uid\\Uuid::isValid` + method and the related constants were introduced in Symfony 7.2. + +Storing UUIDs in Databases +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you :doc:`use Doctrine `, consider using the ``uuid`` Doctrine +type, which converts to/from UUID objects automatically:: + + // src/Entity/Product.php + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Bridge\Doctrine\Types\UuidType; + use Symfony\Component\Uid\Uuid; + + #[ORM\Entity(repositoryClass: ProductRepository::class)] + class Product + { + #[ORM\Column(type: UuidType::NAME)] + private Uuid $someProperty; + + // ... + } + +There's also a Doctrine generator to help auto-generate UUID values for the +entity primary keys:: + + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Bridge\Doctrine\Types\UuidType; + use Symfony\Component\Uid\Uuid; + + class User implements UserInterface + { + #[ORM\Id] + #[ORM\Column(type: UuidType::NAME, unique: true)] + #[ORM\GeneratedValue(strategy: 'CUSTOM')] + #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')] + private ?Uuid $id; + + public function getId(): ?Uuid + { + return $this->id; + } + + // ... + } + +.. warning:: + + Using UUIDs as primary keys is usually not recommended for performance reasons: + indexes are slower and take more space (because UUIDs in binary format take + 128 bits instead of 32/64 bits for auto-incremental integers) and the non-sequential + nature of UUIDs fragments indexes. :ref:`UUID v6 ` and :ref:`UUID v7 ` + are the only variants that solve the fragmentation issue (but the index size issue remains). + +When using built-in Doctrine repository methods (e.g. ``findOneBy()``), Doctrine +knows how to convert these UUID types to build the SQL query +(e.g. ``->findOneBy(['user' => $user->getUuid()])``). However, when using DQL +queries or building the query yourself, you'll need to set ``uuid`` as the type +of the UUID parameters:: + + // src/Repository/ProductRepository.php + + // ... + use Doctrine\DBAL\ParameterType; + use Symfony\Bridge\Doctrine\Types\UuidType; + + class ProductRepository extends ServiceEntityRepository + { + // ... + + public function findUserProducts(User $user): array + { + $qb = $this->createQueryBuilder('p') + // ... + // add UuidType::NAME as the third argument to tell Doctrine that this is a UUID + ->setParameter('user', $user->getUuid(), UuidType::NAME) + + // alternatively, you can convert it to a value compatible with + // the type inferred by Doctrine + ->setParameter('user', $user->getUuid()->toBinary(), ParameterType::BINARY) + ; + + // ... + } + } + +.. _ulid: + +ULIDs +----- + +`ULIDs`_ (*Universally Unique Lexicographically Sortable Identifier*) are 128-bit +numbers usually represented as a 26-character string: ``TTTTTTTTTTRRRRRRRRRRRRRRRR`` +(where ``T`` represents a timestamp and ``R`` represents the random bits). + +ULIDs are an alternative to UUIDs when using those is impractical. They provide +128-bit compatibility with UUID, they are lexicographically sortable and they +are encoded as 26-character strings (vs 36-character UUIDs). + +.. note:: + + If you generate more than one ULID during the same millisecond in the + same process then the random portion is incremented by one bit in order + to provide monotonicity for sorting. The random portion is not random + compared to the previous ULID in this case. + +Generating ULIDs +~~~~~~~~~~~~~~~~ + +Instantiate the ``Ulid`` class to generate a random ULID value:: + + use Symfony\Component\Uid\Ulid; + + $ulid = new Ulid(); // e.g. 01AN4Z07BY79KA1307SR9X4MV3 + +If your ULID value is already generated in another format, use any of the +following methods to create a ``Ulid`` object from it:: + + // all the following examples would generate the same Ulid object + $ulid = Ulid::fromString('01E439TP9XJZ9RPFH3T1PYBCR8'); + $ulid = Ulid::fromBinary("\x01\x71\x06\x9d\x59\x3d\x97\xd3\x8b\x3e\x23\xd0\x6d\xe5\xb3\x08"); + $ulid = Ulid::fromBase32('01E439TP9XJZ9RPFH3T1PYBCR8'); + $ulid = Ulid::fromBase58('1BKocMc5BnrVcuq2ti4Eqm'); + $ulid = Ulid::fromRfc4122('0171069d-593d-97d3-8b3e-23d06de5b308'); + +Like UUIDs, ULIDs have their own factory, ``UlidFactory``, that can be used to generate them:: + + namespace App\Service; + + use Symfony\Component\Uid\Factory\UlidFactory; + + class FooService + { + public function __construct( + private UlidFactory $ulidFactory, + ) { + } + + public function generate(): void + { + $ulid = $this->ulidFactory->create(); + + // ... + } + } + +There's also a special ``NilUlid`` class to represent ULID ``null`` values:: + + use Symfony\Component\Uid\NilUlid; + + $ulid = new NilUlid(); + // equivalent to $ulid = new Ulid('00000000000000000000000000'); + +Converting ULIDs +~~~~~~~~~~~~~~~~ + +Use these methods to transform the ULID object into different bases:: + + $ulid = Ulid::fromString('01E439TP9XJZ9RPFH3T1PYBCR8'); + + $ulid->toBinary(); // string(16) "\x01\x71\x06\x9d\x59\x3d\x97\xd3\x8b\x3e\x23\xd0\x6d\xe5\xb3\x08" + $ulid->toBase32(); // string(26) "01E439TP9XJZ9RPFH3T1PYBCR8" + $ulid->toBase58(); // string(22) "1BKocMc5BnrVcuq2ti4Eqm" + $ulid->toRfc4122(); // string(36) "0171069d-593d-97d3-8b3e-23d06de5b308" + $ulid->toHex(); // string(34) "0x0171069d593d97d38b3e23d06de5b308" + +Working with ULIDs +~~~~~~~~~~~~~~~~~~ + +ULID objects created with the ``Ulid`` class can use the following methods:: + + use Symfony\Component\Uid\Ulid; + + $ulid1 = new Ulid(); + $ulid2 = new Ulid(); + + // checking if a given value is valid as ULID + $isValid = Ulid::isValid($ulidValue); // true or false + + // getting the ULID datetime + $ulid1->getDateTime(); // returns a \DateTimeImmutable instance + + // comparing ULIDs and checking for equality + $ulid1->equals($ulid2); // false + // this method returns $ulid1 <=> $ulid2 + $ulid1->compare($ulid2); // e.g. int(-1) + +Storing ULIDs in Databases +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you :doc:`use Doctrine `, consider using the ``ulid`` Doctrine +type, which converts to/from ULID objects automatically:: + + // src/Entity/Product.php + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Bridge\Doctrine\Types\UlidType; + use Symfony\Component\Uid\Ulid; + + #[ORM\Entity(repositoryClass: ProductRepository::class)] + class Product + { + #[ORM\Column(type: UlidType::NAME)] + private Ulid $someProperty; + + // ... + } + +There's also a Doctrine generator to help auto-generate ULID values for the +entity primary keys:: + + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Bridge\Doctrine\Types\UlidType; + use Symfony\Component\Uid\Ulid; + + class Product + { + #[ORM\Id] + #[ORM\Column(type: UlidType::NAME, unique: true)] + #[ORM\GeneratedValue(strategy: 'CUSTOM')] + #[ORM\CustomIdGenerator(class: 'doctrine.ulid_generator')] + private ?Ulid $id; + + public function getId(): ?Ulid + { + return $this->id; + } + + // ... + } + +.. warning:: + + Using ULIDs as primary keys is usually not recommended for performance reasons. + Although ULIDs don't suffer from index fragmentation issues (because the values + are sequential), their indexes are slower and take more space (because ULIDs + in binary format take 128 bits instead of 32/64 bits for auto-incremental integers). + +When using built-in Doctrine repository methods (e.g. ``findOneBy()``), Doctrine +knows how to convert these ULID types to build the SQL query +(e.g. ``->findOneBy(['user' => $user->getUlid()])``). However, when using DQL +queries or building the query yourself, you'll need to set ``ulid`` as the type +of the ULID parameters:: + + // src/Repository/ProductRepository.php + + // ... + use Symfony\Bridge\Doctrine\Types\UlidType; + + class ProductRepository extends ServiceEntityRepository + { + // ... + + public function findUserProducts(User $user): array + { + $qb = $this->createQueryBuilder('p') + // ... + // add UlidType::NAME as the third argument to tell Doctrine that this is a ULID + ->setParameter('user', $user->getUlid(), UlidType::NAME) + + // alternatively, you can convert it to a value compatible with + // the type inferred by Doctrine + ->setParameter('user', $user->getUlid()->toBinary()) + ; + + // ... + } + } + +Generating and Inspecting UUIDs/ULIDs in the Console +---------------------------------------------------- + +This component provides several commands to generate and inspect UUIDs/ULIDs in +the console. They are not enabled by default, so you must add the following +configuration in your application before using these commands: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + Symfony\Component\Uid\Command\GenerateUlidCommand: ~ + Symfony\Component\Uid\Command\GenerateUuidCommand: ~ + Symfony\Component\Uid\Command\InspectUlidCommand: ~ + Symfony\Component\Uid\Command\InspectUuidCommand: ~ + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\Uid\Command\GenerateUlidCommand; + use Symfony\Component\Uid\Command\GenerateUuidCommand; + use Symfony\Component\Uid\Command\InspectUlidCommand; + use Symfony\Component\Uid\Command\InspectUuidCommand; + + return static function (ContainerConfigurator $container): void { + // ... + + $services + ->set(GenerateUlidCommand::class) + ->set(GenerateUuidCommand::class) + ->set(InspectUlidCommand::class) + ->set(InspectUuidCommand::class); + }; + +Now you can generate UUIDs/ULIDs as follows (add the ``--help`` option to the +commands to learn about all their options): + +.. code-block:: terminal + + # generate 1 random-based UUID + $ php bin/console uuid:generate --random-based + + # generate 1 time-based UUID with a specific node + $ php bin/console uuid:generate --time-based=now --node=fb3502dc-137e-4849-8886-ac90d07f64a7 + + # generate 2 UUIDs and output them in base58 format + $ php bin/console uuid:generate --count=2 --format=base58 + + # generate 1 ULID with the current time as the timestamp + $ php bin/console ulid:generate + + # generate 1 ULID with a specific timestamp + $ php bin/console ulid:generate --time="2021-02-02 14:00:00" + + # generate 2 ULIDs and output them in RFC4122 format + $ php bin/console ulid:generate --count=2 --format=rfc4122 + +In addition to generating new UIDs, you can also inspect them with the following +commands to show all the information for a given UID: + +.. code-block:: terminal + + $ php bin/console uuid:inspect d0a3a023-f515-4fe0-915c-575e63693998 + ---------------------- -------------------------------------- + Label Value + ---------------------- -------------------------------------- + Version 4 + Canonical (RFC 4122) d0a3a023-f515-4fe0-915c-575e63693998 + Base 58 SmHvuofV4GCF7QW543rDD9 + Base 32 6GMEG27X8N9ZG92Q2QBSHPJECR + ---------------------- -------------------------------------- + + $ php bin/console ulid:inspect 01F2TTCSYK1PDRH73Z41BN1C4X + --------------------- -------------------------------------- + Label Value + --------------------- -------------------------------------- + Canonical (Base 32) 01F2TTCSYK1PDRH73Z41BN1C4X + Base 58 1BYGm16jS4kX3VYCysKKq6 + RFC 4122 0178b5a6-67d3-0d9b-889c-7f205750b09d + --------------------- -------------------------------------- + Timestamp 2021-04-09 08:01:24.947 + --------------------- -------------------------------------- + +.. _`unique identifiers`: https://en.wikipedia.org/wiki/UID +.. _`UUIDs`: https://en.wikipedia.org/wiki/Universally_unique_identifier +.. _`ULIDs`: https://github.com/ulid/spec diff --git a/components/using_components.rst b/components/using_components.rst index 174b041facc..f975be7e1b2 100644 --- a/components/using_components.rst +++ b/components/using_components.rst @@ -1,16 +1,14 @@ -.. index:: - single: Components; Installation - single: Components; Usage +.. _how-to-install-and-use-the-symfony2-components: -How to Install and Use the Symfony2 Components -============================================== +How to Install and Use the Symfony Components +============================================= If you're starting a new project (or already have a project) that will use one or more components, the easiest way to integrate everything is with `Composer`_. Composer is smart enough to download the component(s) that you need and take care of autoloading so that you can begin using the libraries immediately. -This article will take you through using the :doc:`/components/finder`, though +This article will take you through using :doc:`/components/finder`, though this applies to using any component. Using the Finder Component @@ -18,84 +16,57 @@ Using the Finder Component **1.** If you're creating a new project, create a new empty directory for it. -**2.** Create a new file called ``composer.json`` and paste the following into it: +**2.** Open a terminal, step into this directory and use Composer to grab the library. -.. code-block:: json +.. code-block:: terminal - { - "require": { - "symfony/finder": "2.2.*" - } - } + $ composer require symfony/finder -If you already have a ``composer.json`` file, just add this line to it. You -may also need to adjust the version (e.g. ``2.1.1`` or ``2.2.*``). +The name ``symfony/finder`` is written at the top of the documentation for +whatever component you want. -You can research the component names and versions at `packagist.org`_. - -**3.** `Install composer`_ if you don't already have it present on your system: - -**4.** Download the vendor libraries and generate the ``vendor/autoload.php`` file: - -.. code-block:: bash +.. tip:: - $ php composer.phar install + `Install Composer`_ if you don't have it already present on your system. + Depending on how you install, you may end up with a ``composer.phar`` + file in your directory. In that case, no worries! Your command line in that + case is ``php composer.phar require symfony/finder``. -**5.** Write your code: +**3.** Write your code! Once Composer has downloaded the component(s), all you need to do is include the ``vendor/autoload.php`` file that was generated by Composer. This file takes care of autoloading all of the libraries so that you can use them immediately:: - // File: src/script.php - - // update this to the path to the "vendor/" directory, relative to this file - require_once '../vendor/autoload.php'; - - use Symfony\Component\Finder\Finder; - - $finder = new Finder(); - $finder->in('../data/'); - - // ... - -.. tip:: - - If you want to use all of the Symfony2 Components, then instead of adding - them one by one: - - .. code-block:: json - - { - "require": { - "symfony/finder": "2.2.*", - "symfony/dom-crawler": "2.2.*", - "symfony/css-selector": "2.2.*" - } - } + // Project structure example: + // my_project/ + // data/ + // ... # Some project data + // src/ + // my_script.php # Main entry point + // vendor/ + // autoload.php # Autoloader generated by Composer + // ... # Packages downloaded by Composer - you can use: + // File example: src/my_script.php + // Autoloader relative path to this PHP file + require_once __DIR__.'/../vendor/autoload.php'; - .. code-block:: json + use Symfony\Component\Finder\Finder; - { - "require": { - "symfony/symfony": "2.2.*" - } - } + $finder = new Finder(); + $finder->in('../data/'); - This will include the Bundle and Bridge libraries, which you may not - actually need. + // rest of your PHP code... -Now What? +Now what? --------- -Now that the component is installed and autoloaded, read the specific component's +Now, the component is installed and autoloaded. Read the specific component's documentation to find out more about how to use it. And have fun! -.. _Composer: http://getcomposer.org -.. _Install composer: http://getcomposer.org/download/ -.. _packagist.org: https://packagist.org/ \ No newline at end of file +.. _Composer: https://getcomposer.org +.. _Install Composer: https://getcomposer.org/download/ diff --git a/components/validator.rst b/components/validator.rst new file mode 100644 index 00000000000..12c61507257 --- /dev/null +++ b/components/validator.rst @@ -0,0 +1,87 @@ +The Validator Component +======================= + + The Validator component provides tools to validate values following the + `JSR-303 Bean Validation specification`_. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/validator + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +.. seealso:: + + This article explains how to use the Validator features as an independent + component in any PHP application. Read the :doc:`/validation` article to + learn about how to validate data and entities in Symfony applications. + +The Validator component behavior is based on two concepts: + +* Constraints, which define the rules to be validated; +* Validators, which are the classes that contain the actual validation logic. + +The following example shows how to validate that a string is at least 10 +characters long:: + + use Symfony\Component\Validator\Constraints\Length; + use Symfony\Component\Validator\Constraints\NotBlank; + use Symfony\Component\Validator\Validation; + + $validator = Validation::createValidator(); + $violations = $validator->validate('Bernhard', [ + new Length(min: 10), + new NotBlank(), + ]); + + if (0 !== count($violations)) { + // there are errors, now you can show them + foreach ($violations as $violation) { + echo $violation->getMessage().'
'; + } + } + +The ``validate()`` method returns the list of violations as an object that +implements :class:`Symfony\\Component\\Validator\\ConstraintViolationListInterface`. +If you have lots of validation errors, you can filter them by error code:: + + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + + $violations = $validator->validate(/* ... */); + if (0 !== count($violations->findByCodes(UniqueEntity::NOT_UNIQUE_ERROR))) { + // handle this specific error (display some message, send an email, etc.) + } + +Retrieving a Validator Instance +------------------------------- + +The Validator object (that implements :class:`Symfony\\Component\\Validator\\Validator\\ValidatorInterface`) is the main access +point of the Validator component. To create a new instance of it, it's +recommended to use the :class:`Symfony\\Component\\Validator\\Validation` class:: + + use Symfony\Component\Validator\Validation; + + $validator = Validation::createValidator(); + +This ``$validator`` object can validate simple variables such as strings, numbers +and arrays, but it can't validate objects. To do so, configure the +``Validator`` as explained in the next sections. + +Learn More +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /components/validator/* + /validation + /validation/* + +.. _`JSR-303 Bean Validation specification`: https://jcp.org/en/jsr/detail?id=303 diff --git a/components/validator/metadata.rst b/components/validator/metadata.rst new file mode 100755 index 00000000000..782e1ee216f --- /dev/null +++ b/components/validator/metadata.rst @@ -0,0 +1,94 @@ +Metadata +======== + +The :class:`Symfony\\Component\\Validator\\Mapping\\ClassMetadata` class +represents and manages all the configured constraints on a given class. + +Properties +---------- + +The Validator component can validate public, protected or private properties. +The following example shows how to validate that the ``$firstName`` property of +the ``Author`` class has at least 3 characters:: + + // ... + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Author + { + private string $firstName; + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('firstName', new Assert\NotBlank()); + $metadata->addPropertyConstraint( + 'firstName', + new Assert\Length(min: 3) + ); + } + } + +Getters +------- + +Constraints can also be applied to the value returned by any public *getter* +method, which are the methods whose names start with ``get``, ``has`` or ``is``. +This feature allows validating your objects dynamically. + +Suppose that, for security reasons, you want to validate that a password field +doesn't match the first name of the user. First, create a public method called +``isPasswordSafe()`` to define this custom validation logic:: + + public function isPasswordSafe(): bool + { + return $this->firstName !== $this->password; + } + +Then, add the Validator component configuration to the class:: + + // ... + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Author + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue( + message: 'The password cannot match your first name', + )); + } + } + +Classes +------- + +Some constraints allow validating the entire object. For example, the +:doc:`Callback ` constraint is a generic +constraint that's applied to the class itself. + +Suppose that the class defines a ``validate()`` method to hold its custom +validation logic:: + + // ... + use Symfony\Component\Validator\Context\ExecutionContextInterface; + + public function validate(ExecutionContextInterface $context): void + { + // ... + } + +Then, add the Validator component configuration to the class:: + + // ... + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Author + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addConstraint(new Assert\Callback('validate')); + } + } diff --git a/components/validator/resources.rst b/components/validator/resources.rst new file mode 100644 index 00000000000..7d6cd0e8e5d --- /dev/null +++ b/components/validator/resources.rst @@ -0,0 +1,178 @@ +Loading Resources +================= + +The Validator component uses metadata to validate a value. This metadata defines +how a class, array or any other value should be validated. When validating a +class, the metadata is defined by the class itself. When validating simple values, +the metadata must be passed to the validation methods. + +Class metadata can be defined in a configuration file or in the class itself. +The Validator component collects that metadata using a set of loaders. + +.. seealso:: + + You'll learn how to define the metadata in :doc:`metadata`. + +The StaticMethodLoader +---------------------- + +The most basic loader is the +:class:`Symfony\\Component\\Validator\\Mapping\\Loader\\StaticMethodLoader`. +This loader gets the metadata by calling a static method of the class. The name +of the method is configured using the +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::addMethodMapping` +method of the validator builder:: + + use Symfony\Component\Validator\Validation; + + $validator = Validation::createValidatorBuilder() + ->addMethodMapping('loadValidatorMetadata') + ->getValidator(); + +In this example, the validation metadata is retrieved executing the +``loadValidatorMetadata()`` method of the class:: + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class User + { + protected string $name; + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('name', new Assert\NotBlank()); + $metadata->addPropertyConstraint('name', new Assert\Length( + min: 5, + max: 20, + )); + } + } + +.. tip:: + + Instead of calling ``addMethodMapping()`` multiple times to add several + method names, you can also use + :method:`Symfony\\Component\\Validator\\ValidatorBuilder::addMethodMappings` + to set an array of supported method names. + +The File Loaders +---------------- + +The component also provides two file loaders, one to load YAML files and one to +load XML files. Use +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::addYamlMapping` or +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::addXmlMapping` to +configure the locations of these files:: + + use Symfony\Component\Validator\Validation; + + $validator = Validation::createValidatorBuilder() + ->addYamlMapping('validator/validation.yaml') + ->getValidator(); + +.. note:: + + If you want to load YAML mapping files, then you will also need to install + :doc:`the Yaml component `. + +.. tip:: + + Just like with the method mappings, you can also use + :method:`Symfony\\Component\\Validator\\ValidatorBuilder::addYamlMappings` and + :method:`Symfony\\Component\\Validator\\ValidatorBuilder::addXmlMappings` + to configure an array of file paths. + +The AttributeLoader +------------------- + +The component provides an +:class:`Symfony\\Component\\Validator\\Mapping\\Loader\\AttributeLoader` to get +the metadata from the attributes of the class. For example:: + + use Symfony\Component\Validator\Constraints as Assert; + // ... + + class User + { + #[Assert\NotBlank] + protected string $name; + } + +To enable the attribute loader, call the +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::enableAttributeMapping` method. + +To disable the attribute loader after it was enabled, call +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::disableAttributeMapping`. + +Using Multiple Loaders +---------------------- + +The component provides a +:class:`Symfony\\Component\\Validator\\Mapping\\Loader\\LoaderChain` class to +execute several loaders sequentially in the same order they were defined: + +The ``ValidatorBuilder`` will already take care of this when you configure +multiple mappings:: + + use Symfony\Component\Validator\Validation; + + $validator = Validation::createValidatorBuilder() + ->enableAttributeMapping() + ->addMethodMapping('loadValidatorMetadata') + ->addXmlMapping('validator/validation.xml') + ->getValidator(); + +Caching +------- + +Using many loaders to load metadata from different places is convenient, but it +can slow down your application because each file needs to be parsed, validated +and converted into a :class:`Symfony\\Component\\Validator\\Mapping\\ClassMetadata` +instance. + +To solve this problem, call the :method:`Symfony\\Component\\Validator\\ValidatorBuilder::setMappingCache` +method of the Validator builder and pass your own caching class (which must +implement the PSR-6 interface ``Psr\Cache\CacheItemPoolInterface``):: + + use Symfony\Component\Validator\Validation; + + $validator = Validation::createValidatorBuilder() + // ... add loaders + ->setMappingCache(new SomePsr6Cache()) + ->getValidator(); + +.. note:: + + The loaders already use a singleton load mechanism. That means that the + loaders will only load and parse a file once and put that in a property, + which will then be used the next time it is asked for metadata. However, + the Validator still needs to merge all metadata of one class from every + loader when it is requested. + +Using a Custom MetadataFactory +------------------------------ + +All the loaders and the cache are passed to an instance of +:class:`Symfony\\Component\\Validator\\Mapping\\Factory\\LazyLoadingMetadataFactory`. +This class is responsible for creating a ``ClassMetadata`` instance from all the +configured resources. + +You can also use a custom metadata factory implementation by creating a class +which implements +:class:`Symfony\\Component\\Validator\\Mapping\\Factory\\MetadataFactoryInterface`. +You can set this custom implementation using +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::setMetadataFactory`:: + + use Acme\Validation\CustomMetadataFactory; + use Symfony\Component\Validator\Validation; + + $validator = Validation::createValidatorBuilder() + ->setMetadataFactory(new CustomMetadataFactory(...)) + ->getValidator(); + +.. warning:: + + 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. diff --git a/components/var_dumper.rst b/components/var_dumper.rst new file mode 100644 index 00000000000..c6966a692af --- /dev/null +++ b/components/var_dumper.rst @@ -0,0 +1,893 @@ +The VarDumper Component +======================= + + The VarDumper component provides mechanisms for extracting the state out of + any PHP variables. Built on top, it provides a better ``dump()`` function + that you can use instead of :phpfunction:`var_dump`. + +Installation +------------ + +.. code-block:: terminal + + $ composer require --dev symfony/var-dumper + +.. include:: /components/require_autoload.rst.inc + +.. note:: + + If using it inside a Symfony application, make sure that the DebugBundle has + been installed (or run ``composer require --dev symfony/debug-bundle`` to install it). + +.. _components-var-dumper-dump: + +The dump() Function +------------------- + +The VarDumper component creates a global ``dump()`` function that you can +use instead of e.g. :phpfunction:`var_dump`. By using it, you'll gain: + +* Per object and resource types specialized view to e.g. filter out + Doctrine internals while dumping a single proxy entity, or get more + insight on opened files with :phpfunction:`stream_get_meta_data`; +* Configurable output formats: HTML or colored command line output; +* Ability to dump internal references, either soft ones (objects or + resources) or hard ones (``=&`` on arrays or objects properties). + Repeated occurrences of the same object/array/resource won't appear + again and again anymore. Moreover, you'll be able to inspect the + reference structure of your data; +* Ability to operate in the context of an output buffering handler. + +For example:: + + require __DIR__.'/vendor/autoload.php'; + + // create a variable, which could be anything! + $someVar = ...; + + dump($someVar); + + // dump() returns the passed value, so you can dump an object and keep using it + dump($someObject)->someMethod(); + +By default, the output format and destination are selected based on your +current PHP SAPI: + +* On the command line (CLI SAPI), the output is written on ``STDOUT``. This + can be surprising to some because this bypasses PHP's output buffering + mechanism; +* On other SAPIs, dumps are written as HTML in the regular output. + +.. tip:: + + You can also select the output format explicitly defining the + ``VAR_DUMPER_FORMAT`` environment variable and setting its value to either + ``html``, ``cli`` or :ref:`server `. + +.. note:: + + If you want to catch the dump output as a string, please read the + :ref:`advanced section ` which contains examples of + it. + You'll also learn how to change the format or redirect the output to + wherever you want. + +.. tip:: + + In order to have the ``dump()`` function always available when running + any PHP code, you can install it globally on your computer: + + #. Run ``composer global require symfony/var-dumper``; + #. Add ``auto_prepend_file = ${HOME}/.composer/vendor/autoload.php`` + to your ``php.ini`` file; + #. From time to time, run ``composer global update symfony/var-dumper`` + to have the latest bug fixes. + +.. tip:: + + The VarDumper component also provides a ``dd()`` ("dump and die") helper + function. This function dumps the variables using ``dump()`` and + immediately ends the execution of the script (using :phpfunction:`exit`). + +.. _var-dumper-dump-server: + +The Dump Server +--------------- + +The ``dump()`` function outputs its contents in the same browser window or +console terminal as your own application. Sometimes mixing the real output +with the debug output can be confusing. That's why this component provides a +server to collect all the dumped data. + +Start the server with the ``server:dump`` command and whenever you call to +``dump()``, the dumped data won't be displayed in the output but sent to that +server, which outputs it to its own console or to an HTML file: + +.. code-block:: terminal + + # displays the dumped data in the console: + $ php bin/console server:dump + [OK] Server listening on tcp://0.0.0.0:9912 + + # stores the dumped data in a file using the HTML format: + $ php bin/console server:dump --format=html > dump.html + +Inside a Symfony application, the output of the dump server is configured with +the :ref:`dump_destination option ` of the +``debug`` package: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/debug.yaml + debug: + dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" + + .. code-block:: xml + + + + + + + + .. code-block:: php + + // config/packages/debug.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return static function (ContainerConfigurator $container): void { + $container->extension('debug', [ + 'dump_destination' => 'tcp://%env(VAR_DUMPER_SERVER)%', + ]); + }; + +Outside a Symfony application, use the :class:`Symfony\\Component\\VarDumper\\Dumper\\ServerDumper` class:: + + require __DIR__.'/vendor/autoload.php'; + + use Symfony\Component\VarDumper\Cloner\VarCloner; + use Symfony\Component\VarDumper\Dumper\CliDumper; + use Symfony\Component\VarDumper\Dumper\ContextProvider\CliContextProvider; + use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider; + use Symfony\Component\VarDumper\Dumper\HtmlDumper; + use Symfony\Component\VarDumper\Dumper\ServerDumper; + use Symfony\Component\VarDumper\VarDumper; + + $cloner = new VarCloner(); + $fallbackDumper = \in_array(\PHP_SAPI, ['cli', 'phpdbg']) ? new CliDumper() : new HtmlDumper(); + $dumper = new ServerDumper('tcp://127.0.0.1:9912', $fallbackDumper, [ + 'cli' => new CliContextProvider(), + 'source' => new SourceContextProvider(), + ]); + + VarDumper::setHandler(function (mixed $var) use ($cloner, $dumper): ?string { + return $dumper->dump($cloner->cloneVar($var)); + }); + +.. note:: + + The second argument of :class:`Symfony\\Component\\VarDumper\\Dumper\\ServerDumper` + is a :class:`Symfony\\Component\\VarDumper\\Dumper\\DataDumperInterface` instance + used as a fallback when the server is unreachable. The third argument are the + context providers, which allow to gather some info about the context in which the + data was dumped. The built-in context providers are: ``cli``, ``request`` and ``source``. + +Then you can use the following command to start a server out-of-the-box: + +.. code-block:: terminal + + $ ./vendor/bin/var-dump-server + [OK] Server listening on tcp://127.0.0.1:9912 + +.. _var-dumper-dump-server-format: + +Configuring the Dump Server with Environment Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you prefer to not modify the application configuration (e.g. to quickly debug +a project given to you) use the ``VAR_DUMPER_FORMAT`` env var. + +First, start the server as usual: + +.. code-block:: terminal + + $ ./vendor/bin/var-dump-server + +Then, run your code with the ``VAR_DUMPER_FORMAT=server`` env var by configuring +this value in the :ref:`.env file of your application `. For +console commands, you can also define this env var as follows: + +.. code-block:: terminal + + $ VAR_DUMPER_FORMAT=server [your-cli-command] + +.. note:: + + The host used by the ``server`` format is the one configured in the + ``VAR_DUMPER_SERVER`` env var or ``127.0.0.1:9912`` if none is defined. + If you prefer, you can also configure the host in the ``VAR_DUMPER_FORMAT`` + env var like this: ``VAR_DUMPER_FORMAT=tcp://127.0.0.1:1234``. + +DebugBundle and Twig Integration +-------------------------------- + +The DebugBundle allows greater integration of this component into Symfony +applications. + +Since generating (even debug) output in the controller or in the model +of your application may just break it by e.g. sending HTTP headers or +corrupting your view, the bundle configures the ``dump()`` function so that +variables are dumped in the web debug toolbar. + +But if the toolbar cannot be displayed because you e.g. called +``die()``/``exit()``/``dd()`` or a fatal error occurred, then dumps are written +on the regular output. + +In a Twig template, two constructs are available for dumping a variable. +Choosing between both is mostly a matter of personal taste, still: + +* ``{% dump foo.bar %}`` is the way to go when the original template output + shall not be modified: variables are not dumped inline, but in the web + debug toolbar; +* on the contrary, ``{{ dump(foo.bar) }}`` dumps inline and thus may or not + be suited to your use case (e.g. you shouldn't use it in an HTML + attribute or a ``'; + $urlValidator = new Constraints\UrlValidator(); + $urlConstraint = new Constraints\Url(); + + // The URL is wrong, so var_dump() should display an error, but it displays + // "null" instead because there is no context to build a validator violation + var_dump($urlValidator->validate($wrongUrl, $urlConstraint)); + +Reproducing Complex Bugs +------------------------ + +If the bug is related to the Symfony Framework or if it's too complex to create +a PHP script, it's better to reproduce the bug by creating a new project. To do so: + +#. Create a new project: + +.. code-block:: terminal + + $ composer create-project symfony/skeleton bug_app + +#. Add and commit the changes generated by Symfony. +#. Now you must add the minimum amount of code to reproduce the bug. This is the + trickiest part and it's explained a bit more later. +#. Add and commit your changes. +#. Create a `new repository`_ on GitHub (give it any name). +#. Follow the instructions on GitHub to add the ``origin`` remote to your local project + and push it. +#. Add a comment in your original issue report to share the URL of your forked + project (e.g. ``https://github.com/YOUR-GITHUB-USERNAME/symfony_issue_23567``) + and, if necessary, explain the steps to reproduce (e.g. "browse this URL", + "fill in this data in the form and submit it", etc.) + +Adding the Minimum Amount of Code Possible +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The key to create a bug reproducer is to solely focus on the feature that you +suspect is failing. For example, imagine that you suspect that the bug is related +to a route definition. Then, after creating your project: + +#. Don't edit any of the default Symfony configuration options. +#. Don't copy your original application code and don't use the same structure + of controllers, actions, etc. as in your original application. +#. Create a small controller and add your routing definition that shows the bug. +#. Don't create or modify any other file. +#. Install the :doc:`local web server ` provided by Symfony + and use the ``symfony server:start`` command to browse to the new route and + see if the bug appears or not. +#. If you can see the bug, you're done and you can already share the code with us. +#. If you can't see the bug, you must keep making small changes. For example, if + your original route was defined using XML, forget about the previous route + and define the route using XML instead. Or maybe your application + registers some event listeners and that's where the real bug is. In that case, + add an event listener that's similar to your real app to see if you can find + the bug. + +In short, the idea is to keep adding small and incremental changes to a new project +until you can reproduce the bug. + +.. _`new repository`: https://github.com/new diff --git a/contributing/code/security.rst b/contributing/code/security.rst index 5d895101380..ba8949971a4 100644 --- a/contributing/code/security.rst +++ b/contributing/code/security.rst @@ -1,28 +1,54 @@ 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 -------------------------- If you think that you have found a security issue in Symfony, don't use the -mailing-list or the bug tracker and don't publish it publicly. Instead, all -security issues must be sent to **security [at] symfony.com**. Emails sent to -this address are forwarded to the Symfony core-team private mailing-list. +bug tracker and don't publish it publicly. Instead, all security issues must +be sent to **security [at] symfony.com**. Emails sent to this address are +forwarded to the Symfony core team private mailing-list. + +The following issues are not considered security issues and should be handled +as regular bug fixes (if you have any doubts, don't hesitate to send us an +email for confirmation): + +* Any security issues found in debug tools that must never be enabled in + production (including the web profiler or anything enabled when ``APP_DEBUG`` + is set to ``true`` or ``APP_ENV`` set to anything but ``prod``); + +* Any security issues found in classes provided to help for testing that should + never be used in production (like for instance mock classes that contain + ``Mock`` in their name or classes in the ``Test`` namespace); + +* Any fix that can be classified as **security hardening** like route + enumeration, login throttling bypasses, denial of service attacks, timing + attacks, or lack of ``SensitiveParameter`` attributes. + +In any case, the core team has the final decision on which issues are +considered security vulnerabilities. + +Security Bug Bounties +--------------------- + +Symfony is an Open-Source project where most of the work is done by volunteers. +We appreciate that developers are trying to find security issues in Symfony and +report them responsibly, but we are currently unable to pay bug bounties. Resolving Process ----------------- For each report, we first try to confirm the vulnerability. When it is -confirmed, the core-team works on a solution following these steps: +confirmed, the core team works on a solution following these steps: -1. Send an acknowledgement to the reporter; -2. Work on a patch; -3. Get a CVE identifier from mitre.org; -4. Write a security announcement for the official Symfony `blog`_ about the +#. Send an acknowledgment to the reporter; +#. Work on a patch; +#. Get a CVE identifier from `mitre.org`_; +#. Write a security announcement for the official Symfony `blog`_ about the vulnerability. This post should contain the following information: * a title that always include the "Security release" string; @@ -32,12 +58,14 @@ confirmed, the core-team works on a solution following these steps: * how to patch/upgrade/workaround affected applications; * the CVE identifier; * credits. -5. Send the patch and the announcement to the reporter for review; -6. Apply the patch to all maintained versions of Symfony; -7. Package new versions for all affected versions; -8. Publish the post on the official Symfony `blog`_ (it must also be added to +#. Send the patch and the announcement to the reporter for review; +#. Apply the patch to all maintained versions of Symfony; +#. Package new versions for all affected versions; +#. Publish the post on the official Symfony `blog`_ (it must also be added to the "`Security Advisories`_" category); -9. Update the security advisory list (see below). +#. Update the public `security advisories database`_ maintained by the + FriendsOfPHP organization and which is used by + :ref:`the check:security command `. .. note:: @@ -48,32 +76,134 @@ confirmed, the core-team works on a solution following these steps: While we are working on a patch, please do not reveal the issue publicly. +.. note:: + + The resolution takes anywhere between a couple of days to a month depending + on its complexity and the coordination with the downstream projects (see + next paragraph). + +Collaborating with Downstream Open-Source Projects +-------------------------------------------------- + +As Symfony is used by many large Open-Source projects, we standardized the way +the Symfony security team collaborates on security issues with downstream +projects. The process works as follows: + +#. After the Symfony security team has acknowledged a security issue, it + immediately sends an email to the downstream project security teams to + inform them of the issue; + +#. The Symfony security team creates a private Git repository to ease the + collaboration on the issue and access to this repository is given to the + Symfony security team, to the Symfony contributors that are impacted by + the issue, and to one representative of each downstream projects; + +#. All people with access to the private repository work on a solution to + solve the issue via pull requests, code reviews, and comments; + +#. Once the fix is found, all involved projects collaborate to find the best + date for a joint release (there is no guarantee that all releases will + be at the same time but we will try hard to make them at about the same + time). When the issue is not known to be exploited in the wild, a period + of two weeks is considered a reasonable amount of time. + +The list of downstream projects participating in this process is kept as small +as possible in order to better manage the flow of confidential information +prior to disclosure. As such, projects are included at the sole discretion of +the Symfony security team. + +As of today, the following projects have validated this process and are part +of the downstream projects included in this process: + +* Drupal (releases typically happen on Wednesdays) +* eZPublish + +Issue Severity +-------------- + +In order to determine the severity of a security issue we take into account +the complexity of any potential attack, the impact of the vulnerability and +also how many projects it is likely to affect. This score out of 15 is then +converted into a level of: Low, Medium, High, Critical, or Exceptional. + +Attack Complexity +~~~~~~~~~~~~~~~~~ + +*Score of between 1 and 5 depending on how complex it is to exploit the +vulnerability* + +* 4 - 5 Basic: attacker must follow a set of simple steps +* 2 - 3 Complex: attacker must follow non-intuitive steps with a high level + of dependencies +* 1 - 2 High: A successful attack depends on conditions beyond the attacker's + control. That is, a successful attack cannot be accomplished at will, but + requires the attacker to invest in some measurable amount of effort in + preparation or execution against the vulnerable component before a successful + attack can be expected. + +Impact +~~~~~~ + +*Scores from the following areas are added together to produce a score. The +score for Impact is capped at 6. Each area is scored between 0 and 4.* + +* Integrity: Does this vulnerability cause non-public data to be accessible? + If so, does the attacker have control over the data disclosed? (0-4) +* Disclosure: Can this exploit allow system data (or data handled by the + system) to be compromised? If so, does the attacker have control over + modification? (0-4) +* Code Execution: Does the vulnerability allow arbitrary code to be executed + on an end-users system, or the server that it runs on? (0-4) +* Availability: Is the availability of a service or application affected? Is + it reduced availability or total loss of availability of a service / + application? Availability includes networked services (e.g. databases) or + resources such as consumption of network bandwidth, processor cycles, or + disk space. (0-4) + +Affected Projects +~~~~~~~~~~~~~~~~~ + +*Scores from the following areas are added together to produce a score. The +score for Affected Projects is capped at 4.* + +* Will it affect some or all using a component? (1-2) +* Is the usage of the component that would cause such a thing already + considered bad practice? (0-1) +* How common/popular is the component (e.g. Console vs HttpFoundation vs + Lock)? (0-2) +* Are a number of well-known open source projects using Symfony affected + that requires coordinated releases? (0-1) + +Score Totals +~~~~~~~~~~~~ + +* Attack Complexity: 1 - 5 +* Impact: 1 - 6 +* Affected Projects: 1 - 4 + +Severity levels +~~~~~~~~~~~~~~~ + +* Low: 1 - 5 +* Medium: 6 - 10 +* High: 11 - 12 +* Critical: 13 - 14 +* Exceptional: 15 + Security Advisories ------------------- -This section indexes security vulnerabilities that were fixed in Symfony -releases, starting from Symfony 1.0.0: - -* January 17, 2013: `Security release: Symfony 2.0.22 and 2.1.7 released `_ (`CVE-2013-1348 `_ and `CVE-2013-1397 `_) -* December 20, 2012: `Security release: Symfony 2.0.20 and 2.1.5 `_ (`CVE-2012-6431 `_ and `CVE-2012-6432 `_) -* November 29, 2012: `Security release: Symfony 2.0.19 and 2.1.4 `_ -* November 25, 2012: `Security release: symfony 1.4.20 released `_ (`CVE-2012-5574 `_) -* August 28, 2012: `Security Release: Symfony 2.0.17 released `_ -* May 30, 2012: `Security Release: symfony 1.4.18 released `_ (`CVE-2012-2667 `_) -* February 24, 2012: `Security Release: Symfony 2.0.11 released `_ -* November 16, 2011: `Security Release: Symfony 2.0.6 `_ -* March 21, 2011: `symfony 1.3.10 and 1.4.10: security releases `_ -* June 29, 2010: `Security Release: symfony 1.3.6 and 1.4.6 `_ -* May 31, 2010: `symfony 1.3.5 and 1.4.5 `_ -* February 25, 2010: `Security Release: 1.2.12, 1.3.3 and 1.4.3 `_ -* February 13, 2010: `symfony 1.3.2 and 1.4.2 `_ -* April 27, 2009: `symfony 1.2.6: Security fix `_ -* October 03, 2008: `symfony 1.1.4 released: Security fix `_ -* May 14, 2008: `symfony 1.0.16 is out `_ -* April 01, 2008: `symfony 1.0.13 is out `_ -* March 21, 2008: `symfony 1.0.12 is (finally) out ! `_ -* June 25, 2007: `symfony 1.0.5 released (security fix) `_ - -.. _Git repository: https://github.com/symfony/symfony -.. _blog: http://symfony.com/blog/ -.. _Security Advisories: http://symfony.com/blog/category/security-advisories +.. tip:: + + You can check your Symfony application for known security vulnerabilities + 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 +.. _blog: https://symfony.com/blog/ +.. _`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/code/stack_trace.rst b/contributing/code/stack_trace.rst new file mode 100644 index 00000000000..6fd6987d4e3 --- /dev/null +++ b/contributing/code/stack_trace.rst @@ -0,0 +1,189 @@ +Getting a Stack Trace +===================== + +When :doc:`reporting a bug ` for an +exception or a wrong behavior in code, it is crucial that you provide +one or several stack traces. To understand why, you first have to +understand what a stack trace is, and how it can be useful to you as a +developer, and also to library maintainers. + +Anatomy of a Stack Trace +------------------------ + +A stack trace is called that way because it allows one to see a trail of +function calls leading to a point in code since the beginning of the +program. That point is not necessarily an exception. For instance, you +can use the native PHP function ``debug_print_backtrace()`` to get such +a trace. For each line in the trace, you get a file and a function or +method call, and the line number for that call. This is often of great +help for understanding the flow of your program and how it can end up in +unexpected places, such as lines of code where exceptions are thrown. + +Stack Traces and Exceptions +--------------------------- + +In PHP, every exception comes with its own stack trace, which is +displayed by default if the exception is not caught. When using Symfony, +such exceptions go through a custom exception handler, which enhances +them in various ways before displaying them according to the current +Server API (CLI or not). +This means a better way to get a stack trace when you do not need the +program to continue is to throw an exception, as follows: +``throw new \Exception();`` + +Nested Exceptions +----------------- + +When applications get bigger, complexity is often tackled with layers of +architecture that need to be kept separate. For instance, if you have a +web application that makes a call to a remote API, it might be good to +wrap exceptions thrown when making that call with exceptions that have +special meaning in your domain, and to build appropriate HTTP exceptions +from those. Exceptions can be nested by using the ``$previous`` +argument that appears in the signature of the ``Exception`` class: +``public __construct ([ string $message = "" [, int $code = 0 [, Throwable $previous = NULL ]]] )`` +This means that sometimes, when you get an exception from an +application, you might actually get several of them. + +What to look for in a Stack Trace +--------------------------------- + +When using a library, you will call code that you did not write. When +using a framework, it is the opposite: because you follow the +conventions of the framework, `the framework finds your code and calls +it `_, and does +things for you beforehand, like routing or access control. +Symfony being both a framework and library of components, it calls your +code and then your code might call it. This means you will always have +at least 2 parts, very often 3 in your stack traces when using Symfony: +a part that starts in one of the entry points of the framework +(``bin/console`` or ``public/index.php`` in most cases), and ends when +reaching your code, most times in a command or in a controller found under +``src``. Then, either the exception is thrown in your code or in +libraries you call. If it is the latter, there should be a third part in +the stack trace with calls made in files under ``vendor``. Before +landing in that directory, code goes through numerous review processes +and CI pipelines, which means it should be less likely to be the source +of the issue than code from your application, so it is important that +you focus first on lines starting with ``src``, and look for anything +suspicious or unexpected, like method calls that are not supposed to +happen. + +Next, you can have a look at what packages are involved. Files under +``vendor`` are organized by Composer in the following way: +``vendor/acme/router`` where ``acme`` is the vendor, ``router`` the +library and ``acme/router`` the Composer package. If you plan on +reporting the bug, make sure to report it to the library throwing the +exception. ``composer home acme/router`` should lead you to the right +place for that. As Symfony is a mono-repository, use ``composer home +symfony/symfony`` when reporting a bug for any component. + +Getting Stack Traces with Symfony +--------------------------------- + +Now that we have all this in mind, let us see how to get a stack trace +with Symfony. + +Stack Traces in your Web Browser +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Several things need to be paid attention to when picking a stack trace +from your development environment through a web browser: + +1. Are there several exceptions? If yes, the most interesting one is + often exception 1/n which, is shown *last* in the default exception page + (it is the one marked as ``exception [1/2]`` in the below example). +2. Under the "Stack Traces" tab, you will find exceptions in plain + text, so that you can easily share them in e.g. bug reports. Make + sure to **remove any sensitive information** before doing so. +3. You may notice there is a logs tab too; this tab does not have to do + with stack traces, it only contains logs produced in arbitrary places + in your application. They may or may not relate to the exception you + are getting, but are not what the term "stack trace" refers to. + +.. image:: /_images/contributing/code/stack-trace.gif + :alt: The default Symfony exception page with the "Exceptions", "Logs" and "Stack Traces" tabs. + :class: with-browser + +Since stack traces may contain sensitive data, they should not be +exposed in production. Getting a stack trace from your production +environment, although more involving, is still possible with solutions +that include but are not limited to sending them to an email address +with Monolog. + +Stack Traces in the CLI +~~~~~~~~~~~~~~~~~~~~~~~ + +Exceptions might occur when running a Symfony command. By default, only +the message is shown because it is often enough to understand what is +going on: + +.. code-block:: terminal + + $ php bin/console debug:exception + + + Command "debug:exception" is not defined. + + Did you mean one of these? + debug:autowiring + debug:config + debug:container + debug:event-dispatcher + debug:form + debug:router + debug:translation + debug:twig + + +If that is not the case, you can obtain a stack trace by increasing the +:doc:`verbosity level ` with ``--verbose``: + +.. code-block:: terminal + + $ php bin/console --verbose debug:exception + + In Application.php line 644: + + [Symfony\Component\Console\Exception\CommandNotFoundException] + Command "debug:exception" is not defined. + + Did you mean one of these? + debug:autowiring + debug:config + debug:container + debug:event-dispatcher + debug:form + debug:router + debug:translation + debug:twig + + + Exception trace: + at /app/vendor/symfony/console/Application.php:644 + Symfony\Component\Console\Application->find() at /app/vendor/symfony/framework-bundle/Console/Application.php:116 + Symfony\Bundle\FrameworkBundle\Console\Application->find() at /app/vendor/symfony/console/Application.php:228 + Symfony\Component\Console\Application->doRun() at /app/vendor/symfony/framework-bundle/Console/Application.php:82 + Symfony\Bundle\FrameworkBundle\Console\Application->doRun() at /app/vendor/symfony/console/Application.php:140 + Symfony\Component\Console\Application->run() at /app/bin/console:42 + +Stack Traces and API Calls +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When getting an exception from an API, you might not get a stack trace, +or it might be displayed in a way that is not suitable for sharing. +Luckily, when in the dev environment, you can obtain a plain text stack +trace by using the profiler. To find the profile, you can have a look +at the ``X-Debug-Token-Link`` response headers: + +.. code-block:: terminal + + $ curl --head http://localhost:8000/api/posts/1 + … more headers + X-Debug-Token: 110e1e + X-Debug-Token-Link: http://localhost:8000/_profiler/110e1e + X-Robots-Tag: noindex + X-Previous-Debug-Token: 209101 + +Following that link will lead you to a page very similar to the one +described above in `Stack Traces in your Web Browser`_. diff --git a/contributing/code/standards.rst b/contributing/code/standards.rst index 61a2ed6b8df..ebfde7dfab4 100644 --- a/contributing/code/standards.rst +++ b/contributing/code/standards.rst @@ -1,23 +1,33 @@ Coding Standards ================ -When contributing code to Symfony2, you must follow its coding standards. To -make a long story short, here is the golden rule: **Imitate the existing -Symfony2 code**. Most open-source Bundles and libraries used by Symfony2 also -follow the same guidelines, and you should too. +Symfony code is contributed by thousands of developers around the world. To make +every piece of code look and feel familiar, Symfony defines some coding standards +that all contributions must follow. -Remember that the main advantage of standards is that every piece of code -looks and feels familiar, it's not about this or that being more readable. +These Symfony coding standards are based on the `PSR-1`_, `PSR-2`_, `PSR-4`_ +and `PSR-12`_ standards, so you may already know most of them. -Symfony follows the standards defined in the `PSR-0`_, `PSR-1`_ and `PSR-2`_ -documents. +Making your Code Follow the Coding Standards +-------------------------------------------- -Since a picture - or some code - is worth a thousand words, here's a short -example containing most features described below: +Instead of reviewing your code manually, Symfony makes it simple to ensure that +your contributed code matches the expected code syntax. First, install the +`PHP CS Fixer tool`_ and then, run this command to fix any problem: -.. code-block:: html+php +.. code-block:: terminal - fooBar = $this->transformText($dummy); } /** - * @param string $dummy Some argument description - * @param array $options + * @deprecated + */ + public function someDeprecatedMethod(): string + { + trigger_deprecation('symfony/package-name', '5.1', 'The %s() method is deprecated, use Acme\Baz::someMethod() instead.', __METHOD__); + + return Baz::someMethod(); + } + + /** + * Transforms the input given as the first argument. + * + * @param $options an options collection to be used within the transformation * - * @return string|null Transformed input + * @throws \RuntimeException when an invalid option is provided */ - private function transformText($dummy, $options = array()) + private function transformText(bool|string $dummy, array $options = []): ?string { - $mergedOptions = array_merge( - $options, - array( - 'some_default' => 'values', - 'another_default' => 'more values', - ) - ); + $defaultOptions = [ + 'some_default' => 'values', + 'another_default' => 'more values', + ]; + + foreach ($options as $name => $value) { + if (!array_key_exists($name, $defaultOptions)) { + throw new \RuntimeException(sprintf('Unrecognized option "%s"', $name)); + } + } + + $mergedOptions = array_merge($defaultOptions, $options); if (true === $dummy) { - return; + return 'something'; } - if ('string' === $dummy) { + + if (\is_string($dummy)) { if ('values' === $mergedOptions['some_default']) { - $dummy = substr($dummy, 0, 5); - } else { - $dummy = ucwords($dummy); + return substr($dummy, 0, 5); } - } else { - throw new \RuntimeException(sprintf('Unrecognized dummy option "%s"', $dummy)); + + return ucwords($dummy); } - return $dummy; + return null; + } + + /** + * Performs some basic operations for a given value. + */ + private function performOperations(mixed $value = null, bool $theSwitch = false): void + { + if (!$theSwitch) { + return; + } + + $this->qux->doFoo($value); + $this->qux->doBar($value); } } Structure ---------- +~~~~~~~~~ * Add a single space after each comma delimiter; -* Add a single space around operators (``==``, ``&&``, ...); +* Add a single space around binary operators (``==``, ``&&``, ...), with + the exception of the concatenation (``.``) operator; + +* Place unary operators (``!``, ``--``, ...) adjacent to the affected variable; -* Add a comma after each array item in a multi-line array, even after the +* Always use `identical comparison`_ unless you need type juggling; + +* Use `Yoda conditions`_ when checking a variable against an expression to avoid + an accidental assignment inside the condition statement (this applies to ``==``, + ``!=``, ``===``, and ``!==``); + +* Add a comma after each array item in a multi-line array, even after the last one; * Add a blank line before ``return`` statements, unless the return is alone inside a statement-group (like an ``if`` statement); +* Use ``return null;`` when a function explicitly returns ``null`` values and + use ``return;`` when the function returns ``void`` values; + +* Do not add the ``void`` return type to methods in tests; + * Use braces to indicate control structure body regardless of the number of statements it contains; * Define one class per file - this does not apply to private helper classes that are not intended to be instantiated from the outside and thus are not - concerned by the `PSR-0`_ standard; + concerned by the `PSR-0`_ and `PSR-4`_ autoload standards; + +* Declare the class inheritance and all the implemented interfaces on the same + line as the class name; * Declare class properties before methods; -* Declare public methods first, then protected ones and finally private ones; +* Declare public methods first, then protected ones and finally private ones. + The exceptions to this rule are the class constructor and the ``setUp()`` and + ``tearDown()`` methods of PHPUnit tests, which must always be the first methods + to increase readability; + +* Declare all the arguments on the same line as the method/function name, no + matter how many arguments there are. The only exception are constructor methods + using `constructor property promotion`_, where each parameter must be on a new + line with `trailing comma`_; * Use parentheses when instantiating classes regardless of the number of arguments the constructor has; -* Exception message strings should be concatenated using :phpfunction:`sprintf`. +* Exception and error message strings must be concatenated using :phpfunction:`sprintf`; + +* Exception and error messages must not contain backticks, + even when referring to a technical element (such as a method or variable name). + Double quotes must be used at all time: + + .. code-block:: diff + + - Expected `foo` option to be one of ... + + Expected "foo" option to be one of ... + +* Exception and error messages must start with a capital letter and finish with a dot ``.``; + +* Exception, error and deprecation messages containing a class name must + use ``get_debug_type()`` instead of ``::class`` to retrieve it: + + .. code-block:: diff + + - throw new \Exception(sprintf('Command "%s" failed.', $command::class)); + + throw new \Exception(sprintf('Command "%s" failed.', get_debug_type($command))); + +* Do not use ``else``, ``elseif``, ``break`` after ``if`` and ``case`` conditions + which return or throw something; + +* Do not use spaces around ``[`` offset accessor and before ``]`` offset accessor; + +* Add a ``use`` statement for every class that is not part of the global namespace; + +* When PHPDoc tags like ``@param`` or ``@return`` include ``null`` and other + types, always place ``null`` at the end of the list of types. Naming Conventions ------------------- +~~~~~~~~~~~~~~~~~~ + +* Use `camelCase`_ for PHP variables, function and method names, arguments + (e.g. ``$acceptableContentTypes``, ``hasSession()``); -* Use camelCase, not underscores, for variable, function and method - names, arguments; +* Use `snake_case`_ for configuration parameters, route names and Twig template + variables (e.g. ``framework.csrf_protection``, ``http_status_code``); -* Use underscores for option names and parameter names; +* Use SCREAMING_SNAKE_CASE for constants (e.g. ``InputArgument::IS_ARRAY``); -* Use namespaces for all classes; +* Use `UpperCamelCase`_ for enumeration cases (e.g. ``InputArgumentMode::IsArray``); -* Prefix abstract classes with ``Abstract``. Please note some early Symfony2 classes - do not follow this convention and have not been renamed for backward compatibility - reasons. However all new abstract classes must follow this naming convention; +* Use namespaces for all PHP classes, interfaces, traits and enums and + `UpperCamelCase`_ for their names (e.g. ``ConsoleLogger``); + +* Prefix all abstract classes with ``Abstract`` except PHPUnit ``*TestCase``. + Please note some early Symfony classes do not follow this convention and + have not been renamed for backward compatibility reasons. However, all new + abstract classes must follow this naming convention; * Suffix interfaces with ``Interface``; * Suffix traits with ``Trait``; +* Don't use a dedicated suffix for classes or enumerations (e.g. like ``Class`` + or ``Enum``), except for the cases listed below. + * Suffix exceptions with ``Exception``; -* Use alphanumeric characters and underscores for file names; +* Prefix PHP attributes that relate to service configuration with ``As`` + (e.g. ``#[AsCommand]``, ``#[AsEventListener]``, etc.); + +* Prefix PHP attributes that relate to controller arguments with ``Map`` + (e.g. ``#[MapEntity]``, ``#[MapCurrentUser]``, etc.); + +* Use UpperCamelCase for naming PHP files (e.g. ``EnvVarProcessor.php``) and + snake case for naming Twig templates and web assets (``section_layout.html.twig``, + ``index.scss``); + +* For type-hinting in PHPDocs and casting, use ``bool`` (instead of ``boolean`` + or ``Boolean``), ``int`` (instead of ``integer``), ``float`` (instead of + ``double`` or ``real``); * Don't forget to look at the more verbose :doc:`conventions` document for more subjective naming considerations. +.. _service-naming-conventions: + +Service Naming Conventions +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* A service name must be the same as the fully qualified class name (FQCN) of + its class (e.g. ``App\EventSubscriber\UserSubscriber``); + +* If there are multiple services for the same class, use the FQCN for the main + service and use lowercase and underscored names for the rest of services. + Optionally divide them in groups separated with dots (e.g. + ``something.service_name``, ``fos_user.something.service_name``); + +* Use lowercase letters for parameter names (except when referring + to environment variables with the ``%env(VARIABLE_NAME)%`` syntax); + +* Add class aliases for public services (e.g. alias ``Symfony\Component\Something\ClassName`` + to ``something.service_name``). + Documentation -------------- +~~~~~~~~~~~~~ + +* Add PHPDoc blocks for classes, methods, and functions only when they add + relevant information that does not duplicate the name, native type + declaration or context (e.g. ``instanceof`` checks); + +* Only use annotations and types defined in `the PHPDoc reference`_. In + order to improve types for static analysis, the following annotations are + also allowed: + + * `Generics`_, with the exception of ``@template-covariant``. + * `Conditional return types`_ using the vendor-prefixed ``@psalm-return``; + * `Class constants`_; + * `Callable types`_; + +* Group annotations together so that annotations of the same type immediately + follow each other, and annotations of a different type are separated by a + single blank line; -* Add PHPDoc blocks for all classes, methods, and functions; +* Omit the ``@return`` annotation if the method does not return anything; -* Omit the ``@return`` tag if the method does not return anything; +* Don't use one-line PHPDoc blocks on classes, methods and functions, even + when they contain just one annotation (e.g. don't put ``/** {@inheritdoc} */`` + in a single line); -* The ``@package`` and ``@subpackage`` annotations are not used. +* When adding a new class or when making significant changes to an existing class, + an ``@author`` tag with personal contact information may be added, or expanded. + Please note it is possible to have the personal contact information updated or + removed per request to the :doc:`core team `. License -------- +~~~~~~~ * Symfony is released under the MIT license, and the license block has to be present at the top of every PHP file, before the namespace. -.. _`PSR-0`: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md -.. _`PSR-1`: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md -.. _`PSR-2`: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md +.. _`PHP CS Fixer tool`: https://cs.symfony.com/ +.. _`PSR-0`: https://www.php-fig.org/psr/psr-0/ +.. _`PSR-1`: https://www.php-fig.org/psr/psr-1/ +.. _`PSR-2`: https://www.php-fig.org/psr/psr-2/ +.. _`PSR-4`: https://www.php-fig.org/psr/psr-4/ +.. _`PSR-12`: https://www.php-fig.org/psr/psr-12/ +.. _`identical comparison`: https://www.php.net/manual/en/language.operators.comparison.php +.. _`Yoda conditions`: https://en.wikipedia.org/wiki/Yoda_conditions +.. _`camelCase`: https://en.wikipedia.org/wiki/Camel_case +.. _`UpperCamelCase`: https://en.wikipedia.org/wiki/Camel_case +.. _`snake_case`: https://en.wikipedia.org/wiki/Snake_case +.. _`constructor property promotion`: https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion +.. _`trailing comma`: https://wiki.php.net/rfc/trailing_comma_in_parameter_list +.. _`the PHPDoc reference`: https://docs.phpdoc.org/3.0/guide/references/phpdoc/index.html +.. _`Conditional return types`: https://psalm.dev/docs/annotating_code/type_syntax/conditional_types/ +.. _`Class constants`: https://psalm.dev/docs/annotating_code/type_syntax/value_types/#regular-class-constants +.. _`Callable types`: https://psalm.dev/docs/annotating_code/type_syntax/callable_types/ +.. _`Generics`: https://psalm.dev/docs/annotating_code/templated_annotations/ diff --git a/contributing/code/tests.rst b/contributing/code/tests.rst index 873f3c382eb..060e3eda02b 100644 --- a/contributing/code/tests.rst +++ b/contributing/code/tests.rst @@ -1,119 +1,71 @@ -Running Symfony2 Tests -====================== +.. _running-symfony2-tests: -Before submitting a :doc:`patch ` for inclusion, you need to run the -Symfony2 test suite to check that you have not broken anything. +Running Symfony Tests +===================== -PHPUnit -------- +The Symfony project uses a CI (Continuous Integration) service which automatically runs tests +for any submitted :doc:`patch `. If the new code breaks any test, +the pull request will show an error message with a link to the full error details. -To run the Symfony2 test suite, `install`_ PHPUnit 3.6.4 or later first: +In any case, it's a good practice to run tests locally before submitting a +:doc:`patch ` for inclusion, to check that you have not broken anything. -.. code-block:: bash +.. _phpunit: +.. _dependencies_optional: - $ pear config-set auto_discover 1 - $ pear install pear.phpunit.de/PHPUnit +Before Running the Tests +------------------------ -Dependencies (optional) ------------------------ +To run the Symfony test suite, install the external dependencies used during the +tests, such as Doctrine, Twig and Monolog. To do so, +`install Composer`_ and execute the following: -To run the entire test suite, including tests that depend on external -dependencies, Symfony2 needs to be able to autoload them. By default, they are -autoloaded from `vendor/` under the main root directory (see -`autoload.php.dist`). +.. code-block:: terminal -The test suite needs the following third-party libraries: + $ composer update -* Doctrine -* Swiftmailer -* Twig -* Monolog - -To install them all, use `Composer`_: - -Step 1: Get `Composer`_ - -.. code-block:: bash - - curl -s http://getcomposer.org/installer | php - -Make sure you download ``composer.phar`` in the same folder where -the ``composer.json`` file is located. - -Step 2: Install vendors - -.. code-block:: bash - - $ php composer.phar --dev install - -.. note:: - - Note that the script takes some time to finish. - -.. note:: - - If you don't have ``curl`` installed, you can also just download the ``installer`` - file manually at http://getcomposer.org/installer. Place this file into your - project and then run: - - .. code-block:: bash - - $ php installer - $ php composer.phar --dev install +.. tip:: -After installation, you can update the vendors to their latest version with -the follow command: + Dependencies might fail to update and in this case Composer might need you to + tell it what Symfony version you are working on. + To do so set ``COMPOSER_ROOT_VERSION`` variable, e.g.: -.. code-block:: bash + .. code-block:: terminal - $ php composer.phar --dev update + $ COMPOSER_ROOT_VERSION=7.2.x-dev composer update -Running -------- +.. _running: -First, update the vendors (see above). +Running the Tests +----------------- -Then, run the test suite from the Symfony2 root directory with the following +Then, run the test suite from the Symfony root directory with the following command: -.. code-block:: bash +.. code-block:: terminal - $ phpunit + $ php ./phpunit symfony -The output should display `OK`. If not, you need to figure out what's going on -and if the tests are broken because of your modifications. +The output should display ``OK``. If not, read the reported errors to figure out +what's going on and if the tests are broken because of the new code. .. tip:: - If you want to test a single component type its path after the `phpunit` - command, e.g.: - - .. code-block:: bash - - $ phpunit src/Symfony/Component/Finder/ - -.. tip:: - - Run the test suite before applying your modifications to check that they - run fine on your configuration. - -Code Coverage -------------- - -If you add a new feature, you also need to check the code coverage by using -the `coverage-html` option: - -.. code-block:: bash + The entire Symfony suite can take up to several minutes to complete. If you + want to test a single component, type its path after the ``phpunit`` command, + e.g.: - $ phpunit --coverage-html=cov/ + .. code-block:: terminal -Check the code coverage by opening the generated `cov/index.html` page in a -browser. + $ php ./phpunit src/Symfony/Component/Finder/ .. tip:: - The code coverage only works if you have XDebug enabled and all - dependencies installed. + On Windows, install the `Cmder`_, `ConEmu`_, `ANSICON`_ or `Mintty`_ free applications + to see colored test results. -.. _install: http://www.phpunit.de/manual/current/en/installation.html -.. _`Composer`: http://getcomposer.org/ +.. _`install Composer`: https://getcomposer.org/download/ +.. _Cmder: https://cmder.app/ +.. _ConEmu: https://conemu.github.io/ +.. _ANSICON: https://github.com/adoxa/ansicon/releases +.. _Mintty: https://mintty.github.io/ diff --git a/contributing/code_of_conduct/care_team.rst b/contributing/code_of_conduct/care_team.rst new file mode 100644 index 00000000000..1b15850da39 --- /dev/null +++ b/contributing/code_of_conduct/care_team.rst @@ -0,0 +1,60 @@ +CARE Team +========= + +Our Pledge +---------- + +In the interest of fostering an open and welcoming environment, the "Code of +Conduct Active Response Ensurers", or CARE team, pledge to ensure that the +spirit of the :doc:`Code of Conduct ` +is respected. Our main priority is to ensure the safety of our community members. +The second goal is to help educate the community as a whole to be aware of the +Code of Conduct and how to help implement its spirit throughout the community. +In case these goals conflict, we will prioritize safety of community members +over all other goals. + +If you think there is or has been a violation to the Code of Conduct please contact +the CARE team or if you prefer contact only individual members of the CARE team. + +Members +------- + +Here are all the members of the CARE team (sorted alphabetically by surname). +You can contact any of them directly using the contact details below or you can +also contact all of them at once by emailing ** care@symfony.com **. + +* **Timo Bakx** + + * *E-mail*: timobakx [at] gmail.com + * *Twitter*: `@TimoBakx `_ + * *SymfonyConnect*: `timobakx `_ + * *SymfonySlack*: `@Timo Bakx `_ + +* **Zan Baldwin** + + * *E-mail*: hello [at] zanbaldwin.com + * *Twitter*: `@ZanBaldwin `_ + * *SymfonyConnect*: `zanbaldwin `_ + * *SymfonySlack*: `@Zan `_ + +* **Valentine Boineau** + + * *E-mail*: valentine.boineau [at] gmail.com + * *Twitter*: `@BoineauV `_ + * *SymfonyConnect*: `valentineboineau `_ + * *SymfonySlack*: `@Valentine `_ + +* **Tobias Nyholm** + + * *E-mail*: tobias.nyholm [at] gmail.com + * *Twitter*: `@tobiasnyholm `_ + * *SymfonyConnect*: `tobias `_ + * *SymfonySlack*: `@Tobias Nyholm `_ + +About the CARE Team +------------------- + +The :doc:`Symfony project leader ` appoints the CARE +team with candidates they see fit. The CARE team will consist of at least +3 people. The team should be representing as many demographics as possible, +ideally from different employers. diff --git a/contributing/code_of_conduct/code_of_conduct.rst b/contributing/code_of_conduct/code_of_conduct.rst new file mode 100644 index 00000000000..6202fdad424 --- /dev/null +++ b/contributing/code_of_conduct/code_of_conduct.rst @@ -0,0 +1,144 @@ +Code of Conduct +=============== + +Our Pledge +---------- + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +Our Standards +------------- + +Examples of behavior that contributes to creating a positive environment +include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others’ private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +Our Responsibilities +-------------------- + +:doc:`CoC Active Response Ensurers (CARE) team members ` +are responsible for clarifying and enforcing our standards of acceptable +behavior and will take appropriate and fair corrective action in response to any +behavior that they deem inappropriate, threatening, offensive, or harmful. + +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, and will communicate reasons for moderation +decisions when appropriate. + +Scope +----- + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +Enforcement +----------- + +Instances of abusive, harassing, or otherwise unacceptable behavior +:doc:`may be reported ` by +contacting the :doc:`CARE team members `. +All complaints will be reviewed and investigated promptly and fairly. + +CARE team members are obligated to respect the privacy and security of the +reporter of any incident. + +Enforcement Guidelines +---------------------- + +The :doc:`CARE team members ` will +follow these Community Impact Guidelines in determining the consequences for any +action they deem in violation of this Code of Conduct: + +1. Correction +~~~~~~~~~~~~~ + +Community Impact: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +Consequence: A private, written warning from a CARE team member, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +2. Warning +~~~~~~~~~~ + +Community Impact: A violation through a single incident or series of actions. + +Consequence: A warning with consequences for continued behavior. No interaction +with the people involved, including unsolicited interaction with those enforcing +the Code of Conduct, for a specified period of time. This includes avoiding +interactions in community spaces as well as external channels like social media. +Violating these terms may lead to a temporary or permanent ban. + +3. Temporary Ban +~~~~~~~~~~~~~~~~ + +Community Impact: A serious violation of community standards, including +sustained inappropriate behavior. + +Consequence: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +4. Permanent Ban +~~~~~~~~~~~~~~~~ + +Community Impact: Demonstrating a pattern of violation of community standards, +including sustained inappropriate behavior, harassment of an individual, or +aggression toward or disparagement of classes of individuals. + +Consequence: A permanent ban from any sort of public interaction within the +community. + +Attribution +----------- + +This Code of Conduct is adapted from the `Contributor Covenant`_, version 2.1, +available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html + +Community Impact Guidelines were inspired by `Mozilla’s code of conduct enforcement ladder`_. + +Related Documents +----------------- + +.. toctree:: + :maxdepth: 1 + + reporting_guidelines + care_team + concrete_example_document + +.. _Contributor Covenant: https://www.contributor-covenant.org +.. _Mozilla’s code of conduct enforcement ladder: https://github.com/mozilla/diversity diff --git a/contributing/code_of_conduct/concrete_example_document.rst b/contributing/code_of_conduct/concrete_example_document.rst new file mode 100644 index 00000000000..60ffe2527db --- /dev/null +++ b/contributing/code_of_conduct/concrete_example_document.rst @@ -0,0 +1,32 @@ +Code of Conduct: Concrete Example Document +========================================== + +This is a living document that serves to give concrete examples of +unwanted behavior. These examples have all taken place somewhere in the +PHP community in the past, and are clear code of conduct violations +according to the Symfony code of conduct. + +Concrete Examples +----------------- + +* Unwelcome comments regarding a person’s lifestyle choices and practices, + including those related to food, health, parenting, drugs, and employment; +* Deliberate misgendering or use of `dead names`_ (The birth name + of a person who has since changed their name, often a transgender person); +* Threats of violence like "The person that created this PR should be + punched in the face"; +* Incitement of violence towards any individual, including encouraging a + person to commit suicide or to engage in self-harm (even as a joke); +* Sustained disruption of discussion; +* Pattern of inappropriate social contact, such as requesting/assuming + inappropriate levels of intimacy with others; +* Continued one-on-one communication after requests to cease; +* Putting down people based on their technology choices or their work; +* Taking photographs of a conference attendee or speaker in the foreground and + publishing them without their permission. + +The original list is inspired and modified from `geek feminism`_ and +confirmed by experiences from PHPWomen. + +.. _dead names: https://en.wiktionary.org/wiki/deadname +.. _geek feminism: https://geekfeminism.org/about/code-of-conduct diff --git a/contributing/code_of_conduct/index.rst b/contributing/code_of_conduct/index.rst new file mode 100644 index 00000000000..5a2beff23a9 --- /dev/null +++ b/contributing/code_of_conduct/index.rst @@ -0,0 +1,10 @@ +Code of Conduct +=============== + +.. toctree:: + :maxdepth: 2 + + code_of_conduct + reporting_guidelines + care_team + concrete_example_document diff --git a/contributing/code_of_conduct/reporting_guidelines.rst b/contributing/code_of_conduct/reporting_guidelines.rst new file mode 100644 index 00000000000..a00394bce65 --- /dev/null +++ b/contributing/code_of_conduct/reporting_guidelines.rst @@ -0,0 +1,98 @@ +Reporting Guidelines +==================== + +If you believe someone is violating the Code of Conduct we ask that you report +it to the :doc:`CARE team ` +by emailing, Twitter, in person or any way you see fit. + +**All reports will be kept confidential.** The privacy of everyone included in +the report is of our highest concern. Second to privacy there is transparency. +After every report we will determine if a public statement should be made. If +that's the case, the identities of all victims, reporters, and the accused will +remain confidential unless those individuals instruct us otherwise. The details +of the incident may also be generalized. + +If you believe anyone is in physical danger or doing something that is against +the law, please notify appropriate emergency services first by calling the relevant +local authorities. If you are unsure what service or agency is appropriate to +contact, include this in your report and we will attempt to notify them. + +In your report please include: + +* Your contact info for follow-up contact. +* Names (legal, nicknames, or pseudonyms) of any individuals involved. +* If there were other witnesses besides you, please try to include them as well. +* When and where the incident occurred. Please be as specific as possible. +* Your description of what occurred. +* If there is a publicly available record (e.g. a mailing list archive or a + public IRC or Slack log), please include a link and a screenshot. +* If you believe this incident is ongoing. +* Any other information you believe we should have. + +What happens after you file a report? +------------------------------------- + +You will receive a reply from the :doc:`CARE team ` +acknowledging receipt as soon as possible, but within 24 hours. + +The team member receiving the report will immediately contact all or some other +CARE team members to review the incident and determine: + +* What happened. +* Whether this event constitutes a Code of Conduct violation. +* What kind of response is appropriate. + +If this is determined to be an ongoing incident or a threat to physical safety, +the team's immediate priority will be to protect everyone involved. This means +we may delay an "official" response until we believe that the situation has ended +and that everyone is physically safe. + +Once the team has a complete account of the events, they will make a decision as +to how to respond. Responses may include: + +* Nothing (if we determine no Code of Conduct violation occurred). +* A private reprimand from the Code of Conduct response team to the individual(s) + involved. +* An imposed vacation (i.e. asking someone to "take a week off" from a mailing + list or Slack). +* A permanent or temporary ban from some or all Symfony conference/community + spaces (events, meetings, mailing lists, IRC, Slack, etc.) +* A request to engage in mediation and/or an accountability plan. +* On a case by case basis, other actions may be possible but will usually be + coordinated with the core team and the Symfony company. + +We'll respond within one week to the person who filed the report with either a +resolution or an explanation of why the situation is not yet resolved. + +Once we've determined our final actions, we'll contact the original reporter to +let them know what action (if any) we'll be taking. We'll take into account feedback +from the reporter on the appropriateness of our response, but our response will be +determined by what will be best for community safety. + +The CARE team keeps a private record of all incidents. By default, all reports +are shared with the entire CARE team unless the reporter specifically asks +to exclude specific CARE team members, in which case these CARE team +members will not be included in any communication on the incidents as well as records +created related to the incidents. + +CARE team members are expected to inform the CARE team and the reporters +in case of a conflict of interest, and recuse themselves if this is deemed to be a problem. + +Appealing the response +---------------------- + +Only permanent resolutions (such as bans) may be appealed. To appeal a decision +of the working group, contact the :doc:`CARE team ` +with your appeal and they will review the case. + +Document origin +--------------- + +Reporting Guidelines derived from those of the `Stumptown Syndicate`_ and the +`Django Software Foundation`_. + +Adopted by `Symfony`_ organizers on 21 February 2018. + +.. _`Stumptown Syndicate`: https://github.com/stumpsyn/policies/blob/master/reporting_guidelines.md/ +.. _`Django Software Foundation`: https://www.djangoproject.com/conduct/reporting/ +.. _`Symfony`: https://symfony.com diff --git a/contributing/community/index.rst b/contributing/community/index.rst index e6b51e3fb99..4a5aab91265 100644 --- a/contributing/community/index.rst +++ b/contributing/community/index.rst @@ -5,5 +5,7 @@ Community :maxdepth: 2 releases - irc - other + review-comments + reviews + mentoring + speaker-mentoring diff --git a/contributing/community/irc.rst b/contributing/community/irc.rst deleted file mode 100644 index cc5bdb21ddf..00000000000 --- a/contributing/community/irc.rst +++ /dev/null @@ -1,60 +0,0 @@ -IRC Meetings -============ - -The purpose of this meeting is to discuss topics in real time with many of the -Symfony2 devs. - -Anyone may propose topics on the `symfony-dev`_ mailing-list until 24 hours -before the meeting, ideally including well prepared relevant information via -some URL. 24 hours before the meeting a link to a `doodle`_ will be posted -including a list of all proposed topics. Anyone can vote on the topics until -the beginning of the meeting to define the order in the agenda. Each topic -will be timeboxed to 15mins and the meeting lasts one hour, leaving enough -time for at least 4 topics. - -.. caution:: - - Note that it's not the expected goal of the meeting to find final - solutions, but more to ensure that there is a common understanding of the - issue at hand and move the discussion forward in ways which are hard to - achieve with less real time communication tools. - -Meetings will happen each Thursday at 17:00 CET (+01:00) on the #symfony-dev -channel on the Freenode IRC server. - -The IRC `logs`_ will later be published on the trac wiki, which will include a -short summary for each of the topics. Tickets will be created for any tasks or -issues identified during the meeting and referenced in the summary. - -Some simple guidelines and pointers for participation: - -* It's possible to change votes until the beginning of the meeting by clicking - on "Edit an entry"; -* The doodle will be closed for voting at the beginning of the meeting; -* Agenda is defined by which topics got the most votes in the doodle, or - whichever was proposed first in case of a tie; -* At the beginning of the meeting one person will identify him/herself as the - moderator; -* The moderator is essentially responsible for ensuring the 15min timebox and - ensuring that tasks are clearly identified; -* Usually the moderator will also handle writing the summary and creating trac - tickets unless someone else steps up; -* Anyone can join and is explicitly invited to participate; -* Ideally one should familiarize oneself with the proposed topic before the - meeting; -* When starting on a new topic the proposer is invited to start things off - with a few words; -* Anyone can then comment as they see fit; -* Depending on how many people participate one should potentially retrain - oneself from pushing a specific argument too hard; -* Remember the IRC `logs`_ will be published later on, so people have the - chance to review comments later on once more; -* People are encouraged to raise their hand to take on tasks defined during - the meeting. - -Here is an `example`_ doodle. - -.. _symfony-dev: http://groups.google.com/group/symfony-devs -.. _doodle: http://doodle.com -.. _logs: http://trac.symfony-project.org/wiki/Symfony2IRCMeetingLogs -.. _example: http://doodle.com/4cnzme7xys3ay53w diff --git a/contributing/community/mentoring.rst b/contributing/community/mentoring.rst new file mode 100644 index 00000000000..511a61e6e82 --- /dev/null +++ b/contributing/community/mentoring.rst @@ -0,0 +1,13 @@ +Mentoring +========= + +Reading the :doc:`contributing ` is already a great way +to get started on becoming a Symfony contributor. However, sometimes +it might still seem overwhelming - contributing can be complex! For this +purpose we created a dedicated `Symfony Slack`_ channel called `#mentoring`_ +to connect new contributors to long-time contributors. This is a great way +to get one-on-one advice on the entire process. These long-time contributors +truly want to help new contributors - so feel free to ask anything! + +.. _`Symfony Slack`: https://symfony.com/slack-invite +.. _`#mentoring`: https://symfony-devs.slack.com/messages/mentoring diff --git a/contributing/community/other.rst b/contributing/community/other.rst deleted file mode 100644 index 3869aa2a4e0..00000000000 --- a/contributing/community/other.rst +++ /dev/null @@ -1,15 +0,0 @@ -Other Resources -=============== - -In order to follow what is happening in the community you might find helpful -these additional resources: - -* List of open `pull requests`_ -* List of recent `commits`_ -* List of open `bugs and enhancements`_ -* List of open source `bundles`_ - -.. _pull requests: https://github.com/symfony/symfony/pulls -.. _commits: https://github.com/symfony/symfony/commits/master -.. _bugs and enhancements: https://github.com/symfony/symfony/issues -.. _bundles: http://knpbundles.com/ diff --git a/contributing/community/releases.rst b/contributing/community/releases.rst index 3a43ba425d2..2c5a796e9b5 100644 --- a/contributing/community/releases.rst +++ b/contributing/community/releases.rst @@ -1,27 +1,46 @@ The Release Process =================== -This document explains the Symfony release process (Symfony being the code -hosted on the main ``symfony/symfony`` `Git repository`_). +This document explains the process followed by the Symfony project to develop, +release and maintain its different versions. -Symfony manages its releases through a *time-based model*; a new Symfony -release comes out every *six months*: one in *May* and one in *November*. +Symfony releases follow the `semantic versioning`_ strategy and they are +published through a *time-based model*: -.. note:: +* A new **Symfony patch version** (e.g. 5.4.12, 6.1.9) comes out roughly every + month. It only contains bug fixes, so you can safely upgrade your applications; +* A new **Symfony minor version** (e.g. 5.4, 6.0, 6.1) comes out every *six months*: + one in *May* and one in *November*. It contains bug fixes and new features, + can contain new deprecations but it doesn't include any breaking change, + so you can safely upgrade your applications; +* A new **Symfony major version** (e.g. 5.0, 6.0, 7.0) comes out every *two years* + in November of odd years (e.g. 2019, 2021, 2023). It can contain breaking changes, + so you may need to do some changes in your applications before upgrading. + +.. tip:: + + `Subscribe to Symfony Release notifications`_ to receive an email when a new + Symfony version is published or when a Symfony version reaches its end of life. - This release process has been adopted as of Symfony 2.2, and all the - "rules" explained in this document must be strictly followed as of Symfony - 2.4. +.. _contributing-release-development: Development ----------- -The six-months period is divided into two phases: +.. note:: + + The Symfony project is an open-source community-driven development framework. + There is no roadmap written or defined in advance. Every feature request + may or may not be developed in future versions based on the community. + Symfony Core Team members can help move things forward if there's enough interest. + +The full development period for any major or minor version lasts six months and +is divided into two phases: -* *Development*: *Four months* to add new features and to enhance existing +* **Development**: *Four months* to add new features and to enhance existing ones; -* *Stabilisation*: *Two months* to fix bugs, prepare the release, and wait +* **Stabilization**: *Two months* to fix bugs, prepare the release, and wait for the whole Symfony ecosystem (third-party libraries, bundles, and projects using Symfony) to catch up. @@ -29,69 +48,88 @@ During the development phase, any new feature can be reverted if it won't be finished in time or if it won't be stable enough to be included in the current final release. -Maintenance ------------ +.. tip:: -Each Symfony version is maintained for a fixed period of time, depending on -the type of the release. + Check out the `Symfony Release`_ to learn more about any specific version. -Standard Releases -~~~~~~~~~~~~~~~~~ +.. _contributing-release-maintenance: +.. _symfony-versions: +.. _releases-lts: -A standard release is maintained for an *eight month* period. +Maintenance +----------- -Long Term Support Releases -~~~~~~~~~~~~~~~~~~~~~~~~~~ +Starting from the Symfony 3.x branch, the number of minor versions is limited to +five per branch (X.0, X.1, X.2, X.3 and X.4). The last minor version of a branch +(e.g. 5.4, 6.4) is considered a **long-term support version** and the other +ones are considered **standard versions**: -Every two years, a new Long Term Support Release (aka LTS release) is -published. Each LTS release is supported for a *three year* period. +======================= ===================== ================================ +Version Type Bugs are fixed for... Security issues are fixed for... +======================= ===================== ================================ +Standard 8 months 8 months +Long-Term Support (LTS) 3 years 4 years +======================= ===================== ================================ .. note:: - Paid support after the three year support provided by the community can - also be bought from `SensioLabs`_. + After the active maintenance of a Symfony version has ended, you can get + `professional Symfony support`_ from SensioLabs, the company which sponsors + the Symfony project. -Schedule --------- +.. _deprecations: -Below is the schedule for the first few versions that use this release model: +Backward Compatibility +---------------------- -.. image:: /images/release-process.jpg - :align: center +Our :doc:`Backward Compatibility Promise ` is very +strict and allows developers to upgrade with confidence from one minor version +of Symfony to the next one. -* **Yellow** represents the Development phase -* **Blue** represents the Stabilisation phase -* **Green** represents the Maintenance period +When a feature implementation cannot be replaced with a better one without +breaking backward compatibility, Symfony deprecates the old implementation and +adds a new preferred one alongside. Read the +:ref:`conventions ` document to +learn more about how deprecations are handled in Symfony. -This results in very predictable dates and maintenance periods. +.. _major-version-development: -* *(special)* Symfony 2.2 will be released at the end of February 2013; -* *(special)* Symfony 2.3 (the first LTS) will be released at the end of May - 2013; -* Symfony 2.4 will be released at the end of November 2013; -* Symfony 2.5 will be released at the end of May 2014; -* ... +This deprecation policy also requires a custom development process for major +versions (6.0, 7.0, etc.) In those cases, Symfony develops at the same time +two versions: the new major one (e.g. 6.0) and the latest version of the +previous branch (e.g. 5.4). -Backward Compatibility ----------------------- +Both versions have the same new features, but they differ in the deprecated +features. The oldest version (5.4 in this example) contains all the deprecated +features whereas the new version (6.0 in this example) removes all of them. -After the release of Symfony 2.3, backward compatibility will be kept at all -cost. If it is not possible, the feature, the enhancement, or the bug fix will -be scheduled for the next major version: Symfony 3.0. +This allows you to upgrade your projects to the latest minor version (e.g. 5.4), +see all the deprecation messages and fix them. Once you have fixed all those +deprecations, you can upgrade to the new major version (e.g. 6.0) without +effort, because it contains the same features (the only difference are the +deprecated features, which your project no longer uses). -.. note:: +PHP Compatibility +----------------- - The work on Symfony 3.0 will start whenever enough major features breaking - backward compatibility are waiting on the todo-list. +The **minimum** PHP version is decided for each **major** Symfony version by consensus +amongst the :doc:`core team ` and documented as +part of the :ref:`technical requirements for running Symfony applications +`. -Deprecations ------------- +Throughout each Symfony release's support lifetime, all released versions of PHP +including new major versions will be supported. In this way, the **maximum** supported +version of PHP for a maintained Symfony release is the latest released +one that is publicly available. -When a feature implementation cannot be replaced with a better one without -breaking backward compatibility, there is still the possibility to deprecate -the old implementation and add a new preferred one along side. Read the -:ref:`conventions` document to -learn more about how deprecations are handled in Symfony. +For out-of-support releases of Symfony, the latest PHP version at time of EOL is the last +supported PHP version. Newer versions of PHP may or may not function. + +.. note:: + + By exception to the rule, bumping the minimum **minor** version of PHP is + possible for a **minor** Symfony version when this helps fix important + issues. Rationale --------- @@ -108,7 +146,9 @@ This release process was adopted to give more *predictability* and * Coordinate the Symfony timeline with popular PHP projects that work well with Symfony and with projects using Symfony; * Give time to the Symfony ecosystem to catch up with the new versions - (bundle authors, documentation writers, translators, ...). + (bundle authors, documentation writers, translators, ...); +* Give companies a strict and predictable timeline they can rely on to plan + their own projects development. The six month period was chosen as two releases fit in a year. It also allows for plenty of time to work on new features and it allows for non-ready @@ -117,10 +157,11 @@ for the next cycle. The dual maintenance mode was adopted to make every Symfony user happy. Fast movers, who want to work with the latest and the greatest, use the standard -releases: a new version is published every six months, and there is a two -months period to upgrade. Companies wanting more stability use the LTS -releases: a new version is published every two years and there is a year to -upgrade. - -.. _Git repository: https://github.com/symfony/symfony -.. _SensioLabs: http://sensiolabs.com/ +version: a new version is published every six months, and there is a two months +period to upgrade. Companies wanting more stability use the LTS versions: a new +version is published every two years and there is a year to upgrade. + +.. _`semantic versioning`: https://semver.org/ +.. _`Subscribe to Symfony Release notifications`: https://symfony.com/account/notifications +.. _`Symfony Release`: https://symfony.com/releases +.. _`professional Symfony support`: https://sensiolabs.com/ diff --git a/contributing/community/review-comments.rst b/contributing/community/review-comments.rst new file mode 100644 index 00000000000..5b9bc932205 --- /dev/null +++ b/contributing/community/review-comments.rst @@ -0,0 +1,190 @@ +Respectful Review Comments +========================== + +:doc:`Reviewing issues and pull requests ` +is a great way to get started with contributing to the Symfony community. +Anyone can do it! But before you give a comment, take a step back and think, +is what you are about to say actually what you intend? + +Communicating over the Internet with nothing but text can pose a +big challenge, especially if you remember that the Symfony community +is world-wide and is composed of a wide variety of people with differing +ideas and opinions. + +Not everyone speaks English or is able to use a keyboard. Some might +have dyslexia or similar conditions that affect their writing. + +Not to mention that some might have a bad experience from previous +contributions (to other projects). + +You're not alone in this. This guide will try to help you write +constructive, respectful and helpful reviews and replies. + +.. tip:: + + This guide is not about lecturing you to "conform" or give-up + your ideas and opinions but helping you to better communicate, + prevent possible confusion, and keeping the Symfony community a + welcoming place for everyone. **You are free to disagree with + someone's opinions, but don't be disrespectful.** + +It’s important to accept that many programming decisions are opinions. +Discuss trade-offs, which you prefer, and reach a resolution quickly. +It's not about being right or wrong, but using what works. + +Tone of Voice +------------- + +We don't expect you to be completely formal, or to even write error-free +English. Just remember this: don't swear, and be respectful to others. + +Don't reply in anger or with an aggressive tone. If you're angry, we understand +that, but swearing/cursing and name calling doesn't really encourage anyone to +help you. Take a deep breath, count to 10 and try to *clearly* explain what problems +you encounter. + +Inclusive Language +------------------ + +In an effort to be inclusive to a wide group of people, it's recommended to +use personal pronouns that don't suggest a particular gender. Unless someone +has stated their pronouns, use "they", "them" instead of "he", "she", "his", +"hers", "his/hers", "he/she", etc. + +Try to avoid using wording that may be considered excluding, needlessly gendered +(e.g. words that have a male or female base), racially motivated or singles out +a particular group in society. For example, it's recommended to use words like +"folks", "team", "everyone" instead of "guys", "ladies", "yanks", etc. + +Giving Positive Feedback +------------------------ + +While reviewing issues and pull requests you may run into some suggestions +(including patches) that don't reflect your ideas, are not good, or downright wrong. + +Now, when you prepare your comment, consider the amount of work and time the author +has spent on their idea and how your response would make them feel. + +Did you correctly understand their intention? Or are you making assumptions? +Whatever your response, be explicit. Remember people don't always understand your +intentions online. + +Avoid using terms that could be seen as referring to personal traits ("dumb", "stupid"). +Assume everyone is intelligent and well-meaning. + +.. tip:: + + Good questions avoid judgment and avoid assumptions about the author's perspective. + + Maybe you can ask for clarification? Suggest an alternative? + Or provide a simple explanation *why* you disagree with their proposal. + + * ``This looks wrong. Are you sure it's correct?`` (e.g. typo/syntax error) + + * ``What do you think of "RequestFactory" instead of RequestCreator?`` + +Even if something *is* really wrong or "a bad idea", stay respectful and +don't get into endless you-are-wrong discussions or "flame wars". + +Don't use hyperbole ("always", "never", "endlessly", "nothing", "worst", "horrible", "terrible"). + +**Don't:** *"I don't like how you wrote this code"* - there is no clear explanation why you +don't like how it's written. + +**Better:** *"I find it hard to read this code as there are many nested if statements, can you make it more +readable? By encapsulating some of the details or maybe adding some comments to explain the overall logic."* - +You explain why you find the code hard to read *and* give some suggestions for improvement. + +If a piece of code is in fact wrong, explain why: + +* "This code doesn't comply with Symfony's CS rules. Please see [...] for details." + +* "Symfony 3 still uses PHP 5 and doesn't allow the usage of scalar type-hints." + +* "I think the code is less readable now." - careful here, be sure explain why you think + the code is less readable, and maybe give some suggestions? + +**Examples of valid reasons to reject:** + +* "We tried that in the past (link to the relevant PR) but we needed to revert it for XXX reason." + +* "That change would introduce too many merge conflicts when merging up Symfony branches. + In the past we've always rejected changes like this." + +* "I profiled this change and it hurts performance significantly" - if you don't profile, it's an opinion, so we can ignore + +* "Code doesn't match Symfony's CS rules (e.g. use ``[]`` instead of ``array()``)" + +* "We only provide integration with very popular projects (e.g. we integrate Bootstrap but not your own CSS framework)" + +* "This would require adding lots of code and making lots of changes for a feature that doesn't look so important. + That could hurt maintenance in the future." + +Asking for Changes +------------------ + +Rarely something is perfect from the start, while the code itself is good. +It may not be optimal or conform to the Symfony coding style. + +Again, understand the author already spent time on the issue and asking +for (small) changes may be misinterpreted or seen as a personal attack. + +Be thankful for their work (so far), stay positive and really help them +to make the contribution a great one. *Especially if they are a first +time contributor.* + +Use words like "Please", "Thank you" and "Could you" instead of making demands; + +* "Thank you for your work so far. I left some suggestions for improvement + to make the code more readable." + +* "Your code contains some coding-style problems, can you fix these before + we merge? Thank you" + +* "Please use 4 spaces instead of tabs", "This needs be on the previous line"; + +During a pull request review you can usually leave more than one comment, +you don't have to use "Please" all the time. But it wouldn't hurt. + +It may not seem like much, but saying "Thank you" does make others feel +more welcome. + +Preventing Escalations +---------------------- + +Sometimes when people receive feedback they may get defensive. +In that case, it is better to try to approach the discussion in +a different way, to not escalate further. + +If you want someone to mediate, please join the ``#contribs`` channel on `Symfony Slack`_, +to have a safe environment and keep working together on common goals. + +Using Humor +----------- + +In short: Extreme misbehavior will not be tolerated and may even get you banned; +Keep it real and friendly. + +**Don't use sarcasm for a serious topic, that's not something that belongs +to the Symfony community.** And don't marginalize someone's problems; +``Well I guess that's not supposed to happen? 😆``. + +Even if someone's explanation is "inviting to joke about it", it's a real +problem to them. Making jokes about this doesn't help with solving their +problem and only makes them *feel stupid*. Instead, try to discover the +actual problem. + +Final Words +----------- + +Don't feel bad if you "failed" to follow these tips. As long as your +intentions were good and you didn't really offend or insult anyone; +you can explain you misunderstood, you didn't mean to marginalize or +simply failed. + +But don't say it "just because", if your apology is not really meant +you *will* lose credibility and respect from other developers. + +*Do unto others as you would have them do unto you.* + +.. _`Symfony Slack`: https://symfony.com/slack-invite diff --git a/contributing/community/reviews.rst b/contributing/community/reviews.rst new file mode 100644 index 00000000000..06426c03985 --- /dev/null +++ b/contributing/community/reviews.rst @@ -0,0 +1,220 @@ +Community Reviews +================= + +Symfony is an open-source project driven by a large community. If you don't feel +ready to contribute code or patches, reviewing issues and pull requests (PRs) +can be a great start to get involved and give back. In fact, people who "triage" +issues are the backbone to Symfony's success! + +.. note:: + + Communicating in a way where your words come across as intended can be + difficult. Please read through the + :doc:`Respectful Review Comments ` + guidelines. + +Why Reviewing Is Important +-------------------------- + +Community reviews are essential for the development of the Symfony framework, +since there are many more pull requests and bug reports than there are members +in the Symfony core team to review, fix and merge them. + +On the `Symfony issue tracker`_, you can find many items in a `Needs Review`_ +status: + +* **Bug Reports**: Bug reports need to be checked for completeness. + Is any important information missing? Can the bug be reproduced? + +* **Pull Requests**: Pull requests contain code that fixes a bug or implements + new functionality. Reviews of pull requests ensure that they are implemented + properly, are covered by test cases, don't introduce new bugs and maintain + backward compatibility. + +Note that **anyone who has some basic familiarity with Symfony and PHP can +review bug reports and pull requests**. You don't need to be an expert to help. + +Be Constructive +--------------- + +Before you begin, remember that you are looking at the result of someone else's +hard work. A good review comment thanks the contributor for their work, +identifies what was done well, identifies what should be improved and suggests a +next step. + +Create a GitHub Account +----------------------- + +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 +----------------------------- + +A good way to get started with reviewing is to pick a bug report from the +`bug reports in need of review`_. + +The steps for the review are: + +#. **Is the Report Complete?** + + Good bug reports contain a link to a project (the "reproduction project") + created with the `Symfony skeleton`_ that reproduces the bug. If it + doesn't, the report should at least contain enough information and code + samples to reproduce the bug. + +#. **Reproduce the Bug** + + Download the reproduction project and test whether the bug can be reproduced + on your system. If the reporter did not provide a reproduction project, + create one based on one `Symfony skeleton`_. + +#. **Update the Issue Status** + + At last, add a comment to the bug report. **Thank the reporter for reporting + the bug**. Include the line ``Status: `` in your comment to trigger + our `Carson Bot`_ which updates the status label of the issue. You can set + the status to one of the following: + + **Needs Work** If the bug *does not* contain enough information to be + reproduced, explain what information is missing and move the report to this + status. + + **Works for me** If the bug *does* contain enough information to be + reproduced but works on your system, or if the reported bug is a feature and + not a bug, provide a short explanation and move the report to this status. + + **Reviewed** If you can reproduce the bug, move the report to this status. + If you created a reproduction project, include the link to the project in + your comment. + +.. topic:: Example + + Here is a sample comment for a bug report that could be reproduced: + + .. code-block:: text + + Thank you @weaverryan for creating this bug report! This indeed looks + like a bug. I reproduced the bug in the "kernel-bug" branch of + https://github.com/webmozart/some-project. + + Status: Reviewed + +The Pull Request Review Process +------------------------------- + +The process for reviewing pull requests (PRs) is similar to the one for bug +reports. Reviews of pull requests usually take a little longer since you need +to understand the functionality that has been fixed or added and find out +whether the implementation is complete. + +It is okay to do partial reviews! If you do a partial review, comment how far +you got and leave the PR in the "Needs Review" state. + +Pick a pull request from the `PRs in need of review`_ and follow these steps: + +#. **Is the PR Complete**? + + Every pull request must contain a header that gives some basic information + about the PR. You can find the template for that header in the + :ref:`Contribution Guidelines `. + +#. **Is the Base Branch Correct?** + + GitHub displays the branch that a PR is based on below the title of the + pull request. Is that branch correct? + + * Bugs should be fixed in the oldest, maintained version that contains the + bug. Check :doc:`Symfony's Release Schedule ` to find the oldest + currently supported version. + + * New features should always be added to the current development version. + Check the `Symfony Roadmap`_ to find the current development version. + +#. **Reproduce the Problem** + + Read the issue that the pull request is supposed to fix. Reproduce the + problem on a new project created with the `Symfony skeleton`_ and try to + understand why it exists. If the linked issue already contains such a + project, install it and run it on your system. + +#. **Review the Code** + + Read the code of the pull request and check it against some common criteria: + + * Does the code address the issue the PR is intended to fix/implement? + * Does the PR stay within scope to address *only* that issue? + * Does the PR contain automated tests? Do those tests cover all relevant + edge cases? + * Does the PR contain sufficient comments to understand its code? + * Does the code break backward compatibility? If yes, does the PR header say + so? + * Does the PR contain deprecations? If yes, does the PR header say so? Does + the code contain ``trigger_deprecation()`` statements for all deprecated + features? + * Are all deprecations and backward compatibility breaks documented in the + latest UPGRADE-X.X.md file? Do those explanations contain "Before"/"After" + examples with clear upgrade instructions? + + .. note:: + + Eventually, some of these aspects will be checked automatically. + +#. **Test the Code** + + Take your project from step 3 and test whether the PR works properly. + Replace the Symfony project in the ``vendor`` directory by the code in the + PR by running the following Git commands. Insert the PR ID (that's the number + after the ``#`` in the PR title) for the ```` placeholders: + + .. code-block:: terminal + + $ cd vendor/symfony/symfony + $ git fetch origin pull//head:pr + $ git checkout pr + + For example: + + .. code-block:: terminal + + $ git fetch origin pull/15723/head:pr15723 + $ git checkout pr15723 + + Now you can :doc:`test the project ` against + the code in the PR. + +#. **Update the PR Status** + + At last, add a comment to the PR. **Thank the contributor for working on the + PR**. Include the line ``Status: `` in your comment to trigger our + `Carson Bot`_ which updates the status label of the issue. You can set the + status to one of the following: + + **Needs Work** If the PR is not yet ready to be merged, explain the issues + that you found and move it to this status. + + **Reviewed** If the PR satisfies all the checks above, move it to this + status. A core contributor will soon look at the PR and decide whether it can + be merged or needs further work. + +.. topic:: Example + + Here is a sample comment for a PR that is not yet ready for merge: + + .. code-block:: text + + Thank you @weaverryan for working on this! It seems that your test + cases don't cover the cases when the counter is zero or smaller. + Could you please add some tests for that? + + Status: Needs Work + +.. _GitHub: https://github.com +.. _Symfony issue tracker: https://github.com/symfony/symfony/issues +.. _`Symfony skeleton`: https://github.com/symfony/skeleton +.. _create a GitHub account: https://help.github.com/github/getting-started-with-github/signing-up-for-a-new-github-account +.. _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/pulls?q=is%3Aopen+is%3Apr+label%3A%22Status%3A+Needs+Review%22 +.. _Symfony Roadmap: https://symfony.com/releases +.. _Carson Bot: https://github.com/carsonbot/carsonbot +.. _`Needs Review`: https://github.com/symfony/symfony/labels/Status%3A%20Needs%20Review diff --git a/contributing/community/speaker-mentoring.rst b/contributing/community/speaker-mentoring.rst new file mode 100644 index 00000000000..82b25c61f57 --- /dev/null +++ b/contributing/community/speaker-mentoring.rst @@ -0,0 +1,44 @@ +Speaker Mentoring +================= + +The Symfony community benefits greatly when as many people as possible +share their knowledge and experience with others. Every different +point of view adds to our collective understanding of how to best use +and evolve the code, design patterns and architecture provided within +the Symfony community. Because of this, we specifically want to hear +from long-time contributors and new users, who often come across entirely +different challenges with a totally fresh new look and perspective. + +How to get started +------------------ + +Giving a first talk at a conference can seem quite intimidating. But +don't worry! At one time, every speaker went through the same process. +And so, we want to make sure that as many people as possible are empowered +to take this path if they are motivated. We have collected a few resources +with advice to get started. More importantly, we can connect experienced +speakers with people who are just taking their first steps in this area: + +.. tip:: + + A good first step might be to give a talk at a local user group to a + smaller crowd that one knows more intimately. A next step could be to + give a talk at a conference in your first language. + +The best way to find people that can review your talk idea or slides is +the `#speaker-mentoring`_ channel on `Symfony Slack`_. There are many +seasoned speakers with knowledge in various parts of Symfony that are +motivated to help you get started on your path towards becoming a +public speaker. They can even do practice runs via video chat! +Furthermore, they can also be an ally when it comes to the day of +giving the talk at a conference! + +A great resource with advice on everything related to `public speaking`_ +is a collection of links maintained by VM (Vicky) Brasseur. It covers +everything from finding a conference call for proposals, how to +refine a proposal, to how to put together slide decks to practical +tips for preparation and talk delivery. + +.. _`#speaker-mentoring`: https://symfony-devs.slack.com/messages/speaker-mentoring +.. _`Symfony Slack`: https://symfony.com/slack-invite +.. _`public speaking`: https://github.com/vmbrasseur/Public_Speaking diff --git a/contributing/core_team.rst b/contributing/core_team.rst new file mode 100644 index 00000000000..f895dcd00d8 --- /dev/null +++ b/contributing/core_team.rst @@ -0,0 +1,381 @@ +Symfony Core Team +================= + +The **Symfony Core** team is the group of developers that determine the +direction and evolution of the Symfony project. Their votes rule if the +features and patches proposed by the community are approved or rejected. + +All the Symfony Core members are long-time contributors with solid technical +expertise and they have demonstrated a strong commitment to drive the project +forward. + +This document states the rules that govern the Symfony core team. These rules +are effective upon publication of this document and all Symfony Core members +must adhere to said rules and protocol. + +Core Team Member Role +--------------------- + +In addition to being a regular contributor, core team members are expected to: + +* Review, approve, and merge pull requests; +* Help enforce, improve, and implement Symfony :doc:`processes and policies `; +* Participate in the Symfony Core Team discussions (on Slack and GitHub). + +Core Team Member Responsibilities +--------------------------------- + +Core Team members are unpaid volunteers and as such, they are not expected to +dedicate any specific amount of time on Symfony. They are expected to help the +project in any way they can. From reviewing pull requests and writing documentation, +to participating in discussions and helping the community in general. However, +their involvement is completely voluntary and can be as much or as little as +they want. + +Core Team Communication +~~~~~~~~~~~~~~~~~~~~~~~ + +As an open source project, public discussions and documentation is favored +over private ones. All communication in the Symfony community conforms to +the :doc:`/contributing/code_of_conduct/code_of_conduct`. Request +assistance from other Core and CARE team members when getting in situations +not following the Code of Conduct. + +Core Team members are invited in a private Slack channel, for quick +interactions and private processes (e.g. security issues). Each member +should feel free to ask for assistance for anything they may encounter. +Expect no judgement from other team members. + +Core Organization +----------------- + +Symfony Core members are divided into groups. Each member can only belong to one +group at a time. The privileges granted to a group are automatically granted to +all higher priority groups. + +The Symfony Core groups, in descending order of priority, are as follows: + +1. **Project Leader** + + * Elects members in any other group; + * Merges pull requests in all Symfony repositories. + +2. **Mergers Team** + + * Merge pull requests on the main Symfony repository. + +In addition, there are other groups created to manage specific topics: + +* **Security Team**: manages the whole security process (triaging reported vulnerabilities, + fixing the reported issues, coordinating the release of security fixes, etc.); +* **Symfony UX Team**: manages the `UX repositories`_; +* **Symfony CLI Team**: manages the `CLI repositories`_; +* **Documentation Team**: manages the whole `symfony-docs repository`_. + +Active Core Members +~~~~~~~~~~~~~~~~~~~ + +* **Project Leader**: + + * **Fabien Potencier** (`fabpot`_). + +* **Mergers Team** (``@symfony/mergers`` on GitHub): + + * **Nicolas Grekas** (`nicolas-grekas`_); + * **Christophe Coevoet** (`stof`_); + * **Christian Flothmann** (`xabbuh`_); + * **Kévin Dunglas** (`dunglas`_); + * **Javier Eguiluz** (`javiereguiluz`_); + * **Grégoire Pineau** (`lyrixx`_); + * **Ryan Weaver** (`weaverryan`_); + * **Robin Chalas** (`chalasr`_); + * **Yonel Ceruto** (`yceruto`_); + * **Tobias Nyholm** (`Nyholm`_); + * **Wouter De Jong** (`wouterj`_); + * **Alexander M. Turek** (`derrabus`_); + * **Jérémy Derussé** (`jderusse`_); + * **Oskar Stark** (`OskarStark`_); + * **Mathieu Santostefano** (`welcomattic`_); + * **Kevin Bond** (`kbond`_); + * **Jérôme Tamarelle** (`gromnan`_); + * **Berislav Balogović** (`hypemc`_); + * **Mathias Arlaud** (`mtarld`_); + * **Florent Morselli** (`spomky`_); + * **Alexandre Daubois** (`alexandre-daubois`_). + +* **Security Team** (``@symfony/security`` on GitHub): + + * **Fabien Potencier** (`fabpot`_); + * **Jérémy Derussé** (`jderusse`_). + +* **Symfony UX Team** (``@symfony/ux`` on GitHub): + + * **Ryan Weaver** (`weaverryan`_); + * **Kevin Bond** (`kbond`_); + * **Simon André** (`smnandre`_); + * **Hugo Alliaume** (`kocal`_); + * **Matheo Daninos** (`webmamba`_). + +* **Symfony CLI Team** (``@symfony-cli/core`` on GitHub): + + * **Fabien Potencier** (`fabpot`_); + * **Tugdual Saunier** (`tucksaun`_). + +* **Documentation Team** (``@symfony/team-symfony-docs`` on GitHub): + + * **Fabien Potencier** (`fabpot`_); + * **Ryan Weaver** (`weaverryan`_); + * **Christian Flothmann** (`xabbuh`_); + * **Wouter De Jong** (`wouterj`_); + * **Javier Eguiluz** (`javiereguiluz`_). + * **Oskar Stark** (`OskarStark`_). + +Former Core Members +~~~~~~~~~~~~~~~~~~~ + +They are no longer part of the core team, but we are very grateful for all their +Symfony contributions: + +* **Bernhard Schussek** (`webmozart`_); +* **Abdellatif AitBoudad** (`aitboudad`_); +* **Romain Neutron** (`romainneutron`_); +* **Jordi Boggiano** (`Seldaek`_); +* **Lukas Kahwe Smith** (`lsmith77`_); +* **Jules Pietri** (`HeahDude`_); +* **Jakub Zalas** (`jakzal`_); +* **Samuel Rozé** (`sroze`_); +* **Tobias Schultze** (`Tobion`_); +* **Maxime Steinhausser** (`ogizanagi`_); +* **Titouan Galopin** (`tgalopin`_); +* **Michael Cullum** (`michaelcullum`_); +* **Thomas Calvet** (`fancyweb`_). + +Core Membership Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +About once a year, the core team discusses the opportunity to invite new members. + +Core Membership Revocation +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A Symfony Core membership can be revoked for any of the following reasons: + +* Refusal to follow the rules and policies stated in this document; +* Lack of activity for the past six months; +* Willful negligence or intent to harm the Symfony project; +* Upon decision of the **Project Leader**. + +Core Membership Compensation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Core Team members work on Symfony on a purely voluntary basis. In return +for their work for the Symfony project, members can get free access to +Symfony conferences. Personal vouchers for Symfony conferences are handed out +on request by the **Project Leader**. + +Code Development Rules +---------------------- + +Symfony project development is based on pull requests proposed by any member +of the Symfony community. Pull request acceptance or rejection is decided based +on the votes cast by the Symfony Core members. + +Pull Request Voting Policy +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ``-1`` votes must always be justified by technical and objective reasons; + +* ``+1`` votes do not require justification, unless there is at least one + ``-1`` vote; + +* Core members can change their votes as many times as they desire + during the course of a pull request discussion; +* Core members are not allowed to vote on their own pull requests. + +Pull Request Merging Policy +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A pull request **can be merged** if: + +* It is a :ref:`unsubstantial change `; +* Enough time was given for peer reviews; +* It is a bug fix and at least two **Mergers Team** members voted ``+1`` + (only one if the submitter is part of the Mergers team) and no Core + member voted ``-1`` (via GitHub reviews or as comments). +* It is a new feature and at least two **Mergers Team** members voted + ``+1`` (if the submitter is part of the Mergers team, two *other* members) + and no Core member voted ``-1`` (via GitHub reviews or as comments). + +.. _core-team_unsubstantial-changes: + +.. note:: + + Unsubstantial changes comprise typos, DocBlock fixes, code standards + fixes, comment, exception message tweaks, and minor CSS, JavaScript and + HTML modifications. + +Pull Request Merging Process +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All code must be committed to the repository through pull requests, except +for :ref:`unsubstantial change ` which can be +committed directly to the repository. + +**Mergers** must always use the command-line ``gh`` tool provided by the +**Project Leader** to merge pull requests. + +When merging a pull request, the tool asks for a category that should be chosen +following these rules: + +* **Feature**: For new features and deprecations; Pull requests must be merged + in the development branch. +* **Bug**: Only for bug fixes; We are very conservative when it comes to + merging older, but still maintained, branches. Read the :doc:`maintenance` + document for more information. +* **Minor**: For everything that does not change the code or when they don't + need to be listed in the CHANGELOG files: typos, Markdown files, test files, + new or missing translations, etc. +* **Security**: It's the category used for security fixes and should never be + used except by the security team. + +Getting the right category is important as it is used by automated tools to +generate the CHANGELOG files when releasing new versions. + +.. tip:: + + Core team members are part of the ``mergers`` group on the ``symfony`` + Github organization. This gives them write-access to many repositories, + including the main ``symfony/symfony`` mono-repository. + + To avoid unintentional pushes to the main project (which in turn creates + new versions on Packagist), Core team members are encouraged to have + two clones of the project locally: + + #. A clone for their own contributions, which they use to push to their + fork on GitHub. Clear out the push URL for the Symfony repository using + ``git remote set-url --push origin dev://null`` (change ``origin`` + to the Git remote poiting to the Symfony repository); + #. A clone for merging, which they use in combination with ``gh`` and + allows them to push to the main repository. + +Upmerging Version Branches +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To synchronize changes in all versions, version branches are regularly +merged from oldest to latest, called "upmerging". This is a manual process. +There is no strict policy on when this occurs, but usually not more than +once a day and at least once before monthly releases. + +Before starting the upmerge, Git must be configured to provide a merge +summary by running: + +.. code-block:: terminal + + # Run command in the "symfony" repository + $ git config merge.stat true + +The upmerge should always be done on all maintained versions at the same +time. Refer to `the releases page`_ to find all actively maintained +versions (indicated by a green color). + +The process follows these steps: + +#. Start on the oldest version and make sure it's up to date with the + upstream repository; +#. Check-out the second oldest version, update from upstream and merge the + previous version from the local branch; +#. Continue this process until you reached the latest version; +#. Push the branches to the repository and monitor the test suite. Failure + might indicate hidden/missed merge conflicts. + +.. code-block:: terminal + + # 'origin' is refered to as the main upstream project + $ git fetch origin + + # update the local branches + $ git checkout 6.4 + $ git reset --hard origin/6.4 + $ git checkout 7.2 + $ git reset --hard origin/7.2 + $ git checkout 7.3 + $ git reset --hard origin/7.3 + + # upmerge 6.4 into 7.2 + $ git checkout 7.2 + $ git merge --no-ff 6.4 + # ... resolve conflicts + $ git commit + + # upmerge 7.2 into 7.3 + $ git checkout 7.3 + $ git merge --no-ff 7.2 + # ... resolve conflicts + $ git commit + + $ git push origin 7.3 7.2 6.4 + +.. warning:: + + Upmerges must be explicit, i.e. no fast-forward merges. + +.. tip:: + + Solving merge conflicts can be challenging. You can always ping other + Core team members to help you in the process (e.g. members that merged + a specific conflicting change). + +Release Policy +~~~~~~~~~~~~~~ + +The **Project Leader** is also the release manager for every Symfony version. + +Symfony Core Rules and Protocol Amendments +------------------------------------------ + +The rules described in this document may be amended at any time at the +discretion of the **Project Leader**. + +.. _`symfony-docs repository`: https://github.com/symfony/symfony-docs +.. _`UX repositories`: https://github.com/symfony/ux +.. _`CLI repositories`: https://github.com/symfony-cli +.. _`fabpot`: https://github.com/fabpot/ +.. _`webmozart`: https://github.com/webmozart/ +.. _`Tobion`: https://github.com/Tobion/ +.. _`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/ +.. _`weaverryan`: https://github.com/weaverryan/ +.. _`aitboudad`: https://github.com/aitboudad/ +.. _`xabbuh`: https://github.com/xabbuh/ +.. _`javiereguiluz`: https://github.com/javiereguiluz/ +.. _`lyrixx`: https://github.com/lyrixx/ +.. _`chalasr`: https://github.com/chalasr/ +.. _`ogizanagi`: https://github.com/ogizanagi/ +.. _`Nyholm`: https://github.com/Nyholm +.. _`sroze`: https://github.com/sroze +.. _`yceruto`: https://github.com/yceruto +.. _`michaelcullum`: https://github.com/michaelcullum +.. _`wouterj`: https://github.com/wouterj +.. _`HeahDude`: https://github.com/HeahDude +.. _`OskarStark`: https://github.com/OskarStark +.. _`romainneutron`: https://github.com/romainneutron +.. _`lsmith77`: https://github.com/lsmith77/ +.. _`derrabus`: https://github.com/derrabus/ +.. _`jderusse`: https://github.com/jderusse/ +.. _`tgalopin`: https://github.com/tgalopin/ +.. _`fancyweb`: https://github.com/fancyweb/ +.. _`welcomattic`: https://github.com/welcomattic/ +.. _`kbond`: https://github.com/kbond/ +.. _`gromnan`: https://github.com/gromnan/ +.. _`smnandre`: https://github.com/smnandre/ +.. _`kocal`: https://github.com/kocal/ +.. _`webmamba`: https://github.com/webmamba/ +.. _`hypemc`: https://github.com/hypemc/ +.. _`mtarld`: https://github.com/mtarld/ +.. _`spomky`: https://github.com/spomky/ +.. _`alexandre-daubois`: https://github.com/alexandre-daubois/ +.. _`tucksaun`: https://github.com/tucksaun/ +.. _`the releases page`: https://symfony.com/releases diff --git a/contributing/diversity/further_reading.rst b/contributing/diversity/further_reading.rst new file mode 100644 index 00000000000..8bb07c39c97 --- /dev/null +++ b/contributing/diversity/further_reading.rst @@ -0,0 +1,56 @@ +Further Reading / Viewing +========================= + +This is a non-exhaustive list of further reading on the topic of diversity. + +Diversity in Open Source +------------------------ + +`Sage Sharp - What makes a good community? `_ +`Ashe Dryden - The Ethics of Unpaid Labor and the OSS Community `_ +`Model View Culture - The Dehumanizing Myth of the Meritocracy `_ +`Annalee - How “Good Intent” Undermines Diversity and Inclusion `_ +`Karolina Szczur - Building Inclusive Communities `_ + +Code of Conduct +--------------- + +`Karolina Szczur - When a Code of Conduct becomes harmful `_ +`Ashe Dryden - Codes of Conduct 101 + FAQ `_ +`Phil Sturgeon - Codes of Conduct: Maybe They're Not So Bad? `_ + +Inclusive language +------------------ + +`Jenée Desmond-Harris - Why I’m finally convinced it's time to stop saying "you guys" `_ +`inclusive language presentations `_ + +Other talks and Blog Posts +-------------------------- + +`Lena Reinhard – A Talk About Nothing `_ +`Lena Reinhard - A Talk about Everything `_ +`Sage Sharp - SCALE: Improving Diversity with Maslow’s hierarchy `_ +`UCSF - Unconscious Bias `_ +`Responding to harassment reports `_ +`Unconscious bias at work `_ +`CIS people declaring their pronouns `_ + +Books +----- + +`Emily Chang - Brotopia `_ + +Websites +-------- + +`Better Allies `_ +`Geek Feminism WIKI `_ +`Open Source Diversity `_ +`Open Demographics documentation `_ +`CHAOSS Metrics `_ +`Up for grabs `_ +`The developmental model of intercultural sensitivity (DMIS) `_ +`DiversifyTech `_ +`so-you-just-learned `_ +`The Post-Meritocracy Manifesto `_ diff --git a/contributing/diversity/governance.rst b/contributing/diversity/governance.rst new file mode 100644 index 00000000000..93a79ed30fa --- /dev/null +++ b/contributing/diversity/governance.rst @@ -0,0 +1,143 @@ +Diversity Initiative Governance +=============================== + +Membership +---------- + +Membership of Symfony's Diversity Initiative is open to any member of the +Symfony community; to avoid the risk of elitism or meritocracy, no requirement +is needed to be involved. All members, at any time, are invited to put forward +ideas and suggestions as a proposal for an actionable item. + +Guidance +-------- + +The project leader, Fabien Potencier, is responsible for publicly appointing +five (5) members of the initiative to provide guidance and drive it forward, +but also retains the right to revoke any of the appointed members at any time. +This guidance team should: + +* Be committed to the initiative's cause and have joined because they want to + help the initiative to deliver its purpose most effectively for the + community's benefit. +* Recognize that meeting the initiative's purpose is an ongoing effort. +* Be committed to good governance and want to contribute to the initiative's + continued improvement. + +The current guidance team is composed of the following people (in alphabetical +order): + +* **Lukas Kahwe Smith** (`lsmith77`_); +* **Michelle Sanver** (`michellesanver`_); +* **Nicolas Grekas** (`nicolas-grekas`_); +* **Timo Bakx** (`TimoBakx`_); +* **Zan Baldwin** (`zanbaldwin`_). + +Veto +~~~~ + +The project leader (Fabien Potencier) will have the right to veto any actionable +item, regardless of the vote of the initiative's guidance team. The project +leader may, at their discretion, also appoint other people from among the +initiative's guidance team to also have the right to veto - in such a case these +people are expected to use appropriate judgment to know when to use a "no" vote +or a veto. Any single veto will reject an actionable item. + +The purpose of having members with the right to veto is to prevent a "people's +majority" from overruling the core interests of the Symfony project. This will +encourage communication between proposing members, the initiative's guidance +team and the Core Team to create realistic proposals, and in return any veto +will come with a full explanation (not just a justification). + +Advice Process +~~~~~~~~~~~~~~ + +When a proposal on an actionable item is ready to be decided on, insight from +the community (advice, general consensus, or non-binding poll) should be +requested from the wider community - this will aim to include both those who +will be meaningfully affected and those with meaningful expertise in the matter +at hand. +This feedback will enable the guidance team to have the confidence to vote for +the best possible decision according to the information they have available, +knowing that the responsibility they accept for said vote is justified. + +Voting +~~~~~~ + +The guidance team has the right to vote on proposals for actionable items. +The quorum of "yes" or "no" votes required for a decision to be considered valid +is at least 75% of active, appointed members of the guidance team - to abstain +from voting means that vote will not be counted towards the quorum. +For an actionable item to pass, approval from more than 50% of the voting +guidance team members is required. Use or management of finances/donations +require at least a two-thirds majority to pass. + +For transparency and ease-of-understanding, this means only the following +combinations of votes will result in an actionable item passing: + ++-----+---------+---------+ +| For | Against | Abstain | ++=====+=========+=========+ +| 5 | 0 | 0 | ++-----+---------+---------+ +| 4 | 1 | 0 | ++-----+---------+---------+ +| 3 | 2 | 0 | ++-----+---------+---------+ +| 4 | 0 | 1 | ++-----+---------+---------+ +| 3 | 1 | 1 | ++-----+---------+---------+ + +Guidance Principles +------------------- + +Purpose +~~~~~~~ + +The initiative should be led by an effective guidance team that provides +strategic guidance in line with the initiative's aims and values, including a +shared understanding with fellow initiative members to ensure that these are +being delivered effectively and sustainably. + +Integrity +~~~~~~~~~ + +The guidance team should act with integrity: adopting values which help achieve +the initiative's purposes, even where difficult or unpopular decisions are +required. Guidance team members should undertake their duties, aware of the +importance of confidence and trust in the initiative from the wider community, +and ultimately acknowledges shared responsibility for the reputation of the +Symfony project like the Core Team. + +Decision-making Effectiveness +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Guidance members should work as an effective team, using the appropriate balance +of skills, experience, backgrounds and knowledge to make sure its +decision-making processes are informed and equitable. Risk assessment and +management systems should be set up and monitored. + +Openness and Accountability +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The behavior and conduct of the initiative's guidance team sets the tone for +the rest of the community. The guidance team should lead by example to create a +culture that enables members to feel it is safe to suggest, question and +challenge - rather than avoid - difficult ideas and topics. The team should +guide the initiative in being transparent, accountable and open. + +Adaptability +~~~~~~~~~~~~ + +The initiative should establish processes that do not require any one person to +hold specific positions while being adaptable to accommodate unforeseen needs of +the community, especially as membership and involvement grows over time (changes +to guidance team member appointment will have to be approved by the current +system, which is Fabien Potencier). + +.. _`lsmith77`: https://github.com/lsmith77/ +.. _`michellesanver`: https://github.com/michellesanver/ +.. _`nicolas-grekas`: https://github.com/nicolas-grekas/ +.. _`TimoBakx`: https://github.com/TimoBakx/ +.. _`zanbaldwin`: https://github.com/zanbaldwin/ diff --git a/contributing/diversity/index.rst b/contributing/diversity/index.rst new file mode 100644 index 00000000000..85fd0694d4e --- /dev/null +++ b/contributing/diversity/index.rst @@ -0,0 +1,8 @@ +Diversity Initiative +==================== + +.. toctree:: + :maxdepth: 2 + + governance + further_reading diff --git a/contributing/documentation/format.rst b/contributing/documentation/format.rst index d6c2bc06491..3318df50841 100644 --- a/contributing/documentation/format.rst +++ b/contributing/documentation/format.rst @@ -1,37 +1,38 @@ Documentation Format ==================== -The Symfony2 documentation uses `reStructuredText`_ as its markup language and -`Sphinx`_ for building the output (HTML, PDF, ...). +The Symfony documentation uses `reStructuredText`_ as its markup language and +a custom tool called `Docs Builder`_ for generating the documentation pages. reStructuredText ---------------- -reStructuredText "is an easy-to-read, what-you-see-is-what-you-get plaintext -markup syntax and parser system". +reStructuredText is a plain text markup syntax similar to Markdown, but much +stricter with its syntax. If you are new to reStructuredText, check out the +`reStructuredText Primer`_ tutorial and the `reStructuredText Reference`_. -You can learn more about its syntax by reading existing Symfony2 `documents`_ -or by reading the `reStructuredText Primer`_ on the Sphinx website. +You can also take some time to familiarize with this format by reading the +existing `Symfony documentation`_ source. -If you are familiar with Markdown, be careful as things are sometimes very -similar but different: +.. warning:: -* Lists starts at the beginning of a line (no indentation is allowed); + If you are familiar with Markdown, be careful as things are sometimes very + similar but different: -* Inline code blocks use double-ticks (````like this````). + * Lists start at the beginning of a line (no indentation is allowed); + * Inline code blocks use double-ticks (````like this````). -Sphinx ------- +Custom reStructuredText Directives +---------------------------------- -Sphinx is a build system that adds some nice tools to create documentation -from reStructuredText documents. As such, it adds new directives and -interpreted text roles to standard reST `markup`_. +The Symfony documentation includes several custom directives that extend the +standard reStructuredText syntax. Syntax Highlighting ~~~~~~~~~~~~~~~~~~~ -All code examples uses PHP as the default highlighted language. You can change -it with the ``code-block`` directive: +PHP is the default syntax highlighter applied to all code blocks. You can +change it with the ``code-block`` directive: .. code-block:: rst @@ -39,25 +40,20 @@ it with the ``code-block`` directive: { foo: bar, bar: { foo: bar, bar: baz } } -If your PHP code begins with ``foobar(); ?> - .. note:: - A list of supported languages is available on the `Pygments website`_. + Code highlighting is supported for all programming languages commonly used + in Symfony Docs, such as ``yaml``, ``xml``, ``twig``, ``html``, ``js``, + ``json``, ``text``, ``bash``, ``diff``, etc. + +.. _docs-configuration-blocks: Configuration Blocks ~~~~~~~~~~~~~~~~~~~~ -Whenever you show a configuration, you must use the ``configuration-block`` +Whenever you include a configuration sample, use the ``configuration-block`` directive to show the configuration in all supported configuration formats -(``PHP``, ``YAML``, and ``XML``) +(``PHP``, ``YAML`` and ``XML``). Example: .. code-block:: rst @@ -69,13 +65,13 @@ directive to show the configuration in all supported configuration formats .. code-block:: xml - + .. code-block:: php // Configuration in PHP -The previous reST snippet renders as follow: +The previous reStructuredText snippet renders as follow: .. configuration-block:: @@ -85,73 +81,152 @@ The previous reST snippet renders as follow: .. code-block:: xml - + .. code-block:: php // Configuration in PHP +All code examples assume that you are using that feature inside a Symfony +application. If you ever need to also show how to use it when working with +standalone components in any PHP application, use the special formats +``php-symfony`` and ``php-standalone``, which will be rendered like this: + +.. configuration-block:: + + .. code-block:: php-symfony + + // PHP code using features provided by the Symfony framework + + .. code-block:: php-standalone + + // PHP code using standalone components + The current list of supported formats are the following: -+-----------------+-------------+ -| Markup format | Displayed | -+=================+=============+ -| html | HTML | -+-----------------+-------------+ -| xml | XML | -+-----------------+-------------+ -| php | PHP | -+-----------------+-------------+ -| yaml | YAML | -+-----------------+-------------+ -| jinja | Twig | -+-----------------+-------------+ -| html+jinja | Twig | -+-----------------+-------------+ -| html+php | PHP | -+-----------------+-------------+ -| ini | INI | -+-----------------+-------------+ -| php-annotations | Annotations | -+-----------------+-------------+ +=================== ============================================================================== +Markup Format Use It to Display +=================== ============================================================================== +``caddy`` Caddy web server configuration +``env`` Bash files (like ``.env`` files) +``html+php`` PHP code blended with HTML +``html+twig`` Twig markup blended with HTML +``html`` HTML +``ini`` INI +``php-annotations`` PHP Annotations +``php-attributes`` PHP Attributes +``php-standalone`` PHP code to be used in any PHP application using standalone Symfony components +``php-symfony`` PHP code example when using the Symfony framework +``php`` PHP +``rst`` reStructuredText markup +``terminal`` Renders the contents as a console terminal (use it to show which commands to run) +``twig`` Pure Twig markup +``varnish3`` Varnish Cache 3 configuration +``varnish4`` Varnish Cache 4 configuration +``vcl`` Varnish Configuration Language +``xml`` XML +``yaml`` YAML +=================== ============================================================================== + +Displaying Tabs +~~~~~~~~~~~~~~~ + +It is possible to display tabs in the documentation. They look similar to +configuration blocks when rendered, but tabs can hold any type of content: + +.. code-block:: rst + + .. tabs:: UX Installation + + .. tab:: Webpack Encore + + Introduction to Webpack + + .. code-block:: yaml + + webpack: + # ... + + .. tab:: AssetMapper + + Introduction to AssetMapper + + Something else about AssetMapper Adding Links ~~~~~~~~~~~~ -To add links to other pages in the documents use the following syntax: +The most common type of links are **internal links** to other documentation pages, +which use the following syntax: .. code-block:: rst - :doc:`/path/to/page` + :doc:`/absolute/path/to/page` -Using the path and filename of the page without the extension, for example: +The page name should not include the file extension (``.rst``). For example: .. code-block:: rst - :doc:`/book/controller` + :doc:`/controller` + + :doc:`/components/event_dispatcher` + + :doc:`/configuration/environments` + +The title of the linked page will be automatically used as the text of the link. +If you want to modify that title, use this alternative syntax: + +.. code-block:: rst + + :doc:`Doctrine Associations ` + +.. note:: + + Although they are technically correct, avoid the use of relative internal + links such as the following, because they break the references in the + generated PDF documentation: + + .. code-block:: rst - :doc:`/components/event_dispatcher/introduction` + :doc:`controller` - :doc:`/cookbook/configuration/environments` + :doc:`event_dispatcher` -The link text will be the main heading of the document linked to. You can -also specify alternative text for the link: + :doc:`environments` + +**Links to specific page sections** follow a different syntax. First, define a +target above section you will link to (syntax: ``.. _`` + target name + ``:``): .. code-block:: rst - :doc:`Spooling Email` + # /service_container/autowiring.rst + + # define the target + .. _autowiring-calls: + + Autowiring other Methods (e.g. Setters and Public Typed Properties) + ------------------------------------------------------------------- -You can also add links to the API documentation: + // section content ... + +Then, use the ``:ref::`` directive to link to that section from another file: .. code-block:: rst - :namespace:`Symfony\\Component\\BrowserKit` + # /reference/attributes.rst + + :ref:`Required ` + +**Links to the API** follow a different syntax, where you must specify the type +of the linked resource (``class`` or ``method``): + +.. code-block:: rst :class:`Symfony\\Component\\Routing\\Matcher\\ApacheUrlMatcher` :method:`Symfony\\Component\\HttpKernel\\Bundle\\Bundle::build` -and to the PHP documentation: +**Links to the PHP documentation** follow a pretty similar syntax: .. code-block:: rst @@ -161,57 +236,49 @@ and to the PHP documentation: :phpfunction:`iterator_to_array` -Testing Documentation -~~~~~~~~~~~~~~~~~~~~~ +New Features, Behavior Changes or Deprecations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To test documentation before a commit: +If you are documenting a brand new feature, a change or a deprecation that's +been made in Symfony, you should precede your description of the change with +the corresponding directive and a short description: -* Install `Sphinx`_; +For a new feature or a behavior change use the ``.. versionadded:: 7.x`` +directive: -* Run the `Sphinx quick setup`_; +.. code-block:: rst -* Install the Sphinx extensions (see below); + .. versionadded:: 7.2 -* Run ``make html`` and view the generated HTML in the ``build`` directory. + ... ... ... was introduced in Symfony 7.2. -Installing the Sphinx extensions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +If you are documenting a behavior change, it may be helpful to *briefly* +describe how the behavior has changed: -* Download the extension from the `source`_ repository +.. code-block:: rst -* Copy the ``sensio`` directory to the ``_exts`` folder under your source - folder (where ``conf.py`` is located) + .. versionadded:: 7.2 -* Add the following to the ``conf.py`` file: + ... ... ... was introduced in Symfony 7.2. Prior to this, + ... ... ... ... ... ... ... ... . -.. code-block:: py - - # ... - sys.path.append(os.path.abspath('_exts')) +For a deprecation use the ``.. deprecated:: 7.x`` directive: - # adding PhpLexer - from sphinx.highlighting import lexers - from pygments.lexers.web import PhpLexer +.. code-block:: rst - # ... - # add the extensions to the list of extensions - extensions = [..., 'sensio.sphinx.refinclude', 'sensio.sphinx.configurationblock', 'sensio.sphinx.phpcode'] + .. deprecated:: 7.2 - # enable highlighting for PHP code not between ```` by default - lexers['php'] = PhpLexer(startinline=True) - lexers['php-annotations'] = PhpLexer(startinline=True) + ... ... ... was deprecated in Symfony 7.2. - # use PHP as the primary domain - primary_domain = 'php' - - # set url for API links - api_url = 'http://api.symfony.com/master/%s' +Whenever a new major version of Symfony is released (e.g. 8.0, 9.0, etc), a new +branch of the documentation is created from the ``x.4`` branch of the previous +major version. At this point, all the ``versionadded`` and ``deprecated`` tags +for Symfony versions that have a lower major version will be removed. For +example, if Symfony 8.0 were released today, 7.0 to 7.4 ``versionadded`` and +``deprecated`` tags would be removed from the new ``8.0`` branch. -.. _reStructuredText: http://docutils.sourceforge.net/rst.html -.. _Sphinx: http://sphinx-doc.org/ -.. _documents: https://github.com/symfony/symfony-docs -.. _reStructuredText Primer: http://sphinx-doc.org/rest.html -.. _markup: http://sphinx-doc.org/markup/ -.. _Pygments website: http://pygments.org/languages/ -.. _source: https://github.com/fabpot/sphinx-php -.. _Sphinx quick setup: http://sphinx-doc.org/tutorial.html#setting-up-the-documentation-sources +.. _`reStructuredText`: https://docutils.sourceforge.io/rst.html +.. _`Docs Builder`: https://github.com/symfony-tools/docs-builder +.. _`Symfony documentation`: https://github.com/symfony/symfony-docs +.. _`reStructuredText Primer`: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html +.. _`reStructuredText Reference`: https://docutils.sourceforge.io/docs/user/rst/quickref.html diff --git a/contributing/documentation/index.rst b/contributing/documentation/index.rst index cd25642aaab..9af054d0502 100644 --- a/contributing/documentation/index.rst +++ b/contributing/documentation/index.rst @@ -1,10 +1,22 @@ Contributing Documentation ========================== -.. toctree:: - :maxdepth: 2 +These short articles explain everything you need to contribute to the Symfony +documentation: - overview - format - translations - license +:doc:`The Contribution Process ` + Explains the steps to follow to contribute fixes and new contents. It's the + same contribution process followed by most open source projects, so you may + already know everything that is needed. + +:doc:`Documentation Formats ` + Explains the technical details of the reStructuredText format that is used to + write the docs. Skip it if you are already familiar with this format. + +:doc:`Documentation Standards ` + Explains how to write docs and code examples to match the style and tone of + the rest of the existing documentation. + +:doc:`License ` + Explains the details of the Creative Commons BY-SA 3.0 license used for the + Symfony Documentation. diff --git a/contributing/documentation/license.rst b/contributing/documentation/license.rst index ccbda535dec..b6b89e7b96b 100644 --- a/contributing/documentation/license.rst +++ b/contributing/documentation/license.rst @@ -1,8 +1,10 @@ -Symfony2 Documentation License -============================== +.. _symfony2-documentation-license: -The Symfony2 documentation is licensed under a Creative Commons -Attribution-Share Alike 3.0 Unported `License`_. +Symfony Documentation License +============================= + +The Symfony documentation is licensed under a Creative Commons +Attribution-Share Alike 3.0 Unported License (`CC BY-SA 3.0`_). **You are free:** @@ -32,13 +34,13 @@ Attribution-Share Alike 3.0 Unported `License`_. * *Other Rights* — In no way are any of the following rights affected by the license: - * Your fair dealing or fair use rights, or other applicable copyright - exceptions and limitations; + * Your fair dealing or fair use rights, or other applicable copyright exceptions + and limitations; - * The author's moral rights; + * The author's moral rights; - * Rights other persons may have either in the work itself or in how - the work is used, such as publicity or privacy rights. + * Rights other persons may have either in the work itself or in how the + work is used, such as publicity or privacy rights. * *Notice* — For any reuse or distribution, you must make clear to others the license terms of this work. The best way to do this is with a link @@ -46,5 +48,12 @@ Attribution-Share Alike 3.0 Unported `License`_. This is a human-readable summary of the `Legal Code (the full license)`_. -.. _License: http://creativecommons.org/licenses/by-sa/3.0/ -.. _Legal Code (the full license): http://creativecommons.org/licenses/by-sa/3.0/legalcode +Other Symfony Licenses +---------------------- + +Check out the :doc:`license of the Symfony code ` +and other `Symfony licenses and trademarks`_. + +.. _`CC BY-SA 3.0`: https://creativecommons.org/licenses/by-sa/3.0/ +.. _Legal Code (the full license): https://creativecommons.org/licenses/by-sa/3.0/legalcode +.. _`Symfony licenses and trademarks`: https://symfony.com/license diff --git a/contributing/documentation/overview.rst b/contributing/documentation/overview.rst index 1faa454a396..7095e4cbc4c 100644 --- a/contributing/documentation/overview.rst +++ b/contributing/documentation/overview.rst @@ -1,234 +1,298 @@ Contributing to the Documentation ================================= -Documentation is as important as code. It follows the exact same principles: -DRY, tests, ease of maintenance, extensibility, optimization, and refactoring -just to name a few. And of course, documentation has bugs, typos, hard to read -tutorials, and more. +Before Your First Contribution +------------------------------ -Contributing ------------- +**Before contributing**, you need to: -Before contributing, you need to become familiar with the :doc:`markup -language ` used by the documentation. +* Sign up for a free `GitHub`_ account, which is the service where the Symfony + documentation is hosted. +* Be familiar with the `reStructuredText`_ markup language, which is used to + write Symfony docs. Read :doc:`this article ` + for a quick overview. -The Symfony2 documentation is hosted on GitHub: +.. _minor-changes-e-g-typos: -.. code-block:: text +Fast Online Contributions +------------------------- - https://github.com/symfony/symfony-docs +If you're making a relatively small change - like fixing a typo or rewording +something - the easiest way to contribute is directly on GitHub! You can do this +while you're reading the Symfony documentation. -If you want to submit a patch, `fork`_ the official repository on GitHub and -then clone your fork: +**Step 1.** Click on the **edit this page** button on the top of the page +and you'll be redirected to GitHub: -.. code-block:: bash +.. image:: /_images/contributing/docs-github-edit-page.png + :alt: The "Edit this page" button is located directly below the first heading. + :class: with-browser - $ git clone git://github.com/YOURUSERNAME/symfony-docs.git +**Step 2.** If this is your first contribution, you have to fork the repository. +Then, edit the contents, preview your changes (with the button at the top left) +and click on the **Commit changes...** button. In the popup, describe your changes +and click on **Propose changes** button. -Consistent with Symfony's source code, the documentation repository is split into -multiple branches: ``2.0``, ``2.1``, ``2.2`` corresponding to the different -versions of Symfony itself. The ``master`` branch holds the documentation -for the development branch of the code. +**Step 3.** GitHub will now create a branch and a commit for your changes and it will +also display a preview of your changes: -Unless you're documenting a feature that was introduced *after* Symfony 2.0 -(e.g. in Symfony 2.1), your changes should always be based on the 2.0 branch. -To do this checkout the 2.0 branch before the next step: +.. image:: /_images/contributing/docs-github-create-pr.png + :alt: The "Comparing changes" page on GitHub. + :class: with-browser -.. code-block:: bash +If everything is correct, click on the **Create pull request** button. - $ git checkout 2.0 +**Step 4.** GitHub will display a new page where you can do some last-minute +changes to your pull request before creating it. For simple contributions, you +can safely ignore these options and just click on the **Create pull request** +button again. -.. tip:: +**Congratulations!** You just created a pull request to the official Symfony +documentation! The community will now review your pull request and (possibly) +suggest tweaks. - Your base branch (e.g. 2.0) will become the "Applies to" in the :ref:`doc-contributing-pr-format` - that you'll use later. +If your contribution is large or if you prefer to work on your own computer, +keep reading this guide to learn an alternative way to send pull requests to the +Symfony Documentation. -Next, create a dedicated branch for your changes (for organization): +Your First Documentation Contribution +------------------------------------- -.. code-block:: bash +In this section, you'll learn how to contribute to the Symfony documentation for +the first time. The next section will explain the shorter process you'll follow +in the future for every contribution after your first one. - $ git checkout -b improving_foo_and_bar +Let's imagine that you want to improve the Setup guide. In order to make your +changes, follow these steps: -You can now make your changes directly to this branch and commit them. When -you're done, push this branch to *your* GitHub fork and initiate a pull request. +**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. -Creating a Pull Request -~~~~~~~~~~~~~~~~~~~~~~~ +**Step 2.** **Clone** the forked repository to your local machine (this example +uses the ``projects/symfony-docs/`` directory to store the documentation; change +this value accordingly): -Following the example, the pull request will default to be between your -``improving_foo_and_bar`` branch and the ``symfony-docs`` ``master`` branch. +.. code-block:: terminal -.. image:: /images/docs-pull-request.png - :align: center + $ cd projects/ + $ git clone git@github.com:YOUR-GITHUB-USERNAME/symfony-docs.git -If you have made your changes based on the 2.0 branch then you need to change -the base branch to be 2.0 on the preview page: +**Step 3.** Add the original Symfony docs repository as a "Git remote" executing +this command: -.. image:: /images/docs-pull-request-change-base.png - :align: center +.. code-block:: terminal -.. note:: + $ cd symfony-docs/ + $ git remote add upstream https://github.com/symfony/symfony-docs.git - All changes made to a branch (e.g. 2.0) will be merged up to each "newer" - branch (e.g. 2.1, master, etc) for the next release on a weekly basis. +If things went right, you'll see the following when listing the "remotes" of +your project: -GitHub covers the topic of `pull requests`_ in detail. +.. code-block:: terminal -.. note:: + $ git remote -v + origin git@github.com:YOUR-GITHUB-USERNAME/symfony-docs.git (fetch) + origin git@github.com:YOUR-GITHUB-USERNAME/symfony-docs.git (push) + upstream https://github.com/symfony/symfony-docs.git (fetch) + upstream https://github.com/symfony/symfony-docs.git (push) - The Symfony2 documentation is licensed under a Creative Commons - Attribution-Share Alike 3.0 Unported :doc:`License `. +Fetch all the commits of the upstream branches by executing this command: -You can also prefix the title of your pull request in a few cases: +.. code-block:: terminal -* ``[WIP]`` (Work in Progress) is used when you are not yet finished with your - pull request, but you would like it to be reviewed. The pull request won't - be merged until you say it is ready. + $ git fetch upstream -* ``[WCM]`` (Waiting Code Merge) is used when you're documenting a new feature - or change that hasn't been accepted yet into the core code. The pull request - will not be merged until it is merged in the core code (or closed if the - change is rejected). +The purpose of this step is to allow you to work simultaneously on the official +Symfony repository and on your own fork. You'll see this in action in a moment. + +**Step 4.** Create a dedicated **new branch** for your changes. Use a short and +memorable name for the new branch (if you are fixing a reported issue, use +``fix_XXX`` as the branch name, where ``XXX`` is the number of the issue): + +.. code-block:: terminal + + $ git checkout -b improve_install_article upstream/6.4 + +In this example, the name of the branch is ``improve_install_article`` and the +``upstream/6.4`` value tells Git to create this branch based on the ``6.4`` +branch of the ``upstream`` remote, which is the original Symfony Docs repository. + +Fixes should always be based on the **oldest maintained branch** which contains +the error. Nowadays this is the ``6.4`` branch. If you are instead documenting a +new feature, switch to the first Symfony version that included it, e.g. +``upstream/7.2``. + +**Step 5.** Now make your changes in the documentation. Add, tweak, reword and +even remove any content and do your best to comply with the +:doc:`/contributing/documentation/standards`. Then commit your changes! + +.. code-block:: terminal -.. _doc-contributing-pr-format: + # if the modified content existed before + $ git add setup.rst + $ git commit setup.rst -Pull Request Format -~~~~~~~~~~~~~~~~~~~ +**Step 6.** **Push** the changes to your forked repository: -Unless you're fixing some minor typos, the pull request description **must** -include the following checklist to ensure that contributions may be reviewed -without needless feedback loops and that your contributions can be included -into the documentation as quickly as possible: +.. code-block:: terminal -.. code-block:: text + $ git push origin improve_install_article - | Q | A - | ------------- | --- - | Doc fix? | [yes|no] - | New docs? | [yes|no] (PR # on symfony/symfony if applicable) - | Applies to | [Symfony version numbers this applies to] - | Fixed tickets | [comma separated list of tickets fixed by the PR] +The ``origin`` value is the name of the Git remote that corresponds to your +forked repository and ``improve_install_article`` is the name of the branch you +created previously. -An example submission could now look as follows: +**Step 7.** Everything is now ready to initiate a **pull request**. Go to your +forked repository at ``https://github.com/YOUR-GITHUB-USERNAME/symfony-docs`` +and click on the **Pull Requests** link located in the sidebar. -.. code-block:: text +Then, click on the big **New pull request** button. As GitHub cannot guess the +exact changes that you want to propose, select the appropriate branches where +changes should be applied: - | Q | A - | ------------- | --- - | Doc fix? | yes - | New docs? | yes (symfony/symfony#2500) - | Applies to | all (or 2.1+) - | Fixed tickets | #1075 +.. image:: /_images/contributing/docs-pull-request-change-base.png + :alt: The base branch select option on the GitHub page. -.. tip:: +In this example, the **base fork** should be ``symfony/symfony-docs`` and +the **base** branch should be the ``4.4``, which is the branch that you selected +to base your changes on. The **head fork** should be your forked copy +of ``symfony-docs`` and the **compare** branch should be ``improve_install_article``, +which is the name of the branch you created and where you made your changes. - Please be patient. It can take from 15 minutes to several days for your changes - to appear on the symfony.com website after the documentation team merges your - pull request. You can check if your changes have introduced some markup issues - by going to the `Documentation Build Errors`_ page (it is updated each French - night at 3AM when the server rebuilds the documentation). +.. _pull-request-format: -Documenting new Features or Behavior Changes --------------------------------------------- +**Step 8.** The last step is to prepare the **description** of the pull request. +A short phrase or paragraph describing the proposed changes is enough to ensure +that your contribution can be reviewed. -If you're documenting a brand new feature or a change that's been made in -Symfony2, you should precede your description of the change with a ``.. versionadded:: 2.X`` -tag and a short description: +**Step 9.** Now that you've successfully submitted your first contribution to +the Symfony documentation, **go and celebrate!** The documentation managers +will carefully review your work in short time and they will let you know about +any required change. -.. code-block:: text +In case you are asked to add or modify something, don't create a new pull +request. Instead, make sure that you are on the correct branch, make your +changes and push the new changes: - .. versionadded:: 2.2 - The ``askHiddenResponse`` method was added in Symfony 2.2. +.. code-block:: terminal - You can also ask a question and hide the response. This is particularly... + $ cd projects/symfony-docs/ + $ git checkout improve_install_article -If you're documenting a behavior change, it may be helpful to *briefly* describe -how the behavior has changed. + # ... do your changes -.. code-block:: text + $ git push - .. versionadded:: 2.2 - The ``include()`` function is a new Twig feature that's available in - Symfony 2.2. Prior, the ``{% include %}`` tag was used. +It's rare, but you might be asked to rebase your pull request to target another +Symfony branch. Read the :ref:`guide on rebasing pull requests `. -Whenever a new minor version of Symfony2 is released (e.g. 2.3, 2.4, etc), -a new branch of the documentation is created from the ``master`` branch. -At this point, all the ``versionadded`` tags for Symfony2 versions that have -reached end-of-life will be removed. For example, if Symfony 2.5 were released -today, and 2.2 had recently reached its end-of-life, the 2.2 ``versionadded`` -tags would be removed from the new 2.5 branch. +**Step 10.** After your pull request is eventually accepted and merged in the +Symfony documentation, you will be included in the `Symfony Documentation +Contributors`_ list. Moreover, if you happen to have a `SymfonyConnect`_ +profile, you will get a cool `Symfony Documentation Badge`_. -Standards ---------- +Your Next Documentation Contributions +------------------------------------- -In order to help the reader as much as possible and to create code examples that -look and feel familiar, you should follow these rules: +Check you out! You've made your first contribution to the Symfony documentation! +Somebody throw a party! Your first contribution took a little extra time because +you had to learn a few standards and set up your computer. But from now on, +your contributions will be much easier to complete. -* The code follows the :doc:`Symfony Coding Standards` - as well as the `Twig Coding Standards`_; -* Each line should break approximately after the first word that crosses the - 72nd character (so most lines end up being 72-78 characters); -* To avoid horizontal scrolling on code blocks, we prefer to break a line - correctly if it crosses the 85th character; -* When you fold one or more lines of code, place ``...`` in a comment at the point - of the fold. These comments are: ``// ...`` (php), ``# ...`` (yaml/bash), ``{# ... #}`` - (twig), ```` (xml/html), ``; ...`` (ini), ``...`` (text); -* When you fold a part of a line, e.g. a variable value, put ``...`` (without comment) - at the place of the fold; -* Description of the folded code: (optional) - If you fold several lines: the description of the fold can be placed after the ``...`` - If you fold only part of a line: the description can be placed before the line; -* If useful, a ``codeblock`` should begin with a comment containing the filename - of the file in the code block. Don't place a blank line after this comment, - unless the next line is also a comment; -* You should put a ``$`` in front of every bash line; -* The ``::`` shorthand is preferred over ``.. code-block:: php`` to begin a PHP - code block; -* You should use a form of *you* instead of *we*. +Here is a **checklist** of steps that will guide you through your next +contribution to the Symfony docs: -An example:: +.. code-block:: terminal - // src/Foo/Bar.php + # create a new branch based on the oldest maintained version + $ cd projects/symfony-docs/ + $ git fetch upstream + $ git checkout -b my_changes upstream/6.4 - // ... - class Bar - { - // ... + # ... do your changes - public function foo($bar) - { - // set foo with a value of bar - $foo = ...; + # (optional) add your changes if this is a new content + $ git add xxx.rst - // ... check if $bar has the correct value + # commit your changes and push them to your fork + $ git commit xxx.rst + $ git push origin my_changes - return $foo->baz($bar, ...); - } - } + # ... go to GitHub and create the Pull Request -.. caution:: + # (optional) make the changes requested by reviewers and commit them + $ git commit xxx.rst + $ git push - In Yaml you should put a space after ``{`` and before ``}`` (e.g. ``{ _controller: ... }``), - but this should not be done in Twig (e.g. ``{'hello' : 'value'}``). +After completing your next contributions, also watch your ranking improve on +the list of `Symfony Documentation Contributors`_. You guessed right: after all +this hard work, it's **time to celebrate again!** -Reporting an Issue ------------------- +Review your changes +------------------- -The most easy contribution you can make is reporting issues: a typo, a grammar -mistake, a bug in a code example, a missing explanation, and so on. +Symfony repository checks every Pull Request automatically to look for common +errors, inappropriate words, syntax issues in code blocks, etc. -Steps: +Optionally you can also build the docs in your local machine to debug issues or +to read the documentation offline. To do so, follow the instructions included in +`the README file of symfony-docs repository`_. -* Submit a bug in the bug tracker; +Frequently Asked Questions +-------------------------- -* *(optional)* Submit a patch. +Why Do My Changes Take So Long to Be Reviewed and/or Merged? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Translating ------------ +Please be patient. It can take up to several days before your pull request can +be fully reviewed. After merging the changes, it could take again several hours +before your changes appear on the Symfony website. -Read the dedicated :doc:`document `. +Why Should I Use the Oldest Maintained Branch Instead of the Latest Branch? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Consistent with Symfony's source code, the documentation repository is split +into multiple branches, corresponding to the different versions of Symfony itself. +The latest (e.g. ``5.x``) branch holds the documentation for the development branch of +the code. + +Unless you're documenting a feature that was introduced after Symfony 6.4, +your changes should always be based on the ``6.4`` branch. Documentation managers +will use the necessary Git-magic to also apply your changes to all the active +branches of the documentation. + +What If I Want to Submit my Work without Fully Finishing It? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can do it. But please use one of these two prefixes to let reviewers know +about the state of your work: + +* ``[WIP]`` (Work in Progress) is used when you are not yet finished with your + pull request, but you would like it to be reviewed. The pull request won't + be merged until you say it is ready. + +* ``[WCM]`` (Waiting Code Merge) is used when you're documenting a new feature + or change that hasn't been accepted yet into the core code. The pull request + will not be merged until it is merged in the core code (or closed if the + change is rejected). -.. _`fork`: https://help.github.com/articles/fork-a-repo -.. _`pull requests`: https://help.github.com/articles/using-pull-requests -.. _`Documentation Build Errors`: http://symfony.com/doc/build_errors -.. _`Twig Coding Standards`: http://twig.sensiolabs.org/doc/coding_standards.html +Would You Accept a Huge Pull Request with Lots of Changes? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First, make sure that the changes are somewhat related. Otherwise, please create +separate pull requests. Anyway, before submitting a huge change, it's probably a +good idea to open an issue in the Symfony Documentation repository to ask the +managers if they agree with your proposed changes. Otherwise, they could refuse +your proposal after you put all that hard work into making the changes. We +definitely don't want you to waste your time! + +.. _`github.com/symfony/symfony-docs`: https://github.com/symfony/symfony-docs +.. _`reStructuredText`: https://docutils.sourceforge.io/rst.html +.. _`GitHub`: https://github.com/ +.. _`fork the repository`: https://help.github.com/github/getting-started-with-github/fork-a-repo +.. _`Symfony Documentation Contributors`: https://symfony.com/contributors/doc +.. _`SymfonyConnect`: https://symfony.com/connect/login +.. _`Symfony Documentation Badge`: https://connect.symfony.com/badge/36/symfony-documentation-contributor +.. _`the README file of symfony-docs repository`: https://github.com/symfony/symfony-docs#readme diff --git a/contributing/documentation/standards.rst b/contributing/documentation/standards.rst new file mode 100644 index 00000000000..5e195d008fd --- /dev/null +++ b/contributing/documentation/standards.rst @@ -0,0 +1,235 @@ +Documentation Standards +======================= + +Contributions must follow these standards to match the style and tone of the +rest of the Symfony documentation. + +Sphinx +------ + +* The following characters are chosen for different heading levels: level 1 + is ``=`` (equal sign), level 2 ``-`` (dash), level 3 ``~`` (tilde), level 4 + ``.`` (dot) and level 5 ``"`` (double quote); +* Each line should break approximately after the first word that crosses the + 72nd character (so most lines end up being 72-78 characters); +* The ``::`` shorthand is *preferred* over ``.. code-block:: php`` to begin a PHP + code block unless it results in the marker being on its own line (read + `the Sphinx documentation`_ to see when you should use the shorthand); +* Inline hyperlinks are **not** used. Separate the link and their target + definition, which you add on the bottom of the page; +* Inline markup should be closed on the same line as the open-string; + +Example +~~~~~~~ + +.. code-block:: text + + Example + ======= + + When you are working on the docs, you should follow the + `Symfony Documentation`_ standards. + + Level 2 + ------- + + A PHP example would be:: + + echo 'Hello World'; + + Level 3 + ~~~~~~~ + + .. code-block:: php + + echo 'You cannot use the :: shortcut here'; + + .. _`Symfony Documentation`: https://symfony.com/doc + +Code Examples +------------- + +* The code follows the :doc:`Symfony Coding Standards ` + as well as the `Twig Coding Standards`_; +* The code examples should look real for a web application context. Avoid abstract + or trivial examples (``foo``, ``bar``, ``demo``, etc.); +* The code should follow the :doc:`Symfony Best Practices `. +* Use ``Acme`` when the code requires a vendor name; +* Use ``example.com`` as the domain of sample URLs and ``example.org`` and + ``example.net`` when additional domains are required. All of these domains are + `reserved by the IANA`_. +* To avoid horizontal scrolling on code blocks, we prefer to break a line + correctly if it crosses the 85th character; +* When you fold one or more lines of code, place ``...`` in a comment at the point + of the fold. These comments are: ``// ...`` (PHP), ``# ...`` (Yaml/bash), ``{# ... #}`` + (Twig), ```` (XML/HTML), ``; ...`` (INI), ``...`` (text); +* When you fold a part of a line, e.g. a variable value, put ``...`` (without comment) + at the place of the fold; +* Description of the folded code: (optional) + + * If you fold several lines: the description of the fold can be placed after the ``...``; + * If you fold only part of a line: the description can be placed before the line; + +* If useful to the reader, a PHP code example should start with the namespace + declaration; +* When referencing classes, be sure to show the ``use`` statements at the + top of your code block. You don't need to show *all* ``use`` statements + in every example, just show what is actually being used in the code block; +* If useful, a ``codeblock`` should begin with a comment containing the filename + of the file in the code block. Don't place a blank line after this comment, + unless the next line is also a comment; +* You should put a ``$`` in front of every bash line. + +Formats +~~~~~~~ + +Configuration examples should show all supported formats using +:ref:`configuration blocks `. The supported formats +(and their orders) are: + +* **Configuration** (including services): YAML, XML, PHP +* **Routing**: Attributes, YAML, XML, PHP +* **Validation**: Attributes, YAML, XML, PHP +* **Doctrine Mapping**: Attributes, YAML, XML, PHP +* **Translation**: XML, YAML, PHP +* **Code Examples** (if applicable): PHP Symfony, PHP Standalone + +Example +~~~~~~~ + +.. code-block:: php + + // src/Foo/Bar.php + namespace Foo; + + use Acme\Demo\Cat; + // ... + + class Bar + { + // ... + + public function foo($bar): mixed + { + // set foo with a value of bar + $foo = ...; + + $cat = new Cat($foo); + + // ... check if $bar has the correct value + + return $cat->baz($bar, ...); + } + } + +.. warning:: + + In YAML you should put a space after ``{`` and before ``}`` (e.g. ``{ _controller: ... }``), + but this should not be done in Twig (e.g. ``{'hello' : 'value'}``). + +Files and Directories +--------------------- + +* When referencing directories, always add a trailing slash to avoid confusions + with regular files (e.g. "execute the ``console`` script located at the ``bin/`` + directory"). +* When referencing file extensions explicitly, you should include a leading dot + for every extension (e.g. "XML files use the ``.xml`` extension"). +* When you list a Symfony file/directory hierarchy, use ``your-project/`` as the + top-level directory. E.g. + + .. code-block:: text + + your-project/ + ├─ app/ + ├─ src/ + ├─ vendor/ + └─ ... + +Images and Diagrams +------------------- + +* **Diagrams** must adhere to the Symfony docs style. These are created + using the Dia_ application, to make sure everyone can edit them. See the + `README on GitHub`_ for instructions on how to create them. +* All images and diagrams must contain **alt descriptions**: + + * Keep the descriptions concise, do not duplicate information surrounding + the figure; + * Describe complex diagrams in text surrounding the diagram instead of + the alt description. In these cases, alt descriptions must describe + where the longer description can be found (e.g. "These elements are + described further in the next sections"); + * Start descriptions with a capital letter and end with a period; + * Do not start with "A screenshot of", "Diagram of", etc. except when + it's useful to know the exact type (e.g. a specific diagram type). + +.. code-block:: text + + .. image:: /_images/example-screenshot.png + :alt: Some concise description of the screenshot. + + .. raw:: html + + + +English Language Standards +-------------------------- + +Symfony documentation uses the United States English dialect, commonly called +`American English`_. The `American English Oxford Dictionary`_ is used as the +vocabulary reference. + +In addition, documentation follows these rules: + +* **Section titles**: use a variant of the title case, where the first + word is always capitalized and all other words are capitalized, except for + the closed-class words (read Wikipedia article about `headings and titles`_). + + E.g.: The Vitamins are in my Fresh California Raisins + +* **Punctuation**: avoid the use of `Serial (Oxford) Commas`_; +* **Pronouns**: avoid the use of `nosism`_ and always use *you* instead of *we*. + (i.e. avoid the first person point of view: use the second instead); +* **Gender-neutral language**: when referencing a hypothetical person, such as + *"a user with a session cookie"*, use gender-neutral pronouns (they/their/them). + For example, instead of: + + * he or she, use they + * him or her, use them + * his or her, use their + * his or hers, use theirs + * himself or herself, use themselves + +* **Avoid belittling words**: Things that seem "obvious" or "simple" for the + person documenting it, can be the exact opposite for the reader. To make sure + everybody feels comfortable when reading the documentation, try to avoid words + like: + + * basically + * clearly + * easy/easily + * just + * logically + * merely + * obviously + * of course + * quick/quickly + * simply + * trivial + +* **Contractions** are allowed: e.g. you can write ``you would`` as well as ``you'd``, + ``it is`` as well as ``it's``, etc. + +.. _`the Sphinx documentation`: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks +.. _`Twig Coding Standards`: https://twig.symfony.com/doc/3.x/coding_standards.html +.. _`reserved by the IANA`: https://tools.ietf.org/html/rfc2606#section-3 +.. _`American English`: https://en.wikipedia.org/wiki/American_English +.. _`American English Oxford Dictionary`: https://www.lexico.com/definition/american_english +.. _`headings and titles`: https://en.wikipedia.org/wiki/Letter_case#Headings_and_publication_titles +.. _`Serial (Oxford) Commas`: https://en.wikipedia.org/wiki/Serial_comma +.. _`Dia`: http://dia-installer.de/ +.. _`README on GitHub`: https://github.com/symfony/symfony-docs/blob/6.4/_images/sources/README.md +.. _`nosism`: https://en.wikipedia.org/wiki/Nosism diff --git a/contributing/documentation/translations.rst b/contributing/documentation/translations.rst index 88ea2112c25..5ebdecd41e2 100644 --- a/contributing/documentation/translations.rst +++ b/contributing/documentation/translations.rst @@ -1,90 +1,14 @@ Translations ============ -The Symfony2 documentation is written in English and many people are involved -in the translation process. +The official Symfony documentation is published only in English. You can +read about the reasons in `this blog post`_. -Contributing ------------- +We have taken steps to improve the experience when using +`Google Translate`_ to prevent code blocks from being translated. -First, become familiar with the :doc:`markup language ` used by the -documentation. +To translate any page in our documentation please copy any URL from the +documentation and paste it into the form on the Google Translate site. -Then, subscribe to the `Symfony docs mailing-list`_, as collaboration happens -there. - -Finally, find the *master* repository for the language you want to contribute -for. Here is the list of the official *master* repositories: - -* *English*: https://github.com/symfony/symfony-docs -* *French*: https://github.com/symfony-fr/symfony-docs-fr -* *Italian*: https://github.com/garak/symfony-docs-it -* *Japanese*: https://github.com/symfony-japan/symfony-docs-ja -* *Polish*: https://github.com/ampluso/symfony-docs-pl -* *Portuguese (Brazilian)*: https://github.com/andreia/symfony-docs-pt-BR -* *Romanian*: https://github.com/sebio/symfony-docs-ro -* *Russian*: https://github.com/avalanche123/symfony-docs-ru -* *Spanish*: https://github.com/gitnacho/symfony-docs-es -* *Turkish*: https://github.com/symfony-tr/symfony-docs-tr - -.. note:: - - If you want to contribute translations for a new language, read the - :ref:`dedicated section `. - -Joining the Translation Team ----------------------------- - -If you want to help translating some documents for your language or fix some -bugs, consider joining us; it's a very easy process: - -* Introduce yourself on the `Symfony docs mailing-list`_; -* *(optional)* Ask which documents you can work on; -* Fork the *master* repository for your language (click the "Fork" button on - the GitHub page); -* Translate some documents; -* Ask for a pull request (click on the "Pull Request" from your page on - GitHub); -* The team manager accepts your modifications and merges them into the master - repository; -* The documentation website is updated every other night from the master - repository. - -.. _translations-adding-a-new-language: - -Adding a new Language ---------------------- - -This section gives some guidelines for starting the translation of the -Symfony2 documentation for a new language. - -As starting a translation is a lot of work, talk about your plan on the -`Symfony docs mailing-list`_ and try to find motivated people willing to help. - -When the team is ready, nominate a team manager; he will be responsible for -the *master* repository. - -Create the repository and copy the *English* documents. - -The team can now start the translation process. - -When the team is confident that the repository is in a consistent and stable -state (everything is translated, or non-translated documents have been removed -from the toctrees -- files named ``index.rst`` and ``map.rst.inc``), the team -manager can ask that the repository is added to the list of official *master* -repositories by sending an email to Fabien (fabien at symfony.com). - -Maintenance ------------ - -Translation does not end when everything is translated. The documentation is a -moving target (new documents are added, bugs are fixed, paragraphs are -reorganized, ...). The translation team need to closely follow the English -repository and apply changes to the translated documents as soon as possible. - -.. caution:: - - Non maintained languages are removed from the official list of - repositories as obsolete documentation is dangerous. - -.. _Symfony docs mailing-list: http://groups.google.com/group/symfony-docs +.. _`this blog post`: https://symfony.com/blog/discontinuing-the-symfony-community-translations +.. _`Google Translate`: https://translate.google.com diff --git a/contributing/index.rst b/contributing/index.rst index a3177b959f0..c44ee7606a1 100644 --- a/contributing/index.rst +++ b/contributing/index.rst @@ -1,11 +1,4 @@ Contributing ============ -.. toctree:: - :hidden: - - code/index - documentation/index - community/index - .. include:: /contributing/map.rst.inc diff --git a/contributing/map.rst.inc b/contributing/map.rst.inc index 7f19882321a..acbb24bb9b0 100644 --- a/contributing/map.rst.inc +++ b/contributing/map.rst.inc @@ -1,23 +1,40 @@ +* :doc:`The Core Team ` + +* **Code of Conduct** + + * :doc:`/contributing/code_of_conduct/code_of_conduct` + * :doc:`/contributing/code_of_conduct/reporting_guidelines` + * :doc:`/contributing/code_of_conduct/care_team` + * :doc:`/contributing/code_of_conduct/concrete_example_document` + * **Code** * :doc:`Bugs ` - * :doc:`Patches ` + * :doc:`Getting a Stack Trace ` + * :doc:`Pull Requests ` + * :doc:`Reviewing Issues and Pull Requests ` + * :doc:`Maintenance ` * :doc:`Security ` * :doc:`Tests ` - * :doc:`Coding Standards` - * :doc:`Code Conventions` - * :doc:`Git` + * :doc:`Backward Compatibility ` + * :doc:`Coding Standards ` + * :doc:`Code Conventions ` + * :doc:`Git ` * :doc:`License ` * **Documentation** * :doc:`Overview ` * :doc:`Format ` - * :doc:`Translations ` + * :doc:`Documentation Standards ` * :doc:`License ` * **Community** * :doc:`Release Process ` - * :doc:`IRC Meetings ` - * :doc:`Other Resources ` + * :doc:`Respectful Review comments ` + * :doc:`Community Reviews ` + +* **Diversity** + + * :doc:`Governance ` diff --git a/contributing/translations/index.rst b/contributing/translations/index.rst new file mode 100644 index 00000000000..82679a6a0f2 --- /dev/null +++ b/contributing/translations/index.rst @@ -0,0 +1,103 @@ +Contributing Translations +========================= + +Some Symfony Components include certain messages that must be translated to +different languages. For example, if a user submits a form with a wrong value in +a :doc:`TimezoneType ` field, Symfony shows the +following error message by default: "This value is not a valid timezone." + +These messages are translated into tens of languages thanks to the Symfony +community. Symfony adds new messages on a regular basis, so this is an ongoing +translation process and you can help us by providing the missing translations. + +How to Contribute a Translation +------------------------------- + +Imagine that you can speak both English and Swedish and want to check if there's +some missing Swedish translations to contribute them. + +**Step 1.** Translations are contributed to the oldest maintained branch of the +Symfony repository. Visit the `Symfony Releases`_ page to find out which is the +current oldest maintained branch. + +Then, you need to either download or browse that Symfony version contents: + +* If you know Git and prefer the command console, clone the Symfony repository + and check out the oldest maintained branch (read the + :doc:`Symfony Documentation contribution guide ` + if you want to learn about this process); +* If you prefer to use a web based interface, visit + `https://github.com/symfony/symfony `_ + and switch to the oldest maintained branch. + +**Step 2.** Check out if there's some missing translation in your language by +checking these directories: + +* ``src/Symfony/Component/Form/Resources/translations/`` +* ``src/Symfony/Component/Security/Core/Resources/translations/`` +* ``src/Symfony/Component/Validator/Resources/translations/`` + +Symfony uses the :ref:`XLIFF format ` to +store translations. In this example, you are looking for missing Swedish +translations, so you should look for files called ``*.sv.xlf``. + +.. note:: + + If there's no XLIFF file for your language yet, create it yourself + duplicating the original English file (e.g. ``validators.en.xlf``). + +**Step 3.** Contribute the missing translations. To do that, compare the file +in your language to the equivalent file in English. + +Imagine that you open the ``validators.sv.xlf`` and see this at the end of the file: + +.. code-block:: xml + + + + + + This value should be either negative or zero. + Detta värde bör vara antingen negativt eller noll. + + + This value is not a valid timezone. + Detta värde är inte en giltig tidszon. + + +If you open the equivalent ``validators.en.xlf`` file, you can see that the +English file has more messages to translate: + +.. code-block:: xml + + + + + + This value should be either negative or zero. + This value should be either negative or zero. + + + This value is not a valid timezone. + This value is not a valid timezone. + + + This password has been leaked in a data breach, it must not be used. Please use another password. + This password has been leaked in a data breach, it must not be used. Please use another password. + + + This value should be between {{ min }} and {{ max }}. + This value should be between {{ min }} and {{ max }}. + + +The messages with ``id=93`` and ``id=94`` are missing in the Swedish file. +Copy and paste the messages from the English file, translate the content +inside the ```` tag and save the changes. + +**Step 4.** Make the pull request against the +`https://github.com/symfony/symfony `_ repository. +If you need help, check the other Symfony guides about +:doc:`contributing code or docs ` because the process is +the same. + +.. _`Symfony Releases`: https://symfony.com/releases diff --git a/controller.rst b/controller.rst new file mode 100644 index 00000000000..05abdaee4ea --- /dev/null +++ b/controller.rst @@ -0,0 +1,993 @@ +Controller +========== + +A controller is a PHP function you create that reads information from the +``Request`` object and creates and returns a ``Response`` object. The response could +be an HTML page, JSON, XML, a file download, a redirect, a 404 error or anything +else. The controller runs whatever arbitrary logic *your application* needs +to render the content of a page. + +.. tip:: + + If you haven't already created your first working page, check out + :doc:`/page_creation` and then come back! + +A Basic Controller +------------------ + +While a controller can be any PHP callable (function, method on an object, +or a ``Closure``), a controller is usually a method inside a controller +class:: + + // src/Controller/LuckyController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + + class LuckyController + { + #[Route('/lucky/number/{max}', name: 'app_lucky_number')] + public function number(int $max): Response + { + $number = random_int(0, $max); + + return new Response( + 'Lucky number: '.$number.'' + ); + } + } + +The controller is the ``number()`` method, which lives inside the +controller class ``LuckyController``. + +This controller is pretty straightforward: + +* *line 2*: Symfony takes advantage of PHP's namespace functionality to + namespace the entire controller class. + +* *line 4*: Symfony again takes advantage of PHP's namespace functionality: + the ``use`` keyword imports the ``Response`` class, which the controller + must return. + +* *line 7*: The class can technically be called anything, but it's suffixed + with ``Controller`` by convention. + +* *line 10*: The action method is allowed to have a ``$max`` argument thanks to the + ``{max}`` :doc:`wildcard in the route `. + +* *line 14*: The controller creates and returns a ``Response`` object. + +Mapping a URL to a Controller +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to *view* the result of this controller, you need to map a URL to it via +a route. This was done above with the ``#[Route('/lucky/number/{max}')]`` +:ref:`route attribute `. + +To see your page, go to this URL in your browser: http://localhost:8000/lucky/number/100 + +For more information on routing, see :doc:`/routing`. + +.. _the-base-controller-class-services: +.. _the-base-controller-classes-services: + +The Base Controller Class & Services +------------------------------------ + +To aid development, Symfony comes with an optional base controller class called +:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController`. +It can be extended to gain access to helper methods. + +Add the ``use`` statement atop your controller class and then modify +``LuckyController`` to extend it: + +.. code-block:: diff + + // src/Controller/LuckyController.php + namespace App\Controller; + + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + + - class LuckyController + + class LuckyController extends AbstractController + { + // ... + } + +That's it! You now have access to methods like :ref:`$this->render() ` +and many others that you'll learn about next. + +Generating URLs +~~~~~~~~~~~~~~~ + +The :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::generateUrl` +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 +~~~~~~~~~~~ + +If you want to redirect the user to another page, use the ``redirectToRoute()`` +and ``redirect()`` methods:: + + use Symfony\Component\HttpFoundation\RedirectResponse; + use Symfony\Component\HttpFoundation\Response; + + // ... + public function index(): RedirectResponse + { + // redirects to the "homepage" route + return $this->redirectToRoute('homepage'); + + // redirectToRoute is a shortcut for: + // return new RedirectResponse($this->generateUrl('homepage')); + + // does a permanent HTTP 301 redirect + return $this->redirectToRoute('homepage', [], 301); + // if you prefer, you can use PHP constants instead of hardcoded numbers + return $this->redirectToRoute('homepage', [], Response::HTTP_MOVED_PERMANENTLY); + + // redirect to a route with parameters + return $this->redirectToRoute('app_lucky_number', ['max' => 10]); + + // redirects to a route and maintains the original query string parameters + return $this->redirectToRoute('blog_show', $request->query->all()); + + // redirects to the current route (e.g. for Post/Redirect/Get pattern): + return $this->redirectToRoute($request->attributes->get('_route')); + + // redirects externally + return $this->redirect('http://symfony.com/doc'); + } + +.. danger:: + + The ``redirect()`` method does not check its destination in any way. If you + redirect to a URL provided by end-users, your application may be open + to the `unvalidated redirects security vulnerability`_. + +.. _controller-rendering-templates: + +Rendering Templates +~~~~~~~~~~~~~~~~~~~ + +If you're serving HTML, you'll want to render a template. The ``render()`` +method renders a template **and** puts that content into a ``Response`` +object for you:: + + // renders templates/lucky/number.html.twig + return $this->render('lucky/number.html.twig', ['number' => $number]); + +Templating and Twig are explained more in the +:doc:`Creating and Using Templates article `. + +.. _controller-accessing-services: +.. _accessing-other-services: + +Fetching Services +~~~~~~~~~~~~~~~~~ + +Symfony comes *packed* with a lot of useful classes and functionalities, called :doc:`services `. +These are used for rendering templates, sending emails, querying the database and +any other "work" you can think of. + +If you need a service in a controller, type-hint an argument with its class +(or interface) name and Symfony will inject it automatically. This requires +your :doc:`controller to be registered as a service `:: + + use Psr\Log\LoggerInterface; + use Symfony\Component\HttpFoundation\Response; + // ... + + #[Route('/lucky/number/{max}')] + public function number(int $max, LoggerInterface $logger): Response + { + $logger->info('We are logging!'); + // ... + } + +Awesome! + +What other services can you type-hint? To see them, use the ``debug:autowiring`` console +command: + +.. code-block:: terminal + + $ php bin/console debug:autowiring + +.. tip:: + + If you need control over the *exact* value of an argument, or require a parameter, + you can use the ``#[Autowire]`` attribute:: + + // ... + use Psr\Log\LoggerInterface; + use Symfony\Component\DependencyInjection\Attribute\Autowire; + use Symfony\Component\HttpFoundation\Response; + + class LuckyController extends AbstractController + { + public function number( + int $max, + + // inject a specific logger service + #[Autowire(service: 'monolog.logger.request')] + LoggerInterface $logger, + + // or inject parameter values + #[Autowire('%kernel.project_dir%')] + string $projectDir + ): Response + { + $logger->info('We are logging!'); + // ... + } + } + + You can read more about this attribute in :ref:`autowire-attribute`. + +Like with all services, you can also use regular +:ref:`constructor injection ` in your +controllers. + +For more information about services, see the :doc:`/service_container` article. + +Generating Controllers +---------------------- + +To save time, you can install `Symfony Maker`_ and tell Symfony to generate a +new controller class: + +.. code-block:: terminal + + $ 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: + +.. code-block:: terminal + + $ 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 + +Managing Errors and 404 Pages +----------------------------- + +When things are not found, you should return a 404 response. To do this, throw a +special type of exception:: + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + + // ... + public function index(): Response + { + // retrieve the object from database + $product = ...; + if (!$product) { + throw $this->createNotFoundException('The product does not exist'); + + // the above is just a shortcut for: + // throw new NotFoundHttpException('The product does not exist'); + } + + return $this->render(/* ... */); + } + +The :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::createNotFoundException` +method is just a shortcut to create a special +:class:`Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException` +object, which ultimately triggers a 404 HTTP response inside Symfony. + +If you throw an exception that extends or is an instance of +:class:`Symfony\\Component\\HttpKernel\\Exception\\HttpException`, Symfony will +use the appropriate HTTP status code. Otherwise, the response will have a 500 +HTTP status code:: + + // this exception ultimately generates a 500 status error + throw new \Exception('Something went wrong!'); + +In every case, an error page is shown to the end user and a full debug +error page is shown to the developer (i.e. when you're in "Debug" mode - see +:ref:`page-creation-environments`). + +To customize the error page that's shown to the user, see the +:doc:`/controller/error_pages` article. + +.. _controller-request-argument: + +The Request object as a Controller Argument +------------------------------------------- + +What if you need to read query parameters, grab a request header or get access +to an uploaded file? That information is stored in Symfony's ``Request`` +object. To access it in your controller, add it as an argument and +**type-hint it with the Request class**:: + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + // ... + + public function index(Request $request): Response + { + $page = $request->query->get('page', 1); + + // ... + } + +:ref:`Keep reading ` for more information about using the +Request object. + +.. _controller_map-request: + +Automatic Mapping Of The Request +-------------------------------- + +It is possible to automatically map request's payload and/or query parameters to +your controller's action arguments with attributes. + +Mapping Query Parameters Individually +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Let's say a user sends you a request with the following query string: +``https://example.com/dashboard?firstName=John&lastName=Smith&age=27``. +Thanks to the :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapQueryParameter` +attribute, arguments of your controller's action can be automatically fulfilled:: + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; + + // ... + + public function dashboard( + #[MapQueryParameter] string $firstName, + #[MapQueryParameter] string $lastName, + #[MapQueryParameter] int $age, + ): Response + { + // ... + } + +The ``MapQueryParameter`` attribute supports the following argument types: + +* ``\BackedEnum`` +* ``array`` +* ``bool`` +* ``float`` +* ``int`` +* ``string`` +* Objects that extend :class:`Symfony\\Component\\Uid\\AbstractUid` + +.. versionadded:: 7.3 + + Support for ``AbstractUid`` objects was introduced in Symfony 7.3. + +``#[MapQueryParameter]`` can take an optional argument called ``filter``. You can use the +`Validate Filters`_ constants defined in PHP:: + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; + + // ... + + public function dashboard( + #[MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\w+$/'])] string $firstName, + #[MapQueryParameter] string $lastName, + #[MapQueryParameter(filter: \FILTER_VALIDATE_INT)] int $age, + ): Response + { + // ... + } + +.. _controller-mapping-query-string: + +Mapping The Whole Query String +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Another possibility is to map the entire query string into an object that will hold +available query parameters. Let's say you declare the following DTO with its +optional validation constraints:: + + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + + class UserDto + { + public function __construct( + #[Assert\NotBlank] + public string $firstName, + + #[Assert\NotBlank] + public string $lastName, + + #[Assert\GreaterThan(18)] + public int $age, + ) { + } + } + +You can then use the :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapQueryString` +attribute in your controller:: + + use App\Model\UserDto; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapQueryString; + + // ... + + public function dashboard( + #[MapQueryString] UserDto $userDto + ): Response + { + // ... + } + +You can customize the validation groups used during the mapping and also the +HTTP status to return if the validation fails:: + + use Symfony\Component\HttpFoundation\Response; + + // ... + + public function dashboard( + #[MapQueryString( + validationGroups: ['strict', 'edit'], + validationFailedStatusCode: Response::HTTP_UNPROCESSABLE_ENTITY + )] UserDto $userDto + ): Response + { + // ... + } + +The default status code returned if the validation fails is 404. + +If you want to map your object to a nested array in your query using a specific key, +set the ``key`` option in the ``#[MapQueryString]`` attribute:: + + use App\Model\SearchDto; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapQueryString; + + // ... + + public function dashboard( + #[MapQueryString(key: 'search')] SearchDto $searchDto + ): Response + { + // ... + } + +.. versionadded:: 7.3 + + The ``key`` option of ``#[MapQueryString]`` was introduced in Symfony 7.3. + +If you need a valid DTO even when the request query string is empty, set a +default value for your controller arguments:: + + use App\Model\UserDto; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapQueryString; + + // ... + + public function dashboard( + #[MapQueryString] UserDto $userDto = new UserDto() + ): Response + { + // ... + } + +.. _controller-mapping-request-payload: + +Mapping Request Payload +~~~~~~~~~~~~~~~~~~~~~~~ + +When creating an API and dealing with other HTTP methods than ``GET`` (like +``POST`` or ``PUT``), user's data are not stored in the query string +but directly in the request payload, like this: + +.. code-block:: json + + { + "firstName": "John", + "lastName": "Smith", + "age": 28 + } + +In this case, it is also possible to directly map this payload to your DTO by +using the :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapRequestPayload` +attribute:: + + use App\Model\UserDto; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; + + // ... + + public function dashboard( + #[MapRequestPayload] UserDto $userDto + ): Response + { + // ... + } + +This attribute allows you to customize the serialization context as well +as the class responsible of doing the mapping between the request and +your DTO:: + + public function dashboard( + #[MapRequestPayload( + serializationContext: ['...'], + resolver: App\Resolver\UserDtoResolver + )] + UserDto $userDto + ): Response + { + // ... + } + +You can also customize the validation groups used, the status code to return if +the validation fails as well as supported payload formats:: + + use Symfony\Component\HttpFoundation\Response; + + // ... + + public function dashboard( + #[MapRequestPayload( + acceptFormat: 'json', + validationGroups: ['strict', 'read'], + validationFailedStatusCode: Response::HTTP_NOT_FOUND + )] UserDto $userDto + ): Response + { + // ... + } + +The default status code returned if the validation fails is 422. + +.. tip:: + + If you build a JSON API, make sure to declare your route as using the JSON + :ref:`format `. This will make the error handling + output a JSON response in case of validation errors, rather than an HTML page:: + + #[Route('/dashboard', name: 'dashboard', format: 'json')] + +Make sure to install `phpstan/phpdoc-parser`_ and `phpdocumentor/type-resolver`_ +if you want to map a nested array of specific DTOs:: + + public function dashboard( + #[MapRequestPayload] EmployeesDto $employeesDto + ): Response + { + // ... + } + + final class EmployeesDto + { + /** + * @param UserDto[] $users + */ + public function __construct( + public readonly array $users = [] + ) {} + } + +Instead of returning an array of DTO objects, you can tell Symfony to transform +each DTO object into an array and return something like this: + +.. code-block:: json + + [ + { + "firstName": "John", + "lastName": "Smith", + "age": 28 + }, + { + "firstName": "Jane", + "lastName": "Doe", + "age": 30 + } + ] + +To do so, map the parameter as an array and configure the type of each element +using the ``type`` option of the attribute:: + + public function dashboard( + #[MapRequestPayload(type: UserDto::class)] array $users + ): Response + { + // ... + } + +.. versionadded:: 7.1 + + The ``type`` option of ``#[MapRequestPayload]`` was introduced in Symfony 7.1. + +.. _controller_map-uploaded-file: + +Mapping Uploaded Files +~~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides an attribute called ``#[MapUploadedFile]`` to map one or more +``UploadedFile`` objects to controller arguments:: + + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\File\UploadedFile; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapUploadedFile; + use Symfony\Component\Routing\Attribute\Route; + + class UserController extends AbstractController + { + #[Route('/user/picture', methods: ['PUT'])] + public function changePicture( + #[MapUploadedFile] UploadedFile $picture, + ): Response { + // ... + } + } + +In this example, the associated :doc:`argument resolver ` +fetches the ``UploadedFile`` based on the argument name (``$picture``). If no file +is submitted, an ``HttpException`` is thrown. You can change this by making the +controller argument nullable: + +.. code-block:: php-attributes + + #[MapUploadedFile] + ?UploadedFile $document + +The ``#[MapUploadedFile]`` attribute also allows to pass a list of constraints +to apply to the uploaded file:: + + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\File\UploadedFile; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapUploadedFile; + use Symfony\Component\Routing\Attribute\Route; + use Symfony\Component\Validator\Constraints as Assert; + + class UserController extends AbstractController + { + #[Route('/user/picture', methods: ['PUT'])] + public function changePicture( + #[MapUploadedFile([ + new Assert\File(mimeTypes: ['image/png', 'image/jpeg']), + new Assert\Image(maxWidth: 3840, maxHeight: 2160), + ])] + UploadedFile $picture, + ): Response { + // ... + } + } + +The validation constraints are checked before injecting the ``UploadedFile`` into +the controller argument. If there's a constraint violation, an ``HttpException`` +is thrown and the controller's action is not executed. + +If you need to upload a collection of files, map them to an array or a variadic +argument. The given constraint will be applied to all files and if any of them +fails, an ``HttpException`` is thrown: + +.. code-block:: php-attributes + + #[MapUploadedFile(new Assert\File(mimeTypes: ['application/pdf']))] + array $documents + + #[MapUploadedFile(new Assert\File(mimeTypes: ['application/pdf']))] + UploadedFile ...$documents + +Use the ``name`` option to rename the uploaded file to a custom value: + +.. code-block:: php-attributes + + #[MapUploadedFile(name: 'something-else')] + UploadedFile $document + +In addition, you can change the status code of the HTTP exception thrown when +there are constraint violations: + +.. code-block:: php-attributes + + #[MapUploadedFile( + constraints: new Assert\File(maxSize: '2M'), + validationFailedStatusCode: Response::HTTP_REQUEST_ENTITY_TOO_LARGE + )] + UploadedFile $document + +.. versionadded:: 7.1 + + The ``#[MapUploadedFile]`` attribute was introduced in Symfony 7.1. + +Managing the Session +-------------------- + +You can store special messages, called "flash" messages, on the user's session. +By design, flash messages are meant to be used exactly once: they vanish from +the session automatically as soon as you retrieve them. This feature makes +"flash" messages particularly great for storing user notifications. + +For example, imagine you're processing a :doc:`form ` submission:: + +.. configuration-block:: + + .. code-block:: php-symfony + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + // ... + + public function update(Request $request): Response + { + // ... + + if ($form->isSubmitted() && $form->isValid()) { + // do some sort of processing + + $this->addFlash( + 'notice', + 'Your changes were saved!' + ); + // $this->addFlash() is equivalent to $request->getSession()->getFlashBag()->add() + + return $this->redirectToRoute(/* ... */); + } + + return $this->render(/* ... */); + } + +:ref:`Reading ` for more information about using Sessions. + +.. _request-object-info: + +The Request and Response Object +------------------------------- + +As mentioned :ref:`earlier `, Symfony will +pass the ``Request`` object to any controller argument that is type-hinted with +the ``Request`` class:: + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + public function index(Request $request): Response + { + $request->isXmlHttpRequest(); // is it an Ajax request? + + $request->getPreferredLanguage(['en', 'fr']); + + // retrieves GET and POST variables respectively + $request->query->get('page'); + $request->getPayload()->get('page'); + + // retrieves SERVER variables + $request->server->get('HTTP_HOST'); + + // retrieves an instance of UploadedFile identified by foo + $request->files->get('foo'); + + // retrieves a COOKIE value + $request->cookies->get('PHPSESSID'); + + // retrieves an HTTP request header, with normalized, lowercase keys + $request->headers->get('host'); + $request->headers->get('content-type'); + } + +The ``Request`` class has several public properties and methods that return any +information you need about the request. + +Like the ``Request``, the ``Response`` object has a public ``headers`` property. +This object is of the type :class:`Symfony\\Component\\HttpFoundation\\ResponseHeaderBag` +and provides methods for getting and setting response headers. The header names are +normalized. As a result, the name ``Content-Type`` is equivalent to +the name ``content-type`` or ``content_type``. + +In Symfony, a controller is required to return a ``Response`` object:: + + use Symfony\Component\HttpFoundation\Response; + + // creates a simple Response with a 200 status code (the default) + $response = new Response('Hello '.$name, Response::HTTP_OK); + + // creates a CSS-response with a 200 status code + $response = new Response(''); + $response->headers->set('Content-Type', 'text/css'); + +To facilitate this, different response objects are included to address different +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 `. + +.. note:: + + Technically, a controller can return a value other than a ``Response``. + However, your application is responsible for transforming that value into a + ``Response`` object. This is handled using :doc:`events ` + (specifically the :ref:`kernel.view event `), + an advanced feature you'll learn about later. + +Accessing Configuration Values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To get the value of any :ref:`configuration parameter ` +from a controller, use the ``getParameter()`` helper method:: + + // ... + public function index(): Response + { + $contentsDir = $this->getParameter('kernel.project_dir').'/contents'; + // ... + } + +Returning JSON Response +~~~~~~~~~~~~~~~~~~~~~~~ + +To return JSON from a controller, use the ``json()`` helper method. This returns a +``JsonResponse`` object that encodes the data automatically:: + + use Symfony\Component\HttpFoundation\JsonResponse; + // ... + + public function index(): JsonResponse + { + // returns '{"username":"jane.doe"}' and sets the proper Content-Type header + return $this->json(['username' => 'jane.doe']); + + // the shortcut defines three optional arguments + // return $this->json($data, $status = 200, $headers = [], $context = []); + } + +If the :doc:`serializer service ` is enabled in your +application, it will be used to serialize the data to JSON. Otherwise, +the :phpfunction:`json_encode` function is used. + +Streaming File Responses +~~~~~~~~~~~~~~~~~~~~~~~~ + +You can use the :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::file` +helper to serve a file from inside a controller:: + + use Symfony\Component\HttpFoundation\BinaryFileResponse; + // ... + + public function download(): BinaryFileResponse + { + // send the file contents and force the browser to download it + return $this->file('/path/to/some_file.pdf'); + } + +The ``file()`` helper provides some arguments to configure its behavior:: + + use Symfony\Component\HttpFoundation\File\File; + use Symfony\Component\HttpFoundation\ResponseHeaderBag; + // ... + + public function download(): BinaryFileResponse + { + // load the file from the filesystem + $file = new File('/path/to/some_file.pdf'); + + return $this->file($file); + + // rename the downloaded file + return $this->file($file, 'custom_name.pdf'); + + // display the file contents in the browser instead of downloading it + return $this->file('invoice_3241.pdf', 'my_invoice.pdf', ResponseHeaderBag::DISPOSITION_INLINE); + } + +Sending Early Hints +~~~~~~~~~~~~~~~~~~~ + +`Early hints`_ tell the browser to start downloading some assets even before the +application sends the response content. This improves perceived performance +because the browser can prefetch resources that will be needed once the full +response is finally sent. These resources are commonly Javascript or CSS files, +but they can be any type of resource. + +.. note:: + + In order to work, the `SAPI`_ you're using must support this feature, like + `FrankenPHP`_. + +You can send early hints from your controller action thanks to the +:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::sendEarlyHints` +method:: + + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + use Symfony\Component\WebLink\Link; + + class HomepageController extends AbstractController + { + #[Route("/", name: "homepage")] + public function index(): Response + { + $response = $this->sendEarlyHints([ + new Link(rel: 'preconnect', href: 'https://fonts.google.com'), + (new Link(href: '/style.css'))->withAttribute('as', 'style'), + (new Link(href: '/script.js'))->withAttribute('as', 'script'), + ]); + + // prepare the contents of the response... + + return $this->render('homepage/index.html.twig', response: $response); + } + } + +Technically, Early Hints are an informational HTTP response with the status code +``103``. The ``sendEarlyHints()`` method creates a ``Response`` object with that +status code and sends its headers immediately. + +This way, browsers can start downloading the assets immediately; like the +``style.css`` and ``script.js`` files in the above example. The +``sendEarlyHints()`` method also returns the ``Response`` object, which you +must use to create the full response sent from the controller action. + +Final Thoughts +-------------- + +In Symfony, a controller is usually a class method which is used to accept +requests, and return a ``Response`` object. When mapped with a URL, a controller +becomes accessible and its response can be viewed. + +To facilitate the development of controllers, Symfony provides an +``AbstractController``. It can be used to extend the controller class allowing +access to some frequently used utilities such as ``render()`` and +``redirectToRoute()``. The ``AbstractController`` also provides the +``createNotFoundException()`` utility which is used to return a page not found +response. + +In other articles, you'll learn how to use specific services from inside your controller +that will help you persist and fetch objects from a database, process form submissions, +handle caching and more. + +Keep Going! +----------- + +Next, learn all about :doc:`rendering templates with Twig `. + +Learn more about Controllers +---------------------------- + +.. toctree:: + :maxdepth: 1 + :glob: + + controller/* + +.. _`Symfony Maker`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html +.. _`unvalidated redirects security vulnerability`: https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html +.. _`Early hints`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103 +.. _`SAPI`: https://www.php.net/manual/en/function.php-sapi-name.php +.. _`FrankenPHP`: https://frankenphp.dev +.. _`Validate Filters`: https://www.php.net/manual/en/filter.constants.php +.. _`phpstan/phpdoc-parser`: https://packagist.org/packages/phpstan/phpdoc-parser +.. _`phpdocumentor/type-resolver`: https://packagist.org/packages/phpdocumentor/type-resolver diff --git a/controller/error_pages.rst b/controller/error_pages.rst new file mode 100644 index 00000000000..96856764ece --- /dev/null +++ b/controller/error_pages.rst @@ -0,0 +1,385 @@ +How to Customize Error Pages +============================ + +In Symfony applications, all errors are treated as exceptions, no matter if they +are a 404 Not Found error or a fatal error triggered by throwing some exception +in your code. + +In the :ref:`development environment `, +Symfony catches all the exceptions and displays a special **exception page** +with lots of debug information to help you discover the root problem: + +.. image:: /_images/controller/error_pages/exceptions-in-dev-environment.png + :alt: A typical exception page in the development environment with the full stacktrace and log information. + :class: with-browser + +Since these pages contain a lot of sensitive internal information, Symfony won't +display them in the production environment. Instead, it'll show a minimal and +generic **error page**: + +.. image:: /_images/controller/error_pages/errors-in-prod-environment.png + :alt: A typical error page in the production environment. + :class: with-browser + +Error pages for the production environment can be customized in different ways +depending on your needs: + +#. If you only want to change the contents and styles of the error pages to match + the rest of your application, :ref:`override the default error templates `; + +#. If you want to change the contents of non-HTML error output, + :ref:`create a new normalizer `; + +#. If you also want to tweak the logic used by Symfony to generate error pages, + :ref:`override the default error controller `; + +#. If you need total control of exception handling to run your own logic + :ref:`use the kernel.exception event `. + +.. _use-default-error-controller: +.. _using-the-default-errorcontroller: + +Overriding the Default Error Templates +-------------------------------------- + +You can use the built-in Twig error renderer to override the default error +templates. Both the TwigBundle and TwigBridge need to be installed for this. Run +this command to ensure both are installed: + +.. code-block:: terminal + + $ composer require symfony/twig-pack + +When the error page loads, :class:`Symfony\\Bridge\\Twig\\ErrorRenderer\\TwigErrorRenderer` +is used to render a Twig template to show the user. + +.. _controller-error-pages-by-status-code: + +This renderer uses the HTTP status code and the following +logic to determine the template filename: + +#. Look for a template for the given status code (like ``error500.html.twig``); + +#. If the previous template doesn't exist, discard the status code and look for + a generic error template (``error.html.twig``). + +.. _overriding-or-adding-templates: + +To override these templates, rely on the standard Symfony method for +:ref:`overriding templates that live inside a bundle ` and +put them in the ``templates/bundles/TwigBundle/Exception/`` directory. + +A typical project that returns HTML pages might look like this: + +.. code-block:: text + + templates/ + └─ bundles/ + └─ TwigBundle/ + └─ Exception/ + ├─ error404.html.twig + ├─ error403.html.twig + └─ error.html.twig # All other HTML errors (including 500) + +Example 404 Error Template +-------------------------- + +To override the 404 error template for HTML pages, create a new +``error404.html.twig`` template located at ``templates/bundles/TwigBundle/Exception/``: + +.. code-block:: html+twig + + {# templates/bundles/TwigBundle/Exception/error404.html.twig #} + {% extends 'base.html.twig' %} + + {% block body %} +

Page not found

+ +

+ The requested page couldn't be located. Checkout for any URL + misspelling or return to the homepage. +

+ {% endblock %} + +In case you need them, the ``TwigErrorRenderer`` passes some information to +the error template via the ``status_code`` and ``status_text`` variables that +store the HTTP status code and message respectively. + +.. tip:: + + You can customize the status code of an exception by implementing + :class:`Symfony\\Component\\HttpKernel\\Exception\\HttpExceptionInterface` + and its required ``getStatusCode()`` method. Otherwise, the ``status_code`` + will default to ``500``. + +Additionally you have access to the :class:`Symfony\\Component\\HttpKernel\\Exception\\HttpException` +object via the ``exception`` Twig variable. For example, if the exception sets a +message (e.g. using ``throw $this->createNotFoundException('The product does not exist')``), +use ``{{ exception.message }}`` to print that message. You can also output the +stack trace using ``{{ exception.traceAsString }}``, but don't do that for end +users because the trace contains sensitive data. + +.. tip:: + + PHP errors are turned into exceptions as well by default, so you can also + access these error details using ``exception``. + +Security & 404 Pages +-------------------- + +Due to the order of how routing and security are loaded, security information will +*not* be available on your 404 pages. This means that it will appear as if your +user is logged out on the 404 page (it will work while testing, but not on production). + +.. _testing-error-pages: + +Testing Error Pages during Development +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +While you're in the development environment, Symfony shows the big *exception* +page instead of your shiny new customized error page. So, how can you see +what it looks like and debug it? + +Fortunately, the default ``ErrorController`` allows you to preview your +*error* pages during development. + +To use this feature, you need to load some special routes provided by FrameworkBundle +(if the application uses :ref:`Symfony Flex ` they are loaded +automatically when installing ``symfony/framework-bundle``): + +.. configuration-block:: + + .. code-block:: yaml + + # config/routes/framework.yaml + when@dev: + _errors: + resource: '@FrameworkBundle/Resources/config/routing/errors.xml' + prefix: /_error + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/routes/framework.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes): void { + if ('dev' === $routes->env()) { + $routes->import('@FrameworkBundle/Resources/config/routing/errors.xml') + ->prefix('/_error') + ; + } + }; + +With this route added, you can use URLs like these to preview the *error* page +for a given status code as HTML or for a given status code and format (you might +need to replace ``http://localhost/`` by the host used in your local setup): + +* ``http://localhost/_error/{statusCode}`` for HTML +* ``http://localhost/_error/{statusCode}.{format}`` for any other format + +.. _overriding-non-html-error-output: + +Overriding Error output for non-HTML formats +-------------------------------------------- + +To override non-HTML error output, the Serializer component needs to be installed. + +.. code-block:: terminal + + $ composer require symfony/serializer-pack + +The Serializer component has a built-in ``FlattenException`` normalizer +(:class:`Symfony\\Component\\Serializer\\Normalizer\\ProblemNormalizer`) and +JSON/XML/CSV/YAML encoders. When your application throws an exception, Symfony +can output it in one of those formats. If you want to change the output +contents, create a new Normalizer that supports the ``FlattenException`` input:: + + # src/Serializer/MyCustomProblemNormalizer.php + namespace App\Serializer; + + use Symfony\Component\ErrorHandler\Exception\FlattenException; + use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + + class MyCustomProblemNormalizer implements NormalizerInterface + { + public function normalize($exception, ?string $format = null, array $context = []): array + { + return [ + 'content' => 'This is my custom problem normalizer.', + 'exception'=> [ + 'message' => $exception->getMessage(), + 'code' => $exception->getStatusCode(), + ], + ]; + } + + public function supportsNormalization($data, ?string $format = null, array $context = []): bool + { + return $data instanceof FlattenException; + } + } + +.. _custom-error-controller: +.. _replacing-the-default-errorcontroller: + +Overriding the Default ErrorController +-------------------------------------- + +If you need a little more flexibility beyond just overriding the template, +then you can change the controller that renders the error page. For example, +you might need to pass some additional variables into your template. + +To do this, create a new controller anywhere in your application and set +the :ref:`framework.error_controller ` +configuration option to point to it: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + error_controller: App\Controller\ErrorController::show + + .. code-block:: xml + + + + + + + App\Controller\ErrorController::show + + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + $framework->errorController('App\Controller\ErrorController::show'); + }; + +The :class:`Symfony\\Component\\HttpKernel\\EventListener\\ErrorListener` +class used by the FrameworkBundle as a listener of the ``kernel.exception`` event creates +the request that will be dispatched to your controller. In addition, your controller +will be passed two parameters: + +``exception`` + The original :phpclass:`Throwable` instance being handled. + +``logger`` + A :class:`\\Symfony\\Component\\HttpKernel\\Log\\DebugLoggerInterface` + instance which may be ``null`` in some circumstances. + +.. tip:: + + The :ref:`error page preview ` also works for + your own controllers set up this way. + +.. _use-kernel-exception-event: + +Working with the ``kernel.exception`` Event +------------------------------------------- + +When an exception is thrown, the :class:`Symfony\\Component\\HttpKernel\\HttpKernel` +class catches it and dispatches a ``kernel.exception`` event. This gives you the +power to convert the exception into a ``Response`` in a few different ways. + +Working with this event is actually much more powerful than what has been explained +before, but also requires a thorough understanding of Symfony internals. Suppose +that your code throws specialized exceptions with a particular meaning to your +application domain. + +:doc:`Writing your own event listener ` +for the ``kernel.exception`` event allows you to have a closer look at the exception +and take different actions depending on it. Those actions might include logging +the exception, redirecting the user to another page or rendering specialized +error pages. + +.. note:: + + If your listener calls ``setResponse()`` on the + :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` + event, propagation will be stopped and the response will be sent to + the client. + +This approach allows you to create centralized and layered error handling: +instead of catching (and handling) the same exceptions in various controllers +time and again, you can have just one (or several) listeners deal with them. + +.. tip:: + + See :class:`Symfony\\Component\\Security\\Http\\Firewall\\ExceptionListener` + class code for a real example of an advanced listener of this type. This + listener handles various security-related exceptions that are thrown in + your application (like :class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException`) + and takes measures like redirecting the user to the login page, logging them + out and other things. + +Dumping Error Pages as Static HTML Files +---------------------------------------- + +.. versionadded:: 7.3 + + The feature to dump error pages into static HTML files was introduced in Symfony 7.3. + +If an error occurs before reaching your Symfony application, web servers display +their own default error pages instead of your custom ones. Dumping your application's +error pages to static HTML ensures users always see your defined pages and improves +performance by allowing the server to deliver errors instantly without calling +your application. + +Symfony provides the following command to turn your error pages into static HTML files: + +.. code-block:: terminal + + # the first argument is the path where the HTML files are stored + $ APP_ENV=prod php bin/console error:dump var/cache/prod/error_pages/ + + # by default, it generates the pages of all 4xx and 5xx errors, but you can + # pass a list of HTTP status codes to only generate those + $ APP_ENV=prod php bin/console error:dump var/cache/prod/error_pages/ 401 403 404 500 + +You must also configure your web server to use these generated pages. For example, +if you use Nginx: + +.. code-block:: nginx + + # /etc/nginx/conf.d/example.com.conf + server { + # Existing server configuration + # ... + + # Serve static error pages + error_page 400 /error_pages/400.html; + error_page 401 /error_pages/401.html; + # ... + error_page 510 /error_pages/510.html; + error_page 511 /error_pages/511.html; + + location ^~ /error_pages/ { + root /path/to/your/symfony/var/cache/error_pages; + internal; # prevent direct URL access + } + } diff --git a/controller/forwarding.rst b/controller/forwarding.rst new file mode 100644 index 00000000000..8d8be859da5 --- /dev/null +++ b/controller/forwarding.rst @@ -0,0 +1,35 @@ +How to Forward Requests to another Controller +============================================= + +Though not very common, you can also forward to another controller internally +with the ``forward()`` method provided by the +:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController` +class. + +Instead of redirecting the user's browser, this makes an "internal" sub-request +and calls the defined controller. The ``forward()`` method returns the +:class:`Symfony\\Component\\HttpFoundation\\Response` object that is returned +from *that* controller:: + + public function index(string $name): Response + { + $response = $this->forward('App\Controller\OtherController::fancy', [ + 'name' => $name, + 'color' => 'green', + ]); + + // ... further modify the response or return it directly + + return $response; + } + +The array passed to the method becomes the arguments for the resulting controller. +The target controller method might look something like this:: + + public function fancy(string $name, string $color): Response + { + // ... create and return a Response object + } + +Like when creating a controller for a route, the order of the arguments of the +``fancy()`` method doesn't matter: the matching is done by name. diff --git a/controller/service.rst b/controller/service.rst new file mode 100644 index 00000000000..88af093ff29 --- /dev/null +++ b/controller/service.rst @@ -0,0 +1,255 @@ +How to Define Controllers as Services +===================================== + +In Symfony, a controller does *not* need to be registered as a service. But if +you're using the :ref:`default services.yaml configuration `, +and your controllers extend the `AbstractController`_ class, they *are* automatically +registered as services. This means you can use dependency injection like any +other normal service. + +If your controllers don't extend the `AbstractController`_ class, you must +explicitly mark your controller services as ``public``. Alternatively, you can +apply the ``controller.service_arguments`` tag to your controller services. This +will make the tagged services ``public`` and will allow you to inject services +in method parameters: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + + # controllers are imported separately to make sure services can be injected + # as action arguments even if you don't extend any base controller class + App\Controller\: + resource: '../src/Controller/' + tags: ['controller.service_arguments'] + +.. note:: + + If you don't use either :doc:`autowiring ` + or :ref:`autoconfiguration ` and you extend the + ``AbstractController``, you'll need to apply other tags and make some method + calls to register your controllers as services: + + .. code-block:: yaml + + # config/services.yaml + + # this extended configuration is only required when not using autowiring/autoconfiguration, + # which is uncommon and not recommended + + abstract_controller.locator: + class: Symfony\Component\DependencyInjection\ServiceLocator + arguments: + - + router: '@router' + request_stack: '@request_stack' + http_kernel: '@http_kernel' + session: '@session' + parameter_bag: '@parameter_bag' + # you can add more services here as you need them (e.g. the `serializer` + # service) and have a look at the AbstractController class to see + # which services are defined in the locator + + App\Controller\: + resource: '../src/Controller/' + tags: ['controller.service_arguments'] + calls: + - [setContainer, ['@abstract_controller.locator']] + +If you prefer, you can use the ``#[AsController]`` PHP attribute to automatically +apply the ``controller.service_arguments`` tag to your controller services:: + + // src/Controller/HelloController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\AsController; + use Symfony\Component\Routing\Attribute\Route; + + #[AsController] + class HelloController + { + #[Route('/hello', name: 'hello', methods: ['GET'])] + public function index(): Response + { + // ... + } + } + +Registering your controller as a service is the first step, but you also need to +update your routing config to reference the service properly, so that Symfony +knows to use it. + +Use the ``service_id::method_name`` syntax to refer to the controller method. +If the service id is the fully-qualified class name (FQCN) of your controller, +as Symfony recommends, then the syntax is the same as if the controller was not +a service like: ``App\Controller\HelloController::index``: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Controller/HelloController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + + class HelloController + { + #[Route('/hello', name: 'hello', methods: ['GET'])] + public function index(): Response + { + // ... + } + } + + .. code-block:: yaml + + # config/routes.yaml + hello: + path: /hello + controller: App\Controller\HelloController::index + methods: GET + + .. code-block:: xml + + + + + + + + + + .. code-block:: php + + // config/routes.php + use App\Controller\HelloController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes): void { + $routes->add('hello', '/hello') + ->controller([HelloController::class, 'index']) + ->methods(['GET']) + ; + }; + +.. _controller-service-invoke: + +Invokable Controllers +--------------------- + +Controllers can also define a single action using the ``__invoke()`` method, +which is a common practice when following the `ADR pattern`_ +(Action-Domain-Responder): + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Controller/Hello.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + + #[Route('/hello/{name}', name: 'hello')] + class Hello + { + public function __invoke(string $name = 'World'): Response + { + return new Response(sprintf('Hello %s!', $name)); + } + } + + .. code-block:: yaml + + # config/routes.yaml + hello: + path: /hello/{name} + controller: App\Controller\HelloController + + .. code-block:: xml + + + + + + + App\Controller\HelloController + + + + + .. code-block:: php + + use App\Controller\HelloController; + + // app/config/routing.php + $collection->add('hello', new Route('/hello', [ + '_controller' => HelloController::class, + ])); + +Alternatives to base Controller Methods +--------------------------------------- + +When using a controller defined as a service, you can still extend the +:ref:`AbstractController base controller ` +and use its shortcuts. But, you don't need to! You can choose to extend *nothing*, +and use dependency injection to access different services. + +The base `Controller class source code`_ is a great way to see how to accomplish +common tasks. For example, ``$this->render()`` is usually used to render a Twig +template and return a Response. But, you can also do this directly: + +In a controller that's defined as a service, you can instead inject the ``twig`` +service and use it directly:: + + // src/Controller/HelloController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Twig\Environment; + + class HelloController + { + public function __construct( + private Environment $twig, + ) { + } + + public function index(string $name): Response + { + $content = $this->twig->render( + 'hello/index.html.twig', + ['name' => $name] + ); + + return new Response($content); + } + } + +You can also use a special :ref:`action-based dependency injection ` +to receive services as arguments to your controller action methods. + +Base Controller Methods and Their Service Replacements +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The best way to see how to replace base ``Controller`` convenience methods is to +look at the `AbstractController`_ class that holds its logic. + +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/AbstractController.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 new file mode 100644 index 00000000000..cff326a8e2b --- /dev/null +++ b/controller/upload_file.rst @@ -0,0 +1,367 @@ +How to Upload Files +=================== + +.. note:: + + Instead of handling file uploading yourself, you may consider using the + `VichUploaderBundle`_ community bundle. This bundle provides all the common + operations (such as file renaming, saving and deleting) and it's tightly + integrated with Doctrine ORM, MongoDB ODM, PHPCR ODM and Propel. + +Imagine that you have a ``Product`` entity in your application and you want to +add a PDF brochure for each product. To do so, add a new property called +``brochureFilename`` in the ``Product`` entity:: + + // src/Entity/Product.php + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + + class Product + { + // ... + + #[ORM\Column(type: 'string')] + private string $brochureFilename; + + public function getBrochureFilename(): string + { + return $this->brochureFilename; + } + + public function setBrochureFilename(string $brochureFilename): self + { + $this->brochureFilename = $brochureFilename; + + return $this; + } + } + +Note that the type of the ``brochureFilename`` column is ``string`` instead of +``binary`` or ``blob`` because it only stores the PDF file name instead of the +file contents. + +The next step is to add a new field to the form that manages the ``Product`` +entity. This must be a ``FileType`` field so the browsers can display the file +upload widget. The trick to make it work is to add the form field as "unmapped", +so Symfony doesn't try to get/set its value from the related entity:: + + // src/Form/ProductType.php + namespace App\Form; + + use App\Entity\Product; + use Symfony\Component\Form\AbstractType; + use Symfony\Component\Form\Extension\Core\Type\FileType; + use Symfony\Component\Form\FormBuilderInterface; + use Symfony\Component\OptionsResolver\OptionsResolver; + use Symfony\Component\Validator\Constraints\File; + + class ProductType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + // ... + ->add('brochure', FileType::class, [ + 'label' => 'Brochure (PDF file)', + + // unmapped means that this field is not associated to any entity property + 'mapped' => false, + + // make it optional so you don't have to re-upload the PDF file + // every time you edit the Product details + 'required' => false, + + // unmapped fields can't define their validation using attributes + // in the associated entity, so you can use the PHP constraint classes + 'constraints' => [ + new File( + maxSize: '1024k', + mimeTypes: [ + 'application/pdf', + 'application/x-pdf', + ], + mimeTypesMessage: 'Please upload a valid PDF document', + ) + ], + ]) + // ... + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Product::class, + ]); + } + } + +Now, update the template that renders the form to display the new ``brochure`` +field (the exact template code to add depends on the method used by your application +to :doc:`customize form rendering `): + +.. code-block:: html+twig + + {# templates/product/new.html.twig #} +

Adding a new product

+ + {{ form_start(form) }} + {# ... #} + + {{ form_row(form.brochure) }} + {{ form_end(form) }} + +Finally, you need to update the code of the controller that handles the form:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use App\Entity\Product; + use App\Form\ProductType; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\DependencyInjection\Attribute\Autowire; + use Symfony\Component\HttpFoundation\File\Exception\FileException; + use Symfony\Component\HttpFoundation\File\UploadedFile; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + use Symfony\Component\String\Slugger\SluggerInterface; + + class ProductController extends AbstractController + { + #[Route('/product/new', name: 'app_product_new')] + public function new( + Request $request, + SluggerInterface $slugger, + #[Autowire('%kernel.project_dir%/public/uploads/brochures')] string $brochuresDirectory + ): Response + { + $product = new Product(); + $form = $this->createForm(ProductType::class, $product); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + /** @var UploadedFile $brochureFile */ + $brochureFile = $form->get('brochure')->getData(); + + // this condition is needed because the 'brochure' field is not required + // so the PDF file must be processed only when a file is uploaded + if ($brochureFile) { + $originalFilename = pathinfo($brochureFile->getClientOriginalName(), PATHINFO_FILENAME); + // this is needed to safely include the file name as part of the URL + $safeFilename = $slugger->slug($originalFilename); + $newFilename = $safeFilename.'-'.uniqid().'.'.$brochureFile->guessExtension(); + + // Move the file to the directory where brochures are stored + try { + $brochureFile->move($brochuresDirectory, $newFilename); + } catch (FileException $e) { + // ... handle exception if something happens during file upload + } + + // updates the 'brochureFilename' property to store the PDF file name + // instead of its contents + $product->setBrochureFilename($newFilename); + } + + // ... persist the $product variable or any other work + + return $this->redirectToRoute('app_product_list'); + } + + return $this->render('product/new.html.twig', [ + 'form' => $form, + ]); + } + } + +There are some important things to consider in the code of the above controller: + +#. In Symfony applications, uploaded files are objects of the + :class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile` class. This class + provides methods for the most common operations when dealing with uploaded files; +#. A well-known security best practice is to never trust the input provided by + users. This also applies to the files uploaded by your visitors. The ``UploadedFile`` + class provides methods to get the original file extension + (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalExtension`), + the original file size (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getSize`), + the original file name (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalName`) + and the original file path (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalPath`). + However, they are considered *not safe* because a malicious user could tamper + that information. That's why it's always better to generate a unique name and + use the :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::guessExtension` + method to let Symfony guess the right extension according to the file MIME type; + +.. note:: + + If a directory was uploaded, ``getClientOriginalPath()`` will contain + the **webkitRelativePath** as provided by the browser. Otherwise this + value will be identical to ``getClientOriginalName()``. + +.. versionadded:: 7.1 + + The ``getClientOriginalPath()`` method was introduced in Symfony 7.1. + +You can use the following code to link to the PDF brochure of a product: + +.. code-block:: html+twig + + View brochure (PDF) + +.. tip:: + + When creating a form to edit an already persisted item, the file form type + still expects a :class:`Symfony\\Component\\HttpFoundation\\File\\File` + instance. As the persisted entity now contains only the relative file path, + you first have to concatenate the configured upload path with the stored + filename and create a new ``File`` class:: + + use Symfony\Component\HttpFoundation\File\File; + // ... + + $product->setBrochureFilename( + new File($brochuresDirectory.DIRECTORY_SEPARATOR.$product->getBrochureFilename()) + ); + +Creating an Uploader Service +---------------------------- + +To avoid logic in controllers, making them big, you can extract the upload +logic to a separate service:: + + // src/Service/FileUploader.php + namespace App\Service; + + use Symfony\Component\HttpFoundation\File\Exception\FileException; + use Symfony\Component\HttpFoundation\File\UploadedFile; + use Symfony\Component\String\Slugger\SluggerInterface; + + class FileUploader + { + public function __construct( + private string $targetDirectory, + private SluggerInterface $slugger, + ) { + } + + public function upload(UploadedFile $file): string + { + $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME); + $safeFilename = $this->slugger->slug($originalFilename); + $fileName = $safeFilename.'-'.uniqid().'.'.$file->guessExtension(); + + try { + $file->move($this->getTargetDirectory(), $fileName); + } catch (FileException $e) { + // ... handle exception if something happens during file upload + } + + return $fileName; + } + + public function getTargetDirectory(): string + { + return $this->targetDirectory; + } + } + +.. tip:: + + In addition to the generic :class:`Symfony\\Component\\HttpFoundation\\File\\Exception\\FileException` + class there are other exception classes to handle failed file uploads: + :class:`Symfony\\Component\\HttpFoundation\\File\\Exception\\CannotWriteFileException`, + :class:`Symfony\\Component\\HttpFoundation\\File\\Exception\\ExtensionFileException`, + :class:`Symfony\\Component\\HttpFoundation\\File\\Exception\\FormSizeFileException`, + :class:`Symfony\\Component\\HttpFoundation\\File\\Exception\\IniSizeFileException`, + :class:`Symfony\\Component\\HttpFoundation\\File\\Exception\\NoFileException`, + :class:`Symfony\\Component\\HttpFoundation\\File\\Exception\\NoTmpDirFileException`, + and :class:`Symfony\\Component\\HttpFoundation\\File\\Exception\\PartialFileException`. + +Then, define a service for this class: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\Service\FileUploader: + arguments: + $targetDirectory: '%brochures_directory%' + + .. code-block:: xml + + + + + + + + %brochures_directory% + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Service\FileUploader; + + return static function (ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set(FileUploader::class) + ->arg('$targetDirectory', '%brochures_directory%') + ; + }; + +Now you're ready to use this service in the controller:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use App\Service\FileUploader; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + // ... + public function new(Request $request, FileUploader $fileUploader): Response + { + // ... + + if ($form->isSubmitted() && $form->isValid()) { + /** @var UploadedFile $brochureFile */ + $brochureFile = $form->get('brochure')->getData(); + if ($brochureFile) { + $brochureFileName = $fileUploader->upload($brochureFile); + $product->setBrochureFilename($brochureFileName); + } + + // ... + } + + // ... + } + +Using a Doctrine Listener +------------------------- + +The previous versions of this article explained how to handle file uploads using +: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 +unwillingly (because your listener persists entities which cause other entities to +be changed and persisted). + +As an alternative, you can use :doc:`Symfony events, listeners and subscribers `. + +.. _`VichUploaderBundle`: https://github.com/dustin10/VichUploaderBundle diff --git a/controller/value_resolver.rst b/controller/value_resolver.rst new file mode 100644 index 00000000000..1844ff0c9be --- /dev/null +++ b/controller/value_resolver.rst @@ -0,0 +1,444 @@ +Extending Action Argument Resolving +=================================== + +In the :doc:`controller guide
`, you've learned that you can get the +:class:`Symfony\\Component\\HttpFoundation\\Request` object via an argument in +your controller. This argument has to be type-hinted by the ``Request`` class +in order to be recognized. This is done via the +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver`. By +creating and registering custom value resolvers, you can extend this +functionality. + +.. _functionality-shipped-with-the-httpkernel: + +Built-In Value Resolvers +------------------------ + +Symfony ships with the following value resolvers in the +:doc:`HttpKernel component `: + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\BackedEnumValueResolver` + Attempts to resolve a backed enum case from a route path parameter that matches the name of the argument. + Leads to a 404 Not Found response if the value isn't a valid backing value for the enum type. + + For example, if your backed enum is:: + + namespace App\Model; + + enum Suit: string + { + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'S'; + } + + And your controller contains the following:: + + class CardController + { + #[Route('/cards/{suit}')] + public function list(Suit $suit): Response + { + // ... + } + + // ... + } + + When requesting the ``/cards/H`` URL, the ``$suit`` variable will store the + ``Suit::Hearts`` case. + + Furthermore, you can limit route parameter's allowed values to + only one (or more) with ``EnumRequirement``:: + + use Symfony\Component\Routing\Requirement\EnumRequirement; + + // ... + + class CardController + { + #[Route('/cards/{suit}', requirements: [ + // this allows all values defined in the Enum + 'suit' => new EnumRequirement(Suit::class), + // this restricts the possible values to the Enum values listed here + 'suit' => new EnumRequirement([Suit::Diamonds, Suit::Spades]), + ])] + public function list(Suit $suit): Response + { + // ... + } + + // ... + } + + The example above allows requesting only ``/cards/D`` and ``/cards/S`` + URLs and leads to 404 Not Found response in two other cases. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\RequestPayloadValueResolver` + Maps the request payload or the query string into the type-hinted object. + + Because this is a :ref:`targeted value resolver `, + you'll have to use either the :ref:`MapRequestPayload ` + or the :ref:`MapQueryString ` attribute + in order to use this resolver. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\RequestAttributeValueResolver` + Attempts to find a request attribute that matches the name of the argument. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\DateTimeValueResolver` + Attempts to find a request attribute that matches the name of the argument + and injects a ``DateTimeInterface`` object if type-hinted with a class + extending ``DateTimeInterface``. + + By default any input that can be parsed as a date string by PHP is accepted. + You can restrict how the input can be formatted with the + :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapDateTime` attribute. + + .. tip:: + + The ``DateTimeInterface`` object is generated with the :doc:`Clock component `. + This gives you full control over the date and time values the controller + receives when testing your application and using the + :class:`Symfony\\Component\\Clock\\MockClock` implementation. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\RequestValueResolver` + Injects the current ``Request`` if type-hinted with ``Request`` or a class + extending ``Request``. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\ServiceValueResolver` + Injects a service if type-hinted with a valid service class or interface. This + works like :doc:`autowiring `. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\SessionValueResolver` + Injects the configured session class implementing ``SessionInterface`` if + type-hinted with ``SessionInterface`` or a class implementing + ``SessionInterface``. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\DefaultValueResolver` + Will set the default value of the argument if present and the argument + is optional. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\UidValueResolver` + Attempts to convert any UID values from a route path parameter into UID objects. + Leads to a 404 Not Found response if the value isn't a valid UID. + + For example, the following will convert the token parameter into a ``UuidV4`` object:: + + // src/Controller/DefaultController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + use Symfony\Component\Uid\UuidV4; + + class DefaultController + { + #[Route('/share/{token}')] + public function share(UuidV4 $token): Response + { + // ... + } + } + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\VariadicValueResolver` + Verifies if the request data is an array and will add all of them to the + argument list. When the action is called, the last (variadic) argument will + contain all the values of this array. + +In addition, some components, bridges and official bundles provide other value resolvers: + +:class:`Symfony\\Component\\Security\\Http\\Controller\\UserValueResolver` + Injects the object that represents the current logged in user if type-hinted + with ``UserInterface``. You can also type-hint your own ``User`` class but you + must then add the ``#[CurrentUser]`` attribute to the argument. Default value + can be set to ``null`` in case the controller can be accessed by anonymous + users. It requires installing the :doc:`SecurityBundle `. + + If the argument is not nullable and there is no logged in user or the logged in + user has a user class not matching the type-hinted class, an ``AccessDeniedException`` + is thrown by the resolver to prevent access to the controller. + +:class:`Symfony\\Component\\Security\\Http\\Controller\\SecurityTokenValueResolver` + Injects the object that represents the current logged in token if type-hinted + with ``TokenInterface`` or a class extending it. + + If the argument is not nullable and there is no logged in token, an ``HttpException`` + with status code 401 is thrown by the resolver to prevent access to the controller. + +:class:`Symfony\\Bridge\\Doctrine\\ArgumentResolver\\EntityValueResolver` + Automatically query for an entity and pass it as an argument to your controller. + + For example, the following will query the ``Product`` entity which has ``{id}`` as primary key:: + + // src/Controller/DefaultController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + + class DefaultController + { + #[Route('/product/{id}')] + public function share(Product $product): Response + { + // ... + } + } + + To learn more about the use of the ``EntityValueResolver``, see the dedicated + section :ref:`Automatically Fetching Objects `. + +PSR-7 Objects Resolver: + Injects a Symfony HttpFoundation ``Request`` object created from a PSR-7 object + of type ``Psr\Http\Message\ServerRequestInterface``, + ``Psr\Http\Message\RequestInterface`` or ``Psr\Http\Message\MessageInterface``. + It requires installing :doc:`the PSR-7 Bridge ` component. + +Managing Value Resolvers +------------------------ + +For each argument, every resolver tagged with ``controller.argument_value_resolver`` +will be called until one provides a value. The order in which they are called depends +on their priority. For example, the ``SessionValueResolver`` will be called before the +``DefaultValueResolver`` because its priority is higher. This allows to write e.g. +``SessionInterface $session = null`` to get the session if there is one, or ``null`` +if there is none. + +In that specific case, you don't need any resolver running before +``SessionValueResolver``, so skipping them would not only improve performance, +but also prevent one of them providing a value before ``SessionValueResolver`` +has a chance to. + +The :class:`Symfony\\Component\\HttpKernel\\Attribute\\ValueResolver` attribute +lets you do this by "targeting" the resolver you want:: + + // src/Controller/SessionController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpFoundation\Session\SessionInterface; + use Symfony\Component\HttpKernel\Attribute\ValueResolver; + use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver; + use Symfony\Component\Routing\Attribute\Route; + + class SessionController + { + #[Route('/')] + public function __invoke( + #[ValueResolver(SessionValueResolver::class)] + SessionInterface $session = null + ): Response + { + // ... + } + } + +In the example above, the ``SessionValueResolver`` will be called first because +it is targeted. The ``DefaultValueResolver`` will be called next if no value has +been provided; that's why you can assign ``null`` as ``$session``'s default value. + +You can target a resolver by passing its name as ``ValueResolver``'s first argument. +For convenience, built-in resolvers' name are their FQCN. + +A targeted resolver can also be disabled by passing ``ValueResolver``'s ``$disabled`` +argument to ``true``; this is how :ref:`MapEntity allows to disable the +EntityValueResolver for a specific controller `. +Yes, ``MapEntity`` extends ``ValueResolver``! + +Adding a Custom Value Resolver +------------------------------ + +In the next example, you'll create a value resolver to inject an ID value +object whenever a controller argument has a type implementing +``IdentifierInterface`` (e.g. ``BookingId``):: + + // src/Controller/BookingController.php + namespace App\Controller; + + use App\Reservation\BookingId; + use Symfony\Component\HttpFoundation\Response; + + class BookingController + { + public function index(BookingId $id): Response + { + // ... do something with $id + } + } + +Adding a new value resolver requires creating a class that implements +:class:`Symfony\\Component\\HttpKernel\\Controller\\ValueResolverInterface` +and defining a service for it. + +This interface contains a ``resolve()`` method, which is called for each +argument of the controller. It receives the current ``Request`` object and an +:class:`Symfony\\Component\\HttpKernel\\ControllerMetadata\\ArgumentMetadata` +instance, which contains all information from the method signature. + +The ``resolve()`` method should return either an empty array (if it cannot resolve +this argument) or an array with the resolved value(s). Usually arguments are +resolved as a single value, but variadic arguments require resolving multiple +values. That's why you must always return an array, even for single values:: + + // src/ValueResolver/IdentifierValueResolver.php + namespace App\ValueResolver; + + use App\IdentifierInterface; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; + use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; + + class BookingIdValueResolver implements ValueResolverInterface + { + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + // get the argument type (e.g. BookingId) + $argumentType = $argument->getType(); + if ( + !$argumentType + || !is_subclass_of($argumentType, IdentifierInterface::class, true) + ) { + return []; + } + + // get the value from the request, based on the argument name + $value = $request->attributes->get($argument->getName()); + if (!is_string($value)) { + return []; + } + + // create and return the value object + return [$argumentType::fromString($value)]; + } + } + +This method first checks whether it can resolve the value: + +* The argument must be type-hinted with a class implementing a custom ``IdentifierInterface``; +* The argument name (e.g. ``$id``) must match the name of a request + attribute (e.g. using a ``/booking/{id}`` route placeholder). + +When those requirements are met, the method creates a new instance of the +custom value object and returns it as the value for this argument. + +That's it! Now all you have to do is add the configuration for the service +container. This can be done by adding one of the following tags to your value resolver. + +``controller.argument_value_resolver`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This tag is automatically added to every service implementing ``ValueResolverInterface``, +but you can set it yourself to change its ``priority`` or ``name`` attributes. + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + _defaults: + # ... be sure autowiring is enabled + autowire: true + # ... + + App\ValueResolver\BookingIdValueResolver: + tags: + - controller.argument_value_resolver: + name: booking_id + priority: 150 + + .. code-block:: xml + + + + + + + + + + + + controller.argument_value_resolver + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\ValueResolver\BookingIdValueResolver; + + return static function (ContainerConfigurator $containerConfigurator): void { + $services = $containerConfigurator->services(); + + $services->set(BookingIdValueResolver::class) + ->tag('controller.argument_value_resolver', ['name' => 'booking_id', 'priority' => 150]) + ; + }; + +While adding a priority is optional, it's recommended to add one to make sure +the expected value is injected. The built-in ``RequestAttributeValueResolver``, +which fetches attributes from the ``Request``, has a priority of ``100``. If your +resolver also fetches ``Request`` attributes, set a priority of ``100`` or more. +Otherwise, set a priority lower than ``100`` to make sure the argument resolver +is not triggered when the ``Request`` attribute is present. + +To ensure your resolvers are added in the right position you can run the following +command to see which argument resolvers are present and in which order they run: + +.. code-block:: terminal + + $ php bin/console debug:container debug.argument_resolver.inner + +You can also configure the name passed to the ``ValueResolver`` attribute to target +your resolver. Otherwise it will default to the service's id. + +.. _value-resolver-targeted: + +``controller.targeted_value_resolver`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Set this tag if you want your resolver to be called only if it is targeted by a +``ValueResolver`` attribute. Like ``controller.argument_value_resolver``, you +can customize the name by which your resolver can be targeted. + +As an alternative, you can add the +:class:`Symfony\\Component\\HttpKernel\\Attribute\\AsTargetedValueResolver` attribute +to your resolver and pass your custom name as its first argument:: + + // src/ValueResolver/IdentifierValueResolver.php + namespace App\ValueResolver; + + use Symfony\Component\HttpKernel\Attribute\AsTargetedValueResolver; + use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; + + #[AsTargetedValueResolver('booking_id')] + class BookingIdValueResolver implements ValueResolverInterface + { + // ... + } + +You can then pass this name as ``ValueResolver``'s first argument to target your resolver:: + + // src/Controller/BookingController.php + namespace App\Controller; + + use App\Reservation\BookingId; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\ValueResolver; + + class BookingController + { + public function index(#[ValueResolver('booking_id')] BookingId $id): Response + { + // ... do something with $id + } + } diff --git a/cookbook/assetic/apply_to_option.rst b/cookbook/assetic/apply_to_option.rst deleted file mode 100644 index 68558b2e3a1..00000000000 --- a/cookbook/assetic/apply_to_option.rst +++ /dev/null @@ -1,182 +0,0 @@ -.. index:: - single: Assetic; Apply filters - -How to Apply an Assetic Filter to a Specific File Extension -=========================================================== - -Assetic filters can be applied to individual files, groups of files or even, -as you'll see here, files that have a specific extension. To show you how -to handle each option, let's suppose that you want to use Assetic's CoffeeScript -filter, which compiles CoffeeScript files into Javascript. - -The main configuration is just the paths to coffee and node. These default -respectively to ``/usr/bin/coffee`` and ``/usr/bin/node``: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - coffee: - bin: /usr/bin/coffee - node: /usr/bin/node - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', array( - 'filters' => array( - 'coffee' => array( - 'bin' => '/usr/bin/coffee', - 'node' => '/usr/bin/node', - ), - ), - )); - -Filter a Single File --------------------- - -You can now serve up a single CoffeeScript file as JavaScript from within your -templates: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% javascripts '@AcmeFooBundle/Resources/public/js/example.coffee' filter='coffee' %} - - {% endjavascripts %} - - .. code-block:: html+php - - javascripts( - array('@AcmeFooBundle/Resources/public/js/example.coffee'), - array('coffee') - ) as $url): ?> - - - -This is all that's needed to compile this CoffeeScript file and server it -as the compiled JavaScript. - -Filter Multiple Files ---------------------- - -You can also combine multiple CoffeeScript files into a single output file: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% javascripts '@AcmeFooBundle/Resources/public/js/example.coffee' - '@AcmeFooBundle/Resources/public/js/another.coffee' - filter='coffee' %} - - {% endjavascripts %} - - .. code-block:: html+php - - javascripts( - array( - '@AcmeFooBundle/Resources/public/js/example.coffee', - '@AcmeFooBundle/Resources/public/js/another.coffee', - ), - array('coffee') - ) as $url): ?> - - - -Both the files will now be served up as a single file compiled into regular -JavaScript. - -.. _cookbook-assetic-apply-to: - -Filtering based on a File Extension ------------------------------------ - -One of the great advantages of using Assetic is reducing the number of asset -files to lower HTTP requests. In order to make full use of this, it would -be good to combine *all* your JavaScript and CoffeeScript files together -since they will ultimately all be served as JavaScript. Unfortunately just -adding the JavaScript files to the files to be combined as above will not -work as the regular JavaScript files will not survive the CoffeeScript compilation. - -This problem can be avoided by using the ``apply_to`` option in the config, -which allows you to specify that a filter should always be applied to particular -file extensions. In this case you can specify that the Coffee filter is -applied to all ``.coffee`` files: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - coffee: - bin: /usr/bin/coffee - node: /usr/bin/node - apply_to: "\.coffee$" - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', array( - 'filters' => array( - 'coffee' => array( - 'bin' => '/usr/bin/coffee', - 'node' => '/usr/bin/node', - 'apply_to' => '\.coffee$', - ), - ), - )); - -With this, you no longer need to specify the ``coffee`` filter in the template. -You can also list regular JavaScript files, all of which will be combined -and rendered as a single JavaScript file (with only the ``.coffee`` files -being run through the CoffeeScript filter): - -.. configuration-block:: - - .. code-block:: html+jinja - - {% javascripts '@AcmeFooBundle/Resources/public/js/example.coffee' - '@AcmeFooBundle/Resources/public/js/another.coffee' - '@AcmeFooBundle/Resources/public/js/regular.js' %} - - {% endjavascripts %} - - .. code-block:: html+php - - javascripts( - array( - '@AcmeFooBundle/Resources/public/js/example.coffee', - '@AcmeFooBundle/Resources/public/js/another.coffee', - '@AcmeFooBundle/Resources/public/js/regular.js', - ) - ) as $url): ?> - - diff --git a/cookbook/assetic/asset_management.rst b/cookbook/assetic/asset_management.rst deleted file mode 100644 index e47b60e8b87..00000000000 --- a/cookbook/assetic/asset_management.rst +++ /dev/null @@ -1,440 +0,0 @@ -.. index:: - single: Assetic; Introduction - -How to Use Assetic for Asset Management -======================================= - -Assetic combines two major ideas: :ref:`assets` and -:ref:`filters`. The assets are files such as CSS, -JavaScript and image files. The filters are things that can be applied to -these files before they are served to the browser. This allows a separation -between the asset files stored in the application and the files actually presented -to the user. - -Without Assetic, you just serve the files that are stored in the application -directly: - -.. configuration-block:: - - .. code-block:: html+jinja - - - {% endjavascripts %} - - .. code-block:: html+php - - javascripts( - array('@AcmeFooBundle/Resources/public/js/*') - ) as $url): ?> - - - -.. tip:: - - You can also include CSS Stylesheets: see :ref:`cookbook-assetic-including-css`. - -In this example, all of the files in the ``Resources/public/js/`` directory -of the ``AcmeFooBundle`` will be loaded and served from a different location. -The actual rendered tag might simply look like: - -.. code-block:: html - - - -This is a key point: once you let Assetic handle your assets, the files are -served from a different location. This *will* cause problems with CSS files -that reference images by their relative path. See :ref:`cookbook-assetic-cssrewrite`. - -.. _cookbook-assetic-including-css: - -Including CSS Stylesheets -~~~~~~~~~~~~~~~~~~~~~~~~~ - -To bring in CSS stylesheets, you can use the same methodologies seen -above, except with the ``stylesheets`` tag. If you're using the default -block names from the Symfony Standard Distribution, this will usually live -inside a ``stylesheets`` block: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% stylesheets 'bundles/acme_foo/css/*' filter='cssrewrite' %} - - {% endstylesheets %} - - .. code-block:: html+php - - stylesheets( - array('bundles/acme_foo/css/*'), - array('cssrewrite') - ) as $url): ?> - - - -But because Assetic changes the paths to your assets, this *will* break any -background images (or other paths) that uses relative paths, unless you use -the :ref:`cssrewrite` filter. - -.. note:: - - Notice that in the original example that included JavaScript files, you - referred to the files using a path like ``@AcmeFooBundle/Resources/public/file.js``, - but that in this example, you referred to the CSS files using their actual, - publicly-accessible path: ``bundles/acme_foo/css``. You can use either, except - that there is a known issue that causes the ``cssrewrite`` filter to fail - when using the ``@AcmeFooBundle`` syntax for CSS Stylesheets. - -.. _cookbook-assetic-cssrewrite: - -Fixing CSS Paths with the ``cssrewrite`` Filter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Since Assetic generates new URLs for your assets, any relative paths inside -your CSS files will break. To fix this, make sure to use the ``cssrewrite`` -filter with your ``stylesheets`` tag. This parses your CSS files and corrects -the paths internally to reflect the new location. - -You can see an example in the previous section. - -.. caution:: - - When using the ``cssrewrite`` filter, don't refer to your CSS files using - the ``@AcmeFooBundle``. See the note in the above section for details. - -Combining Assets -~~~~~~~~~~~~~~~~ - -One feature of Assetic is that it will combine many files into one. This helps -to reduce the number of HTTP requests, which is great for front end performance. -It also allows you to maintain the files more easily by splitting them into -manageable parts. This can help with re-usability as you can easily split -project-specific files from those which can be used in other applications, -but still serve them as a single file: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% javascripts - '@AcmeFooBundle/Resources/public/js/*' - '@AcmeBarBundle/Resources/public/js/form.js' - '@AcmeBarBundle/Resources/public/js/calendar.js' %} - - {% endjavascripts %} - - .. code-block:: html+php - - javascripts( - array( - '@AcmeFooBundle/Resources/public/js/*', - '@AcmeBarBundle/Resources/public/js/form.js', - '@AcmeBarBundle/Resources/public/js/calendar.js', - ) - ) as $url): ?> - - - -In the ``dev`` environment, each file is still served individually, so that -you can debug problems more easily. However, in the ``prod`` environment -(or more specifically, when the ``debug`` flag is ``false``), this will be -rendered as a single ``script`` tag, which contains the contents of all of -the JavaScript files. - -.. tip:: - - If you're new to Assetic and try to use your application in the ``prod`` - environment (by using the ``app.php`` controller), you'll likely see - that all of your CSS and JS breaks. Don't worry! This is on purpose. - For details on using Assetic in the ``prod`` environment, see :ref:`cookbook-assetic-dumping`. - -And combining files doesn't only apply to *your* files. You can also use Assetic to -combine third party assets, such as jQuery, with your own into a single file: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% javascripts - '@AcmeFooBundle/Resources/public/js/thirdparty/jquery.js' - '@AcmeFooBundle/Resources/public/js/*' %} - - {% endjavascripts %} - - .. code-block:: html+php - - javascripts( - array( - '@AcmeFooBundle/Resources/public/js/thirdparty/jquery.js', - '@AcmeFooBundle/Resources/public/js/*', - ) - ) as $url): ?> - - - -.. _cookbook-assetic-filters: - -Filters -------- - -Once they're managed by Assetic, you can apply filters to your assets before -they are served. This includes filters that compress the output of your assets -for smaller file sizes (and better front-end optimization). Other filters -can compile JavaScript file from CoffeeScript files and process SASS into CSS. -In fact, Assetic has a long list of available filters. - -Many of the filters do not do the work directly, but use existing third-party -libraries to do the heavy-lifting. This means that you'll often need to install -a third-party library to use a filter. The great advantage of using Assetic -to invoke these libraries (as opposed to using them directly) is that instead -of having to run them manually after you work on the files, Assetic will -take care of this for you and remove this step altogether from your development -and deployment processes. - -To use a filter, you first need to specify it in the Assetic configuration. -Adding a filter here doesn't mean it's being used - it just means that it's -available to use (you'll use the filter below). - -For example to use the JavaScript YUI Compressor the following config should -be added: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - yui_js: - jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar" - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', array( - 'filters' => array( - 'yui_js' => array( - 'jar' => '%kernel.root_dir%/Resources/java/yuicompressor.jar', - ), - ), - )); - -Now, to actually *use* the filter on a group of JavaScript files, add it -into your template: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% javascripts '@AcmeFooBundle/Resources/public/js/*' filter='yui_js' %} - - {% endjavascripts %} - - .. code-block:: html+php - - javascripts( - array('@AcmeFooBundle/Resources/public/js/*'), - array('yui_js') - ) as $url): ?> - - - -A more detailed guide about configuring and using Assetic filters as well as -details of Assetic's debug mode can be found in :doc:`/cookbook/assetic/yuicompressor`. - -Controlling the URL used ------------------------- - -If you wish to, you can control the URLs that Assetic produces. This is -done from the template and is relative to the public document root: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% javascripts '@AcmeFooBundle/Resources/public/js/*' output='js/compiled/main.js' %} - - {% endjavascripts %} - - .. code-block:: html+php - - javascripts( - array('@AcmeFooBundle/Resources/public/js/*'), - array(), - array('output' => 'js/compiled/main.js') - ) as $url): ?> - - - -.. note:: - - Symfony also contains a method for cache *busting*, where the final URL - generated by Assetic contains a query parameter that can be incremented - via configuration on each deployment. For more information, see the - :ref:`ref-framework-assets-version` configuration option. - -.. _cookbook-assetic-dumping: - -Dumping Asset Files -------------------- - -In the ``dev`` environment, Assetic generates paths to CSS and JavaScript -files that don't physically exist on your computer. But they render nonetheless -because an internal Symfony controller opens the files and serves back the -content (after running any filters). - -This kind of dynamic serving of processed assets is great because it means -that you can immediately see the new state of any asset files you change. -It's also bad, because it can be quite slow. If you're using a lot of filters, -it might be downright frustrating. - -Fortunately, Assetic provides a way to dump your assets to real files, instead -of being generated dynamically. - -Dumping Asset Files in the ``prod`` environment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In the ``prod`` environment, your JS and CSS files are represented by a single -tag each. In other words, instead of seeing each JavaScript file you're including -in your source, you'll likely just see something like this: - -.. code-block:: html - - - -Moreover, that file does **not** actually exist, nor is it dynamically rendered -by Symfony (as the asset files are in the ``dev`` environment). This is on -purpose - letting Symfony generate these files dynamically in a production -environment is just too slow. - -Instead, each time you use your app in the ``prod`` environment (and therefore, -each time you deploy), you should run the following task: - -.. code-block:: bash - - $ php app/console assetic:dump --env=prod --no-debug - -This will physically generate and write each file that you need (e.g. ``/js/abcd123.js``). -If you update any of your assets, you'll need to run this again to regenerate -the file. - -Dumping Asset Files in the ``dev`` environment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -By default, each asset path generated in the ``dev`` environment is handled -dynamically by Symfony. This has no disadvantage (you can see your changes -immediately), except that assets can load noticeably slow. If you feel like -your assets are loading too slowly, follow this guide. - -First, tell Symfony to stop trying to process these files dynamically. Make -the following change in your ``config_dev.yml`` file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_dev.yml - assetic: - use_controller: false - - .. code-block:: xml - - - - - .. code-block:: php - - // app/config/config_dev.php - $container->loadFromExtension('assetic', array( - 'use_controller' => false, - )); - -Next, since Symfony is no longer generating these assets for you, you'll -need to dump them manually. To do so, run the following: - -.. code-block:: bash - - $ php app/console assetic:dump - -This physically writes all of the asset files you need for your ``dev`` -environment. The big disadvantage is that you need to run this each time -you update an asset. Fortunately, by passing the ``--watch`` option, the -command will automatically regenerate assets *as they change*: - -.. code-block:: bash - - $ php app/console assetic:dump --watch - -Since running this command in the ``dev`` environment may generate a bunch -of files, it's usually a good idea to point your generated assets files to -some isolated directory (e.g. ``/js/compiled``), to keep things organized: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% javascripts '@AcmeFooBundle/Resources/public/js/*' output='js/compiled/main.js' %} - - {% endjavascripts %} - - .. code-block:: html+php - - javascripts( - array('@AcmeFooBundle/Resources/public/js/*'), - array(), - array('output' => 'js/compiled/main.js') - ) as $url): ?> - - diff --git a/cookbook/assetic/index.rst b/cookbook/assetic/index.rst deleted file mode 100644 index f91943bfdda..00000000000 --- a/cookbook/assetic/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Assetic -======= - -.. toctree:: - :maxdepth: 2 - - asset_management - yuicompressor - jpeg_optimize - apply_to_option diff --git a/cookbook/assetic/jpeg_optimize.rst b/cookbook/assetic/jpeg_optimize.rst deleted file mode 100644 index f02c18d986d..00000000000 --- a/cookbook/assetic/jpeg_optimize.rst +++ /dev/null @@ -1,256 +0,0 @@ -.. index:: - single: Assetic; Image optimization - -How to Use Assetic For Image Optimization with Twig Functions -============================================================= - -Amongst its many filters, Assetic has four filters which can be used for on-the-fly -image optimization. This allows you to get the benefits of smaller file sizes -without having to use an image editor to process each image. The results -are cached and can be dumped for production so there is no performance hit -for your end users. - -Using Jpegoptim ---------------- - -`Jpegoptim`_ is a utility for optimizing JPEG files. To use it with Assetic, -add the following to the Assetic config: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - jpegoptim: - bin: path/to/jpegoptim - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', array( - 'filters' => array( - 'jpegoptim' => array( - 'bin' => 'path/to/jpegoptim', - ), - ), - )); - -.. note:: - - Notice that to use jpegoptim, you must have it already installed on your - system. The ``bin`` option points to the location of the compiled binary. - -It can now be used from a template: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% image '@AcmeFooBundle/Resources/public/images/example.jpg' - filter='jpegoptim' output='/images/example.jpg' %} - Example - {% endimage %} - - .. code-block:: html+php - - images( - array('@AcmeFooBundle/Resources/public/images/example.jpg'), - array('jpegoptim') - ) as $url): ?> - Example - - -Removing all EXIF Data -~~~~~~~~~~~~~~~~~~~~~~ - -By default, running this filter only removes some of the meta information -stored in the file. Any EXIF data and comments are not removed, but you can -remove these by using the ``strip_all`` option: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - jpegoptim: - bin: path/to/jpegoptim - strip_all: true - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', array( - 'filters' => array( - 'jpegoptim' => array( - 'bin' => 'path/to/jpegoptim', - 'strip_all' => 'true', - ), - ), - )); - -Lowering Maximum Quality -~~~~~~~~~~~~~~~~~~~~~~~~ - -The quality level of the JPEG is not affected by default. You can gain -further file size reductions by setting the max quality setting lower than -the current level of the images. This will of course be at the expense of -image quality: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - jpegoptim: - bin: path/to/jpegoptim - max: 70 - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', array( - 'filters' => array( - 'jpegoptim' => array( - 'bin' => 'path/to/jpegoptim', - 'max' => '70', - ), - ), - )); - -Shorter syntax: Twig Function ------------------------------ - -If you're using Twig, it's possible to achieve all of this with a shorter -syntax by enabling and using a special Twig function. Start by adding the -following config: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - jpegoptim: - bin: path/to/jpegoptim - twig: - functions: - jpegoptim: ~ - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', array( - 'filters' => array( - 'jpegoptim' => array( - 'bin' => 'path/to/jpegoptim', - ), - ), - 'twig' => array( - 'functions' => array('jpegoptim'), - ), - ), - )); - -The Twig template can now be changed to the following: - -.. code-block:: html+jinja - - Example - -You can specify the output directory in the config in the following way: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - jpegoptim: - bin: path/to/jpegoptim - twig: - functions: - jpegoptim: { output: images/*.jpg } - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', array( - 'filters' => array( - 'jpegoptim' => array( - 'bin' => 'path/to/jpegoptim', - ), - ), - 'twig' => array( - 'functions' => array( - 'jpegoptim' => array( - output => 'images/*.jpg' - ), - ), - ), - )); - -.. _`Jpegoptim`: http://www.kokkonen.net/tjko/projects.html diff --git a/cookbook/assetic/yuicompressor.rst b/cookbook/assetic/yuicompressor.rst deleted file mode 100644 index 7524d6741da..00000000000 --- a/cookbook/assetic/yuicompressor.rst +++ /dev/null @@ -1,163 +0,0 @@ -.. index:: - single: Assetic; YUI Compressor - -How to Minify JavaScripts and Stylesheets with YUI Compressor -============================================================= - -Yahoo! provides an excellent utility for minifying JavaScripts and stylesheets -so they travel over the wire faster, the `YUI Compressor`_. Thanks to Assetic, -you can take advantage of this tool very easily. - -Download the YUI Compressor JAR -------------------------------- - -The YUI Compressor is written in Java and distributed as a JAR. `Download the JAR`_ -from the Yahoo! site and save it to ``app/Resources/java/yuicompressor.jar``. - -Configure the YUI Filters -------------------------- - -Now you need to configure two Assetic filters in your application, one for -minifying JavaScripts with the YUI Compressor and one for minifying -stylesheets: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - # java: "/usr/bin/java" - filters: - yui_css: - jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar" - yui_js: - jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar" - - .. code-block:: xml - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', array( - // 'java' => '/usr/bin/java', - 'filters' => array( - 'yui_css' => array( - 'jar' => '%kernel.root_dir%/Resources/java/yuicompressor.jar', - ), - 'yui_js' => array( - 'jar' => '%kernel.root_dir%/Resources/java/yuicompressor.jar', - ), - ), - )); - -.. note:: - - Windows users need to remember to update config to proper java location. - In Windows7 x64 bit by default it's ``C:\Program Files (x86)\Java\jre6\bin\java.exe``. - -You now have access to two new Assetic filters in your application: -``yui_css`` and ``yui_js``. These will use the YUI Compressor to minify -stylesheets and JavaScripts, respectively. - -Minify your Assets ------------------- - -You have YUI Compressor configured now, but nothing is going to happen until -you apply one of these filters to an asset. Since your assets are a part of -the view layer, this work is done in your templates: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% javascripts '@AcmeFooBundle/Resources/public/js/*' filter='yui_js' %} - - {% endjavascripts %} - - .. code-block:: html+php - - javascripts( - array('@AcmeFooBundle/Resources/public/js/*'), - array('yui_js') - ) as $url): ?> - - - -.. note:: - - The above example assumes that you have a bundle called ``AcmeFooBundle`` - and your JavaScript files are in the ``Resources/public/js`` directory under - your bundle. This isn't important however - you can include your Javascript - files no matter where they are. - -With the addition of the ``yui_js`` filter to the asset tags above, you should -now see minified JavaScripts coming over the wire much faster. The same process -can be repeated to minify your stylesheets. - -.. configuration-block:: - - .. code-block:: html+jinja - - {% stylesheets '@AcmeFooBundle/Resources/public/css/*' filter='yui_css' %} - - {% endstylesheets %} - - .. code-block:: html+php - - stylesheets( - array('@AcmeFooBundle/Resources/public/css/*'), - array('yui_css') - ) as $url): ?> - - - -Disable Minification in Debug Mode ----------------------------------- - -Minified JavaScripts and Stylesheets are very difficult to read, let alone -debug. Because of this, Assetic lets you disable a certain filter when your -application is in debug mode. You can do this by prefixing the filter name -in your template with a question mark: ``?``. This tells Assetic to only -apply this filter when debug mode is off. - -.. configuration-block:: - - .. code-block:: html+jinja - - {% javascripts '@AcmeFooBundle/Resources/public/js/*' filter='?yui_js' %} - - {% endjavascripts %} - - .. code-block:: html+php - - javascripts( - array('@AcmeFooBundle/Resources/public/js/*'), - array('?yui_js') - ) as $url): ?> - - - - -.. tip:: - - Instead of adding the filter to the asset tags, you can also globally - enable it by adding the apply-to attribute to the filter configuration, for - example in the yui_js filter ``apply_to: "\.js$"``. To only have the filter - applied in production, add this to the config_prod file rather than the - common config file. For details on applying filters by file extension, - see :ref:`cookbook-assetic-apply-to`. - - -.. _`YUI Compressor`: http://developer.yahoo.com/yui/compressor/ -.. _`Download the JAR`: http://yuilibrary.com/projects/yuicompressor/ diff --git a/cookbook/bundles/best_practices.rst b/cookbook/bundles/best_practices.rst deleted file mode 100644 index 337e802b475..00000000000 --- a/cookbook/bundles/best_practices.rst +++ /dev/null @@ -1,287 +0,0 @@ -.. index:: - single: Bundle; Best practices - -How to use Best Practices for Structuring Bundles -================================================= - -A bundle is a directory that has a well-defined structure and can host anything -from classes to controllers and web resources. Even if bundles are very -flexible, you should follow some best practices if you want to distribute them. - -.. index:: - pair: Bundle; Naming conventions - -.. _bundles-naming-conventions: - -Bundle Name ------------ - -A bundle is also a PHP namespace. The namespace must follow the technical -interoperability `standards`_ for PHP 5.3 namespaces and class names: it -starts with a vendor segment, followed by zero or more category segments, and -it ends with the namespace short name, which must end with a ``Bundle`` -suffix. - -A namespace becomes a bundle as soon as you add a bundle class to it. The -bundle class name must follow these simple rules: - -* Use only alphanumeric characters and underscores; -* Use a CamelCased name; -* Use a descriptive and short name (no more than 2 words); -* Prefix the name with the concatenation of the vendor (and optionally the - category namespaces); -* Suffix the name with ``Bundle``. - -Here are some valid bundle namespaces and class names: - -+-----------------------------------+--------------------------+ -| Namespace | Bundle Class Name | -+===================================+==========================+ -| ``Acme\Bundle\BlogBundle`` | ``AcmeBlogBundle`` | -+-----------------------------------+--------------------------+ -| ``Acme\Bundle\Social\BlogBundle`` | ``AcmeSocialBlogBundle`` | -+-----------------------------------+--------------------------+ -| ``Acme\BlogBundle`` | ``AcmeBlogBundle`` | -+-----------------------------------+--------------------------+ - -By convention, the ``getName()`` method of the bundle class should return the -class name. - -.. note:: - - If you share your bundle publicly, you must use the bundle class name as - the name of the repository (``AcmeBlogBundle`` and not ``BlogBundle`` - for instance). - -.. note:: - - Symfony2 core Bundles do not prefix the Bundle class with ``Symfony`` - and always add a ``Bundle`` subnamespace; for example: - :class:`Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle`. - -Each bundle has an alias, which is the lower-cased short version of the bundle -name using underscores (``acme_hello`` for ``AcmeHelloBundle``, or -``acme_social_blog`` for ``Acme\Social\BlogBundle`` for instance). This alias -is used to enforce uniqueness within a bundle (see below for some usage -examples). - -Directory Structure -------------------- - -The basic directory structure of a ``HelloBundle`` bundle must read as -follows: - -.. code-block:: text - - XXX/... - HelloBundle/ - HelloBundle.php - Controller/ - Resources/ - meta/ - LICENSE - config/ - doc/ - index.rst - translations/ - views/ - public/ - Tests/ - -The ``XXX`` directory(ies) reflects the namespace structure of the bundle. - -The following files are mandatory: - -* ``HelloBundle.php``; -* ``Resources/meta/LICENSE``: The full license for the code; -* ``Resources/doc/index.rst``: The root file for the Bundle documentation. - -.. note:: - - These conventions ensure that automated tools can rely on this default - structure to work. - -The depth of sub-directories should be kept to the minimal for most used -classes and files (2 levels at a maximum). More levels can be defined for -non-strategic, less-used files. - -The bundle directory is read-only. If you need to write temporary files, store -them under the ``cache/`` or ``log/`` directory of the host application. Tools -can generate files in the bundle directory structure, but only if the generated -files are going to be part of the repository. - -The following classes and files have specific emplacements: - -+------------------------------+-----------------------------+ -| Type | Directory | -+==============================+=============================+ -| Commands | ``Command/`` | -+------------------------------+-----------------------------+ -| Controllers | ``Controller/`` | -+------------------------------+-----------------------------+ -| Service Container Extensions | ``DependencyInjection/`` | -+------------------------------+-----------------------------+ -| Event Listeners | ``EventListener/`` | -+------------------------------+-----------------------------+ -| Configuration | ``Resources/config/`` | -+------------------------------+-----------------------------+ -| Web Resources | ``Resources/public/`` | -+------------------------------+-----------------------------+ -| Translation files | ``Resources/translations/`` | -+------------------------------+-----------------------------+ -| Templates | ``Resources/views/`` | -+------------------------------+-----------------------------+ -| Unit and Functional Tests | ``Tests/`` | -+------------------------------+-----------------------------+ - -Classes -------- - -The bundle directory structure is used as the namespace hierarchy. For -instance, a ``HelloController`` controller is stored in -``Bundle/HelloBundle/Controller/HelloController.php`` and the fully qualified -class name is ``Bundle\HelloBundle\Controller\HelloController``. - -All classes and files must follow the Symfony2 coding :doc:`standards -`. - -Some classes should be seen as facades and should be as short as possible, like -Commands, Helpers, Listeners, and Controllers. - -Classes that connect to the Event Dispatcher should be suffixed with -``Listener``. - -Exceptions classes should be stored in an ``Exception`` sub-namespace. - -Vendors -------- - -A bundle must not embed third-party PHP libraries. It should rely on the -standard Symfony2 autoloading instead. - -A bundle should not embed third-party libraries written in JavaScript, CSS, or -any other language. - -Tests ------ - -A bundle should come with a test suite written with PHPUnit and stored under -the ``Tests/`` directory. Tests should follow the following principles: - -* The test suite must be executable with a simple ``phpunit`` command run from - a sample application; -* The functional tests should only be used to test the response output and - some profiling information if you have some; -* The tests should cover at least 95% of the code base. - -.. note:: - A test suite must not contain ``AllTests.php`` scripts, but must rely on the - existence of a ``phpunit.xml.dist`` file. - -Documentation -------------- - -All classes and functions must come with full PHPDoc. - -Extensive documentation should also be provided in the :doc:`reStructuredText -` format, under the ``Resources/doc/`` -directory; the ``Resources/doc/index.rst`` file is the only mandatory file and -must be the entry point for the documentation. - -Controllers ------------ - -As a best practice, controllers in a bundle that's meant to be distributed -to others must not extend the -:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` base class. -They can implement -:class:`Symfony\\Component\\DependencyInjection\\ContainerAwareInterface` or -extend :class:`Symfony\\Component\\DependencyInjection\\ContainerAware` -instead. - -.. note:: - - If you have a look at - :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` methods, - you will see that they are only nice shortcuts to ease the learning curve. - -Routing -------- - -If the bundle provides routes, they must be prefixed with the bundle alias. -For an AcmeBlogBundle for instance, all routes must be prefixed with -``acme_blog_``. - -Templates ---------- - -If a bundle provides templates, they must use Twig. A bundle must not provide -a main layout, except if it provides a full working application. - -Translation Files ------------------ - -If a bundle provides message translations, they must be defined in the XLIFF -format; the domain should be named after the bundle name (``bundle.hello``). - -A bundle must not override existing messages from another bundle. - -Configuration -------------- - -To provide more flexibility, a bundle can provide configurable settings by -using the Symfony2 built-in mechanisms. - -For simple configuration settings, rely on the default ``parameters`` entry of -the Symfony2 configuration. Symfony2 parameters are simple key/value pairs; a -value being any valid PHP value. Each parameter name should start with the -bundle alias, though this is just a best-practice suggestion. The rest of the -parameter name will use a period (``.``) to separate different parts (e.g. -``acme_hello.email.from``). - -The end user can provide values in any configuration file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - parameters: - acme_hello.email.from: fabien@example.com - - .. code-block:: xml - - - - fabien@example.com - - - .. code-block:: php - - // app/config/config.php - $container->setParameter('acme_hello.email.from', 'fabien@example.com'); - - .. code-block:: ini - - ; app/config/config.ini - [parameters] - acme_hello.email.from = fabien@example.com - -Retrieve the configuration parameters in your code from the container:: - - $container->getParameter('acme_hello.email.from'); - -Even if this mechanism is simple enough, you are highly encouraged to use the -semantic configuration described in the cookbook. - -.. note:: - - If you are defining services, they should also be prefixed with the bundle - alias. - -Learn more from the Cookbook ----------------------------- - -* :doc:`/cookbook/bundles/extension` - -.. _standards: http://symfony.com/PSR0 diff --git a/cookbook/bundles/extension.rst b/cookbook/bundles/extension.rst deleted file mode 100644 index 6ede7320fab..00000000000 --- a/cookbook/bundles/extension.rst +++ /dev/null @@ -1,605 +0,0 @@ -.. index:: - single: Configuration; Semantic - single: Bundle; Extension configuration - -How to expose a Semantic Configuration for a Bundle -=================================================== - -If you open your application configuration file (usually ``app/config/config.yml``), -you'll see a number of different configuration "namespaces", such as ``framework``, -``twig``, and ``doctrine``. Each of these configures a specific bundle, allowing -you to configure things at a high level and then let the bundle make all the -low-level, complex changes that result. - -For example, the following tells the ``FrameworkBundle`` to enable the form -integration, which involves the defining of quite a few services as well -as integration of other related components: - -.. configuration-block:: - - .. code-block:: yaml - - framework: - # ... - form: true - - .. code-block:: xml - - - - - - .. code-block:: php - - $container->loadFromExtension('framework', array( - // ... - 'form' => true, - // ... - )); - -When you create a bundle, you have two choices on how to handle configuration: - -1. **Normal Service Configuration** (*easy*): - - You can specify your services in a configuration file (e.g. ``services.yml``) - that lives in your bundle and then import it from your main application - configuration. This is really easy, quick and totally effective. If you - make use of :ref:`parameters`, then - you still have the flexibility to customize your bundle from your application - configuration. See ":ref:`service-container-imports-directive`" for more - details. - -2. **Exposing Semantic Configuration** (*advanced*): - - This is the way configuration is done with the core bundles (as described - above). The basic idea is that, instead of having the user override individual - parameters, you let the user configure just a few, specifically created - options. As the bundle developer, you then parse through that configuration - and load services inside an "Extension" class. With this method, you won't - need to import any configuration resources from your main application - configuration: the Extension class can handle all of this. - -The second option - which you'll learn about in this article - is much more -flexible, but also requires more time to setup. If you're wondering which -method you should use, it's probably a good idea to start with method #1, -and then change to #2 later if you need to. - -The second method has several specific advantages: - -* Much more powerful than simply defining parameters: a specific option value - might trigger the creation of many service definitions; - -* Ability to have configuration hierarchy - -* Smart merging when several configuration files (e.g. ``config_dev.yml`` - and ``config.yml``) override each other's configuration; - -* Configuration validation (if you use a :ref:`Configuration Class`); - -* IDE auto-completion when you create an XSD and developers use XML. - -.. sidebar:: Overriding bundle parameters - - If a Bundle provides an Extension class, then you should generally *not* - override any service container parameters from that bundle. The idea - is that if an Extension class is present, every setting that should be - configurable should be present in the configuration made available by - that class. In other words the extension class defines all the publicly - supported configuration settings for which backward compatibility will - be maintained. - -.. index:: - single: Bundle; Extension - single: Dependency Injection; Extension - -Creating an Extension Class ---------------------------- - -If you do choose to expose a semantic configuration for your bundle, you'll -first need to create a new "Extension" class, which will handle the process. -This class should live in the ``DependencyInjection`` directory of your bundle -and its name should be constructed by replacing the ``Bundle`` suffix of the -Bundle class name with ``Extension``. For example, the Extension class of -``AcmeHelloBundle`` would be called ``AcmeHelloExtension``:: - - // Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php - namespace Acme\HelloBundle\DependencyInjection; - - use Symfony\Component\HttpKernel\DependencyInjection\Extension; - use Symfony\Component\DependencyInjection\ContainerBuilder; - - class AcmeHelloExtension extends Extension - { - public function load(array $configs, ContainerBuilder $container) - { - // ... where all of the heavy logic is done - } - - public function getXsdValidationBasePath() - { - return __DIR__.'/../Resources/config/'; - } - - public function getNamespace() - { - return 'http://www.example.com/symfony/schema/'; - } - } - -.. note:: - - The ``getXsdValidationBasePath`` and ``getNamespace`` methods are only - required if the bundle provides optional XSD's for the configuration. - -The presence of the previous class means that you can now define an ``acme_hello`` -configuration namespace in any configuration file. The namespace ``acme_hello`` -is constructed from the extension's class name by removing the word ``Extension`` -and then lowercasing and underscoring the rest of the name. In other words, -``AcmeHelloExtension`` becomes ``acme_hello``. - -You can begin specifying configuration under this namespace immediately: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - acme_hello: ~ - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('acme_hello', array()); - -.. tip:: - - If you follow the naming conventions laid out above, then the ``load()`` - method of your extension code is always called as long as your bundle - is registered in the Kernel. In other words, even if the user does not - provide any configuration (i.e. the ``acme_hello`` entry doesn't even - appear), the ``load()`` method will be called and passed an empty ``$configs`` - array. You can still provide some sensible defaults for your bundle if - you want. - -Parsing the ``$configs`` Array ------------------------------- - -Whenever a user includes the ``acme_hello`` namespace in a configuration file, -the configuration under it is added to an array of configurations and -passed to the ``load()`` method of your extension (Symfony2 automatically -converts XML and YAML to an array). - -Take the following configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - acme_hello: - foo: fooValue - bar: barValue - - .. code-block:: xml - - - - - - - - barValue - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('acme_hello', array( - 'foo' => 'fooValue', - 'bar' => 'barValue', - )); - -The array passed to your ``load()`` method will look like this:: - - array( - array( - 'foo' => 'fooValue', - 'bar' => 'barValue', - ), - ) - -Notice that this is an *array of arrays*, not just a single flat array of the -configuration values. This is intentional. For example, if ``acme_hello`` -appears in another configuration file - say ``config_dev.yml`` - with different -values beneath it, then the incoming array might look like this:: - - array( - array( - 'foo' => 'fooValue', - 'bar' => 'barValue', - ), - array( - 'foo' => 'fooDevValue', - 'baz' => 'newConfigEntry', - ), - ) - -The order of the two arrays depends on which one is set first. - -It's your job, then, to decide how these configurations should be merged -together. You might, for example, have later values override previous values -or somehow merge them together. - -Later, in the :ref:`Configuration Class` -section, you'll learn of a truly robust way to handle this. But for now, -you might just merge them manually:: - - public function load(array $configs, ContainerBuilder $container) - { - $config = array(); - foreach ($configs as $subConfig) { - $config = array_merge($config, $subConfig); - } - - // ... now use the flat $config array - } - -.. caution:: - - Make sure the above merging technique makes sense for your bundle. This - is just an example, and you should be careful to not use it blindly. - -Using the ``load()`` Method ---------------------------- - -Within ``load()``, the ``$container`` variable refers to a container that only -knows about this namespace configuration (i.e. it doesn't contain service -information loaded from other bundles). The goal of the ``load()`` method -is to manipulate the container, adding and configuring any methods or services -needed by your bundle. - -Loading External Configuration Resources -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -One common thing to do is to load an external configuration file that may -contain the bulk of the services needed by your bundle. For example, suppose -you have a ``services.xml`` file that holds much of your bundle's service -configuration:: - - use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; - use Symfony\Component\Config\FileLocator; - - public function load(array $configs, ContainerBuilder $container) - { - // ... prepare your $config variable - - $loader = new XmlFileLoader( - $container, - new FileLocator(__DIR__.'/../Resources/config') - ); - $loader->load('services.xml'); - } - -You might even do this conditionally, based on one of the configuration values. -For example, suppose you only want to load a set of services if an ``enabled`` -option is passed and set to true:: - - public function load(array $configs, ContainerBuilder $container) - { - // ... prepare your $config variable - - $loader = new XmlFileLoader( - $container, - new FileLocator(__DIR__.'/../Resources/config') - ); - - if (isset($config['enabled']) && $config['enabled']) { - $loader->load('services.xml'); - } - } - -Configuring Services and Setting Parameters -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Once you've loaded some service configuration, you may need to modify the -configuration based on some of the input values. For example, suppose you -have a service whose first argument is some string "type" that it will use -internally. You'd like this to be easily configured by the bundle user, so -in your service configuration file (e.g. ``services.xml``), you define this -service and use a blank parameter - ``acme_hello.my_service_type`` - as -its first argument: - -.. code-block:: xml - - - - - - - - - - - %acme_hello.my_service_type% - - - - -But why would you define an empty parameter and then pass it to your service? -The answer is that you'll set this parameter in your extension class, based -on the incoming configuration values. Suppose, for example, that you want -to allow the user to define this *type* option under a key called ``my_type``. -Add the following to the ``load()`` method to do this:: - - public function load(array $configs, ContainerBuilder $container) - { - // ... prepare your $config variable - - $loader = new XmlFileLoader( - $container, - new FileLocator(__DIR__.'/../Resources/config') - ); - $loader->load('services.xml'); - - if (!isset($config['my_type'])) { - throw new \InvalidArgumentException( - 'The "my_type" option must be set' - ); - } - - $container->setParameter( - 'acme_hello.my_service_type', - $config['my_type'] - ); - } - -Now, the user can effectively configure the service by specifying the ``my_type`` -configuration value: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - acme_hello: - my_type: foo - # ... - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('acme_hello', array( - 'my_type' => 'foo', - ..., - )); - -Global Parameters -~~~~~~~~~~~~~~~~~ - -When you're configuring the container, be aware that you have the following -global parameters available to use: - -* ``kernel.name`` -* ``kernel.environment`` -* ``kernel.debug`` -* ``kernel.root_dir`` -* ``kernel.cache_dir`` -* ``kernel.logs_dir`` -* ``kernel.bundle_dirs`` -* ``kernel.bundles`` -* ``kernel.charset`` - -.. caution:: - - All parameter and service names starting with a ``_`` are reserved for the - framework, and new ones must not be defined by bundles. - -.. _cookbook-bundles-extension-config-class: - -Validation and Merging with a Configuration Class -------------------------------------------------- - -So far, you've done the merging of your configuration arrays by hand and -are checking for the presence of config values manually using the ``isset()`` -PHP function. An optional *Configuration* system is also available which -can help with merging, validation, default values, and format normalization. - -.. note:: - - Format normalization refers to the fact that certain formats - largely XML - - result in slightly different configuration arrays and that these arrays - need to be "normalized" to match everything else. - -To take advantage of this system, you'll create a ``Configuration`` class -and build a tree that defines your configuration in that class:: - - // src/Acme/HelloBundle/DependencyInjection/Configuration.php - namespace Acme\HelloBundle\DependencyInjection; - - use Symfony\Component\Config\Definition\Builder\TreeBuilder; - use Symfony\Component\Config\Definition\ConfigurationInterface; - - class Configuration implements ConfigurationInterface - { - public function getConfigTreeBuilder() - { - $treeBuilder = new TreeBuilder(); - $rootNode = $treeBuilder->root('acme_hello'); - - $rootNode - ->children() - ->scalarNode('my_type')->defaultValue('bar')->end() - ->end(); - - return $treeBuilder; - } - } - -This is a *very* simple example, but you can now use this class in your ``load()`` -method to merge your configuration and force validation. If any options other -than ``my_type`` are passed, the user will be notified with an exception -that an unsupported option was passed:: - - public function load(array $configs, ContainerBuilder $container) - { - $configuration = new Configuration(); - - $config = $this->processConfiguration($configuration, $configs); - - // ... - } - -The ``processConfiguration()`` method uses the configuration tree you've defined -in the ``Configuration`` class to validate, normalize and merge all of the -configuration arrays together. - -The ``Configuration`` class can be much more complicated than shown here, -supporting array nodes, "prototype" nodes, advanced validation, XML-specific -normalization and advanced merging. You can read more about this in :doc:`the Config Component documentation`. -You can also see it action by checking out some of the core Configuration classes, -such as the one from the `FrameworkBundle Configuration`_ or the `TwigBundle Configuration`_. - -Modifying the configuration of another Bundle -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you have multiple bundles that depend on each other, it may be useful -to allow one ``Extension`` class to modify the configuration passed to another -bundle's ``Extension`` class, as if the end-developer has actually placed that -configuration in his/her ``app/config/config.yml`` file. - -For more details, see :doc:`/cookbook/bundles/prepend_extension`. - -Default Configuration Dump -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.1 - The ``config:dump-reference`` command was added in Symfony 2.1 - -The ``config:dump-reference`` command allows a bundle's default configuration to -be output to the console in yaml. - -As long as your bundle's configuration is located in the standard location -(``YourBundle\DependencyInjection\Configuration``) and does not have a -``__construct()`` it will work automatically. If you have something -different, your ``Extension`` class must override the -:method:`Extension::getConfiguration() ` -method and return an instance of your -``Configuration``. - -Comments and examples can be added to your configuration nodes using the -``->info()`` and ``->example()`` methods:: - - // src/Acme/HelloBundle/DependencyExtension/Configuration.php - namespace Acme\HelloBundle\DependencyInjection; - - use Symfony\Component\Config\Definition\Builder\TreeBuilder; - use Symfony\Component\Config\Definition\ConfigurationInterface; - - class Configuration implements ConfigurationInterface - { - public function getConfigTreeBuilder() - { - $treeBuilder = new TreeBuilder(); - $rootNode = $treeBuilder->root('acme_hello'); - - $rootNode - ->children() - ->scalarNode('my_type') - ->defaultValue('bar') - ->info('what my_type configures') - ->example('example setting') - ->end() - ->end() - ; - - return $treeBuilder; - } - } - -This text appears as yaml comments in the output of the ``config:dump-reference`` -command. - -.. index:: - pair: Convention; Configuration - -Extension Conventions ---------------------- - -When creating an extension, follow these simple conventions: - -* The extension must be stored in the ``DependencyInjection`` sub-namespace; - -* The extension must be named after the bundle name and suffixed with - ``Extension`` (``AcmeHelloExtension`` for ``AcmeHelloBundle``); - -* The extension should provide an XSD schema. - -If you follow these simple conventions, your extensions will be registered -automatically by Symfony2. If not, override the -:method:`Bundle::build() ` -method in your bundle:: - - // ... - use Acme\HelloBundle\DependencyInjection\UnconventionalExtensionClass; - - class AcmeHelloBundle extends Bundle - { - public function build(ContainerBuilder $container) - { - parent::build($container); - - // register extensions that do not follow the conventions manually - $container->registerExtension(new UnconventionalExtensionClass()); - } - } - -In this case, the extension class must also implement a ``getAlias()`` method -and return a unique alias named after the bundle (e.g. ``acme_hello``). This -is required because the class name doesn't follow the standards by ending -in ``Extension``. - -Additionally, the ``load()`` method of your extension will *only* be called -if the user specifies the ``acme_hello`` alias in at least one configuration -file. Once again, this is because the Extension class doesn't follow the -standards set out above, so nothing happens automatically. - -.. _`FrameworkBundle Configuration`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php -.. _`TwigBundle Configuration`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php diff --git a/cookbook/bundles/index.rst b/cookbook/bundles/index.rst deleted file mode 100644 index df0cf217b9d..00000000000 --- a/cookbook/bundles/index.rst +++ /dev/null @@ -1,13 +0,0 @@ -Bundles -======= - -.. toctree:: - :maxdepth: 2 - - installation - best_practices - inheritance - override - remove - extension - prepend_extension diff --git a/cookbook/bundles/inheritance.rst b/cookbook/bundles/inheritance.rst deleted file mode 100644 index 7090d4ffe01..00000000000 --- a/cookbook/bundles/inheritance.rst +++ /dev/null @@ -1,108 +0,0 @@ -.. index:: - single: Bundle; Inheritance - -How to use Bundle Inheritance to Override parts of a Bundle -=========================================================== - -When working with third-party bundles, you'll probably come across a situation -where you want to override a file in that third-party bundle with a file -in one of your own bundles. Symfony gives you a very convenient way to override -things like controllers, templates, and other files in a bundle's -``Resources/`` directory. - -For example, suppose that you're installing the `FOSUserBundle`_, but you -want to override its base ``layout.html.twig`` template, as well as one of -its controllers. Suppose also that you have your own ``AcmeUserBundle`` -where you want the overridden files to live. Start by registering the ``FOSUserBundle`` -as the "parent" of your bundle:: - - // src/Acme/UserBundle/AcmeUserBundle.php - namespace Acme\UserBundle; - - use Symfony\Component\HttpKernel\Bundle\Bundle; - - class AcmeUserBundle extends Bundle - { - public function getParent() - { - return 'FOSUserBundle'; - } - } - -By making this simple change, you can now override several parts of the ``FOSUserBundle`` -simply by creating a file with the same name. - -.. note:: - - Despite the method name, there is no parent/child relationship between - the bundles, it is just a way to extend and override an existing bundle. - -Overriding Controllers -~~~~~~~~~~~~~~~~~~~~~~ - -Suppose you want to add some functionality to the ``registerAction`` of a -``RegistrationController`` that lives inside ``FOSUserBundle``. To do so, -just create your own ``RegistrationController.php`` file, override the bundle's -original method, and change its functionality:: - - // src/Acme/UserBundle/Controller/RegistrationController.php - namespace Acme\UserBundle\Controller; - - use FOS\UserBundle\Controller\RegistrationController as BaseController; - - class RegistrationController extends BaseController - { - public function registerAction() - { - $response = parent::registerAction(); - - // ... do custom stuff - return $response; - } - } - -.. tip:: - - Depending on how severely you need to change the behavior, you might - call ``parent::registerAction()`` or completely replace its logic with - your own. - -.. note:: - - Overriding controllers in this way only works if the bundle refers to - the controller using the standard ``FOSUserBundle:Registration:register`` - syntax in routes and templates. This is the best practice. - -Overriding Resources: Templates, Routing, Validation, etc -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Most resources can also be overridden, simply by creating a file in the same -location as your parent bundle. - -For example, it's very common to need to override the ``FOSUserBundle``'s -``layout.html.twig`` template so that it uses your application's base layout. -Since the file lives at ``Resources/views/layout.html.twig`` in the ``FOSUserBundle``, -you can create your own file in the same location of ``AcmeUserBundle``. -Symfony will ignore the file that lives inside the ``FOSUserBundle`` entirely, -and use your file instead. - -The same goes for routing files, validation configuration and other resources. - -.. note:: - - The overriding of resources only works when you refer to resources with - the ``@FosUserBundle/Resources/config/routing/security.xml`` method. - If you refer to resources without using the @BundleName shortcut, they - can't be overridden in this way. - -.. caution:: - - Translation files do not work in the same way as described above. All - translation files are accumulated into a set of "pools" (one for each) - domain. Symfony loads translation files from bundles first (in the order - that the bundles are initialized) and then from your ``app/Resources`` - directory. If the same translation is specified in two resources, the - translation from the resource that's loaded last will win. - -.. _`FOSUserBundle`: https://github.com/friendsofsymfony/fosuserbundle - diff --git a/cookbook/bundles/installation.rst b/cookbook/bundles/installation.rst deleted file mode 100644 index c844d067ca3..00000000000 --- a/cookbook/bundles/installation.rst +++ /dev/null @@ -1,146 +0,0 @@ -.. index:: - single: Bundle; Installation - -How to install 3rd party Bundles -================================ - -Most bundles provide their own installation instructions. However, the -basic steps for installing a bundle are the same. - -Add Composer Dependencies -------------------------- - -Starting from Symfony 2.1, dependencies are managed with Composer. It's -a good idea to learn some basics of Composer in `their documentation`_. - -Before you can use composer to install a bundle, you should look for a -`Packagist`_ package of that bundle. For example, if you search for the popular -`FOSUserBundle`_ you will find a packaged called `friendsofsymfony/user-bundle`_. - -.. note:: - - Packagist is the main archive for Composer. If you are searching - for a bundle, the best thing you can do is check out - `KnpBundles`_, it is the unofficial achive of Symfony Bundles. If - a bundle contains a ``README`` file, it is displayed there and if it - has a Packagist package it shows a link to the package. It's a - really useful site to begin searching for bundles. - -Now that you have the package name, you should determine the version -you want to use. Usually different versions of a bundle correspond to -a particular version of Symfony. This information should be in the ``README`` -file. If it isn't, you can use the version you want. If you choose an incompatible -version, Composer will throw dependency errors when you try to install. If -this happens, you can try a different version. - -In the case of the FOSUserBundle, the ``README`` file has a caution that version -1.2.0 must be used for Symfony 2.0 and 1.3+ for Symfony 2.1+. Packagist displays -example ``require`` statements for all existing versions of a package. The -current development version of FOSUserBundle is ``"friendsofsymfony/user-bundle": "2.0.*@dev"``. - -Now you can add the bundle to your ``composer.json`` file and update the -dependencies. You can do this manually: - -1. **Add it to the composer.json file:** - - .. code-block:: json - - { - ..., - "require": { - ..., - "friendsofsymfony/user-bundle": "2.0.*@dev" - } - } - -2. **Update the dependency:** - - .. code-block:: bash - - $ php composer.phar update friendsofsymfony/user-bundle - - or update all dependencies - - .. code-block:: bash - - $ php composer.phar update - -Or you can do this in one command: - -.. code-block:: bash - - $ php composer.phar require friendsofsymfony/user-bundle:2.0.*@dev - -Enable the Bundle ------------------ - -At this point, the bundle is installed in your Symfony project (in -``vendor/friendsofsymfony/``) and the autoloader recognizes its classes. -The only thing you need to do now is register the bundle in ``AppKernel``:: - - // app/AppKernel.php - - // ... - class AppKernel extends Kernel - { - // ... - - public function registerBundles() - { - $bundles = array( - // ..., - new FOS\UserBundle\FOSUserBundle(), - ); - - // ... - } - } - -Configure the Bundle --------------------- - -Usually a bundle requires some configuration to be added to app's -``app/config/config.yml`` file. The bundle's documentation will likely -describe that configuration. But you can also get a reference of the -bundle's config via the ``config:dump-reference`` command. - -For instance, in order to look the reference of the ``assetic`` config you -can use this: - -.. code-block:: bash - - $ app/console config:dump-reference AsseticBundle - -or this: - -.. code-block:: bash - - $ app/console config:dump-reference assetic - -The output will look like this: - -.. code-block:: text - - assetic: - debug: %kernel.debug% - use_controller: - enabled: %kernel.debug% - profiler: false - read_from: %kernel.root_dir%/../web - write_to: %assetic.read_from% - java: /usr/bin/java - node: /usr/local/bin/node - node_paths: [] - # ... - -Other Setup ------------ - -At this point, check the ``README`` file of your brand new bundle to see -what do to next. - -.. _their documentation: http://getcomposer.org/doc/00-intro.md -.. _Packagist: https://packagist.org -.. _FOSUserBundle: https://github.com/FriendsOfSymfony/FOSUserBundle -.. _`friendsofsymfony/user-bundle`: https://packagist.org/packages/friendsofsymfony/user-bundle -.. _KnpBundles: http://knpbundles.com/ diff --git a/cookbook/bundles/override.rst b/cookbook/bundles/override.rst deleted file mode 100644 index 2846f2a0764..00000000000 --- a/cookbook/bundles/override.rst +++ /dev/null @@ -1,122 +0,0 @@ -.. index:: - single: Bundle; Inheritance - -How to Override any Part of a Bundle -==================================== - -This document is a quick reference for how to override different parts of -third-party bundles. - -Templates ---------- - -For information on overriding templates, see -* :ref:`overriding-bundle-templates`. -* :doc:`/cookbook/bundles/inheritance` - -Routing -------- - -Routing is never automatically imported in Symfony2. If you want to include -the routes from any bundle, then they must be manually imported from somewhere -in your application (e.g. ``app/config/routing.yml``). - -The easiest way to "override" a bundle's routing is to never import it at -all. Instead of importing a third-party bundle's routing, simply copying -that routing file into your application, modify it, and import it instead. - -Controllers ------------ - -Assuming the third-party bundle involved uses non-service controllers (which -is almost always the case), you can easily override controllers via bundle -inheritance. For more information, see :doc:`/cookbook/bundles/inheritance`. - -Services & Configuration ------------------------- - -In order to override/extend a service, there are two options. First, you can -set the parameter holding the service's class name to your own class by setting -it in ``app/config/config.yml``. This of course is only possible if the class name is -defined as a parameter in the service config of the bundle containing the -service. For example, to override the class used for Symfony's ``translator`` -service, you would override the ``translator.class`` parameter. Knowing exactly -which parameter to override may take some research. For the translator, the -parameter is defined and used in the ``Resources/config/translation.xml`` file -in the core FrameworkBundle: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - parameters: - translator.class: Acme\HelloBundle\Translation\Translator - - .. code-block:: xml - - - - Acme\HelloBundle\Translation\Translator - - - .. code-block:: php - - // app/config/config.php - $container->setParameter('translator.class', 'Acme\HelloBundle\Translation\Translator'); - -Secondly, if the class is not available as a parameter, you want to make sure the -class is always overridden when your bundle is used, or you need to modify -something beyond just the class name, you should use a compiler pass:: - - // src/Acme/FooBundle/DependencyInjection/Compiler/OverrideServiceCompilerPass.php - namespace Acme\DemoBundle\DependencyInjection\Compiler; - - use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; - use Symfony\Component\DependencyInjection\ContainerBuilder; - - class OverrideServiceCompilerPass implements CompilerPassInterface - { - public function process(ContainerBuilder $container) - { - $definition = $container->getDefinition('original-service-id'); - $definition->setClass('Acme\DemoBundle\YourService'); - } - } - -In this example you fetch the service definition of the original service, and set -its class name to your own class. - -See :doc:`/cookbook/service_container/compiler_passes` for information on how to use -compiler passes. If you want to do something beyond just overriding the class - -like adding a method call - you can only use the compiler pass method. - -Entities & Entity mapping -------------------------- - -In progress... - -Forms ------ - -In order to override a form type, it has to be registered as a service (meaning -it is tagged as "form.type"). You can then override it as you would override any -service as explained in `Services & Configuration`_. This, of course, will only -work if the type is referred to by its alias rather than being instantiated, -e.g.:: - - $builder->add('name', 'custom_type'); - -rather than:: - - $builder->add('name', new CustomType()); - -Validation metadata -------------------- - -In progress... - -Translations ------------- - -In progress... diff --git a/cookbook/bundles/prepend_extension.rst b/cookbook/bundles/prepend_extension.rst deleted file mode 100644 index 74f1500c8f4..00000000000 --- a/cookbook/bundles/prepend_extension.rst +++ /dev/null @@ -1,135 +0,0 @@ -.. index:: - single: Configuration; Semantic - single: Bundle; Extension configuration - -How to simplify configuration of multiple Bundles -================================================= - -When building reusable and extensible applications, developers are often -faced with a choice: either create a single large Bundle or multiple smaller -Bundles. Creating a single Bundle has the draw back that it's impossible for -users to choose to remove functionality they are not using. Creating multiple -Bundles has the draw back that configuration becomes more tedious and settings -often need to be repeated for various Bundles. - -Using the below approach, it is possible to remove the disadvantage of the -multiple Bundle approach by enabling a single Extension to prepend the settings -for any Bundle. It can use the settings defined in the ``app/config/config.yml`` -to prepend settings just as if they would have been written explicitly by the -user in the application configuration. - -For example, this could be used to configure the entity manager name to use in -multiple Bundles. Or it can be used to enable an optional feature that depends -on another Bundle being loaded as well. - -To give an Extension the power to do this, it needs to implement -:class:`Symfony\\Component\\DependencyInjection\\Extension\\PrependExtensionInterface`:: - - // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php - namespace Acme\HelloBundle\DependencyInjection; - - use Symfony\Component\HttpKernel\DependencyInjection\Extension; - use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; - use Symfony\Component\DependencyInjection\ContainerBuilder; - - class AcmeHelloExtension extends Extension implements PrependExtensionInterface - { - // ... - - public function prepend(ContainerBuilder $container) - { - // ... - } - } - -Inside the :method:`Symfony\\Component\\DependencyInjection\\Extension\\PrependExtensionInterface::prepend` -method, developers have full access to the :class:`Symfony\\Component\\DependencyInjection\\ContainerBuilder` -instance just before the :method:`Symfony\\Component\\DependencyInjection\\Extension\\ExtensionInterface::load` -method is called on each of the registered Bundle Extensions. In order to -prepend settings to a Bundle extension developers can use the -:method:`Symfony\\Component\\DependencyInjection\\ContainerBuilder::prependExtensionConfig` -method on the :class:`Symfony\\Component\\DependencyInjection\\ContainerBuilder` -instance. As this method only prepends settings, any other settings done explicitly -inside the ``app/config/config.yml`` would override these prepended settings. - -The following example illustrates how to prepend -a configuration setting in multiple Bundles as well as disable a flag in multiple Bundles -in case a specific other Bundle is not registered:: - - public function prepend(ContainerBuilder $container) - { - // get all Bundles - $bundles = $container->getParameter('kernel.bundles'); - // determine if AcmeGoodbyeBundle is registered - if (!isset($bundles['AcmeGoodbyeBundle'])) { - // disable AcmeGoodbyeBundle in Bundles - $config = array('use_acme_goodbye' => false); - foreach ($container->getExtensions() as $name => $extension) { - switch ($name) { - case 'acme_something': - case 'acme_other': - // set use_acme_goodbye to false in the config of acme_something and acme_other - // note that if the user manually configured use_acme_goodbye to true in the - // app/config/config.yml then the setting would in the end be true and not false - $container->prependExtensionConfig($name, $config); - break; - } - } - } - - // process the configuration of AcmeHelloExtension - $configs = $container->getExtensionConfig($this->getAlias()); - // use the Configuration class to generate a config array with the settings ``acme_hello`` - $config = $this->processConfiguration(new Configuration(), $configs); - - // check if entity_manager_name is set in the ``acme_hello`` configuration - if (isset($config['entity_manager_name'])) { - // prepend the acme_something settings with the entity_manager_name - $config = array('entity_manager_name' => $config['entity_manager_name']); - $container->prependExtensionConfig('acme_something', $config); - } - } - -The above would be the equivalent of writing the following into the ``app/config/config.yml`` -in case ``AcmeGoodbyeBundle`` is not registered and the ``entity_manager_name`` setting -for ``acme_hello`` is set to ``non_default``: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - - acme_something: - # ... - use_acme_goodbye: false - entity_manager_name: non_default - - acme_other: - # ... - use_acme_goodbye: false - - .. code-block:: xml - - - - - non_default - - - - - .. code-block:: php - - // app/config/config.php - - $container->loadFromExtension('acme_something', array( - ..., - 'use_acme_goodbye' => false, - 'entity_manager_name' => 'non_default', - )); - $container->loadFromExtension('acme_other', array( - ..., - 'use_acme_goodbye' => false, - )); - diff --git a/cookbook/bundles/remove.rst b/cookbook/bundles/remove.rst deleted file mode 100644 index 1f24c612b92..00000000000 --- a/cookbook/bundles/remove.rst +++ /dev/null @@ -1,105 +0,0 @@ -.. index:: - single: Bundle; Removing AcmeDemoBundle - -How to remove the AcmeDemoBundle -================================ - -The Symfony2 Standard Edition comes with a complete demo that lives inside a -bundle called ``AcmeDemoBundle``. It is a great boilerplate to refer to while -starting a project, but you'll probably want to eventually remove it. - -.. tip:: - - This article uses the ``AcmeDemoBundle`` as an example, but you can use - these steps to remove any bundle. - -1. Unregister the bundle in the ``AppKernel`` ---------------------------------------------- - -To disconnect the bundle from the framework, you should remove the bundle from -the ``Appkernel::registerBundles()`` method. The bundle is normally found in -the ``$bundles`` array but the ``AcmeDemoBundle`` is only registered in a -development environment and you can find him in the if statement after:: - - // app/AppKernel.php - - // ... - class AppKernel extends Kernel - { - public function registerBundles() - { - $bundles = array(...); - - if (in_array($this->getEnvironment(), array('dev', 'test'))) { - // comment or remove this line: - // $bundles[] = new Acme\DemoBundle\AcmeDemoBundle(); - // ... - } - } - } - -2. Remove bundle configuration ------------------------------- - -Now that Symfony doesn't know about the bundle, you need to remove any -configuration and routing configuration inside the ``app/config`` directory -that refers to the bundle. - -2.1 Remove bundle routing -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The routing for the AcmeDemoBundle can be found in ``app/config/routing_dev.yml``. -Remove the ``_acme_demo`` entry at the bottom of this file. - -2.2 Remove bundle configuration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Some bundles contain configuration in one of the ``app/config/config*.yml`` -files. Be sure to remove the related configuration from these files. You can -quickly spot bundle configuration by looking at a ``acme_demo`` (or whatever -the name of the bundle is, e.g. ``fos_user`` for the ``FOSUserBundle``) string in -the configuration files. - -The ``AcmeDemoBundle`` doesn't have configuration. However, the bundle is -used in the configuration for the ``app/config/security.yml`` file. You can -use it as a boilerplate for your own security, but you **can** also remove -everything: it doesn't matter to Symfony if you remove it or not. - -3. Remove the bundle from the Filesystem ----------------------------------------- - -Now you have removed every reference to the bundle in your application, you -should remove the bundle from the filesystem. The bundle is located in the -``src/Acme/DemoBundle`` directory. You should remove this directory and you -can remove the ``Acme`` directory as well. - -.. tip:: - - If you don't know the location of a bundle, you can use the - :method:`Symfony\\Bundle\\FrameworkBundle\\Bundle\\Bundle::getPath` method - to get the path of the bundle:: - - echo $this->container->get('kernel')->getBundle('AcmeDemoBundle')->getPath(); - -4. Remove integration in other bundles --------------------------------------- - -.. note:: - - This doesn't apply to the ``AcmeDemoBundle`` - no other bundles depend - on it, so you can skip this step. - -Some bundles rely on other bundles, if you remove one of the two, the other -will probably not work. Be sure that no other bundles, third party or self-made, -rely on the bundle you are about to remove. - -.. tip:: - - If one bundle relies on another, in most it means that it uses some services - from the bundle. Searching for a ``acme_demo`` string may help you spot - them. - -.. tip:: - - If a third party bundle relies on another bundle, you can find that bundle - mentioned in the ``composer.json`` file included in the bundle directory. diff --git a/cookbook/cache/index.rst b/cookbook/cache/index.rst deleted file mode 100644 index 567d418b750..00000000000 --- a/cookbook/cache/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Cache -===== - -.. toctree:: - :maxdepth: 2 - - varnish diff --git a/cookbook/cache/varnish.rst b/cookbook/cache/varnish.rst deleted file mode 100644 index c5b029e31cd..00000000000 --- a/cookbook/cache/varnish.rst +++ /dev/null @@ -1,97 +0,0 @@ -.. index:: - single: Cache; Varnish - -How to use Varnish to speed up my Website -========================================= - -Because Symfony2's cache uses the standard HTTP cache headers, the -:ref:`symfony-gateway-cache` can easily be replaced with any other reverse -proxy. Varnish is a powerful, open-source, HTTP accelerator capable of serving -cached content quickly and including support for :ref:`Edge Side -Includes`. - -.. index:: - single: Varnish; configuration - -Configuration -------------- - -As seen previously, Symfony2 is smart enough to detect whether it talks to a -reverse proxy that understands ESI or not. It works out of the box when you -use the Symfony2 reverse proxy, but you need a special configuration to make -it work with Varnish. Thankfully, Symfony2 relies on yet another standard -written by Akamaï (`Edge Architecture`_), so the configuration tips in this -chapter can be useful even if you don't use Symfony2. - -.. note:: - - Varnish only supports the ``src`` attribute for ESI tags (``onerror`` and - ``alt`` attributes are ignored). - -First, configure Varnish so that it advertises its ESI support by adding a -``Surrogate-Capability`` header to requests forwarded to the backend -application: - -.. code-block:: text - - sub vcl_recv { - set req.http.Surrogate-Capability = "abc=ESI/1.0"; - } - -Then, optimize Varnish so that it only parses the Response contents when there -is at least one ESI tag by checking the ``Surrogate-Control`` header that -Symfony2 adds automatically: - -.. code-block:: text - - sub vcl_fetch { - if (beresp.http.Surrogate-Control ~ "ESI/1.0") { - unset beresp.http.Surrogate-Control; - - // for Varnish >= 3.0 - set beresp.do_esi = true; - // for Varnish < 3.0 - // esi; - } - } - -.. caution:: - - Compression with ESI was not supported in Varnish until version 3.0 - (read `GZIP and Varnish`_). If you're not using Varnish 3.0, put a web - server in front of Varnish to perform the compression. - -.. index:: - single: Varnish; Invalidation - -Cache Invalidation ------------------- - -You should never need to invalidate cached data because invalidation is already -taken into account natively in the HTTP cache models (see :ref:`http-cache-invalidation`). - -Still, Varnish can be configured to accept a special HTTP ``PURGE`` method -that will invalidate the cache for a given resource: - -.. code-block:: text - - sub vcl_hit { - if (req.request == "PURGE") { - set obj.ttl = 0s; - error 200 "Purged"; - } - } - - sub vcl_miss { - if (req.request == "PURGE") { - error 404 "Not purged"; - } - } - -.. caution:: - - You must protect the ``PURGE`` HTTP method somehow to avoid random people - purging your cached data. - -.. _`Edge Architecture`: http://www.w3.org/TR/edge-arch -.. _`GZIP and Varnish`: https://www.varnish-cache.org/docs/3.0/phk/gzip.html \ No newline at end of file diff --git a/cookbook/configuration/apache_router.rst b/cookbook/configuration/apache_router.rst deleted file mode 100644 index beac6121416..00000000000 --- a/cookbook/configuration/apache_router.rst +++ /dev/null @@ -1,139 +0,0 @@ -.. index:: - single: Apache Router - -How to use the Apache Router -============================ - -Symfony2, while fast out of the box, also provides various ways to increase that speed with a little bit of tweaking. -One of these ways is by letting apache handle routes directly, rather than using Symfony2 for this task. - -Change Router Configuration Parameters --------------------------------------- - -To dump Apache routes you must first tweak some configuration parameters to tell -Symfony2 to use the ``ApacheUrlMatcher`` instead of the default one: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_prod.yml - parameters: - router.options.matcher.cache_class: ~ # disable router cache - router.options.matcher_class: Symfony\Component\Routing\Matcher\ApacheUrlMatcher - - .. code-block:: xml - - - - null - - Symfony\Component\Routing\Matcher\ApacheUrlMatcher - - - - .. code-block:: php - - // app/config/config_prod.php - $container->setParameter('router.options.matcher.cache_class', null); // disable router cache - $container->setParameter( - 'router.options.matcher_class', - 'Symfony\Component\Routing\Matcher\ApacheUrlMatcher' - ); - -.. tip:: - - Note that :class:`Symfony\\Component\\Routing\\Matcher\\ApacheUrlMatcher` - extends :class:`Symfony\\Component\\Routing\\Matcher\\UrlMatcher` so even - if you don't regenerate the url_rewrite rules, everything will work (because - at the end of ``ApacheUrlMatcher::match()`` a call to ``parent::match()`` - is done). - -Generating mod_rewrite rules ----------------------------- - -To test that it's working, let's create a very basic route for demo bundle: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - hello: - path: /hello/{name} - defaults: { _controller: AcmeDemoBundle:Demo:hello } - - .. code-block:: xml - - - - AcmeDemoBundle:Demo:hello - - - .. code-block:: php - - // app/config/routing.php - $collection->add('hello', new Route('/hello/{name}', array( - '_controller' => 'AcmeDemoBundle:Demo:hello', - ))); - -Now generate **url_rewrite** rules: - -.. code-block:: bash - - $ php app/console router:dump-apache -e=prod --no-debug - -Which should roughly output the following: - -.. code-block:: apache - - # skip "real" requests - RewriteCond %{REQUEST_FILENAME} -f - RewriteRule .* - [QSA,L] - - # hello - RewriteCond %{REQUEST_URI} ^/hello/([^/]+?)$ - RewriteRule .* app.php [QSA,L,E=_ROUTING__route:hello,E=_ROUTING_name:%1,E=_ROUTING__controller:AcmeDemoBundle\:Demo\:hello] - -You can now rewrite `web/.htaccess` to use the new rules, so with this example -it should look like this: - -.. code-block:: apache - - - RewriteEngine On - - # skip "real" requests - RewriteCond %{REQUEST_FILENAME} -f - RewriteRule .* - [QSA,L] - - # hello - RewriteCond %{REQUEST_URI} ^/hello/([^/]+?)$ - RewriteRule .* app.php [QSA,L,E=_ROUTING__route:hello,E=_ROUTING_name:%1,E=_ROUTING__controller:AcmeDemoBundle\:Demo\:hello] - - -.. note:: - - Procedure above should be done each time you add/change a route if you want to take full advantage of this setup - -That's it! -You're now all set to use Apache Route rules. - -Additional tweaks ------------------ - -To save a little bit of processing time, change occurrences of ``Request`` -to ``ApacheRequest`` in ``web/app.php``:: - - // web/app.php - - require_once __DIR__.'/../app/bootstrap.php.cache'; - require_once __DIR__.'/../app/AppKernel.php'; - //require_once __DIR__.'/../app/AppCache.php'; - - use Symfony\Component\HttpFoundation\ApacheRequest; - - $kernel = new AppKernel('prod', false); - $kernel->loadClassCache(); - //$kernel = new AppCache($kernel); - $kernel->handle(ApacheRequest::createFromGlobals())->send(); diff --git a/cookbook/configuration/environments.rst b/cookbook/configuration/environments.rst deleted file mode 100644 index 3cedfb91438..00000000000 --- a/cookbook/configuration/environments.rst +++ /dev/null @@ -1,352 +0,0 @@ -.. index:: - single: Environments - -How to Master and Create new Environments -========================================= - -Every application is the combination of code and a set of configuration that -dictates how that code should function. The configuration may define the -database being used, whether or not something should be cached, or how verbose -logging should be. In Symfony2, the idea of "environments" is the idea that -the same codebase can be run using multiple different configurations. For -example, the ``dev`` environment should use configuration that makes development -easy and friendly, while the ``prod`` environment should use a set of configuration -optimized for speed. - -.. index:: - single: Environments; Configuration files - -Different Environments, Different Configuration Files ------------------------------------------------------ - -A typical Symfony2 application begins with three environments: ``dev``, -``prod``, and ``test``. As discussed, each "environment" simply represents -a way to execute the same codebase with different configuration. It should -be no surprise then that each environment loads its own individual configuration -file. If you're using the YAML configuration format, the following files -are used: - -* for the ``dev`` environment: ``app/config/config_dev.yml`` -* for the ``prod`` environment: ``app/config/config_prod.yml`` -* for the ``test`` environment: ``app/config/config_test.yml`` - -This works via a simple standard that's used by default inside the ``AppKernel`` -class: - -.. code-block:: php - - // app/AppKernel.php - - // ... - - class AppKernel extends Kernel - { - // ... - - public function registerContainerConfiguration(LoaderInterface $loader) - { - $loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml'); - } - } - -As you can see, when Symfony2 is loaded, it uses the given environment to -determine which configuration file to load. This accomplishes the goal of -multiple environments in an elegant, powerful and transparent way. - -Of course, in reality, each environment differs only somewhat from others. -Generally, all environments will share a large base of common configuration. -Opening the "dev" configuration file, you can see how this is accomplished -easily and transparently: - -.. configuration-block:: - - .. code-block:: yaml - - imports: - - { resource: config.yml } - # ... - - .. code-block:: xml - - - - - - - .. code-block:: php - - $loader->import('config.php'); - // ... - -To share common configuration, each environment's configuration file -simply first imports from a central configuration file (``config.yml``). -The remainder of the file can then deviate from the default configuration -by overriding individual parameters. For example, by default, the ``web_profiler`` -toolbar is disabled. However, in the ``dev`` environment, the toolbar is -activated by modifying the default value in the ``dev`` configuration file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_dev.yml - imports: - - { resource: config.yml } - - web_profiler: - toolbar: true - # ... - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/config_dev.php - $loader->import('config.php'); - - $container->loadFromExtension('web_profiler', array( - 'toolbar' => true, - - // ... - )); - -.. index:: - single: Environments; Executing different environments - -Executing an Application in Different Environments --------------------------------------------------- - -To execute the application in each environment, load up the application using -either the ``app.php`` (for the ``prod`` environment) or the ``app_dev.php`` -(for the ``dev`` environment) front controller: - -.. code-block:: text - - http://localhost/app.php -> *prod* environment - http://localhost/app_dev.php -> *dev* environment - -.. note:: - - The given URLs assume that your web server is configured to use the ``web/`` - directory of the application as its root. Read more in - :doc:`Installing Symfony2`. - -If you open up one of these files, you'll quickly see that the environment -used by each is explicitly set: - -.. code-block:: php - :linenos: - - handle(Request::createFromGlobals())->send(); - -As you can see, the ``prod`` key specifies that this environment will run -in the ``prod`` environment. A Symfony2 application can be executed in any -environment by using this code and changing the environment string. - -.. note:: - - The ``test`` environment is used when writing functional tests and is - not accessible in the browser directly via a front controller. In other - words, unlike the other environments, there is no ``app_test.php`` front - controller file. - -.. index:: - single: Configuration; Debug mode - -.. sidebar:: *Debug* Mode - - Important, but unrelated to the topic of *environments* is the ``false`` - key on line 8 of the front controller above. This specifies whether or - not the application should run in "debug mode". Regardless of the environment, - a Symfony2 application can be run with debug mode set to ``true`` or - ``false``. This affects many things in the application, such as whether - or not errors should be displayed or if cache files are dynamically rebuilt - on each request. Though not a requirement, debug mode is generally set - to ``true`` for the ``dev`` and ``test`` environments and ``false`` for - the ``prod`` environment. - - Internally, the value of the debug mode becomes the ``kernel.debug`` - parameter used inside the :doc:`service container `. - If you look inside the application configuration file, you'll see the - parameter used, for example, to turn logging on or off when using the - Doctrine DBAL: - - .. configuration-block:: - - .. code-block:: yaml - - doctrine: - dbal: - logging: "%kernel.debug%" - # ... - - .. code-block:: xml - - - - .. code-block:: php - - $container->loadFromExtension('doctrine', array( - 'dbal' => array( - 'logging' => '%kernel.debug%', - - // ... - ), - // ... - )); - -.. index:: - single: Environments; Creating a new environment - -Creating a New Environment --------------------------- - -By default, a Symfony2 application has three environments that handle most -cases. Of course, since an environment is nothing more than a string that -corresponds to a set of configuration, creating a new environment is quite -easy. - -Suppose, for example, that before deployment, you need to benchmark your -application. One way to benchmark the application is to use near-production -settings, but with Symfony2's ``web_profiler`` enabled. This allows Symfony2 -to record information about your application while benchmarking. - -The best way to accomplish this is via a new environment called, for example, -``benchmark``. Start by creating a new configuration file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_benchmark.yml - imports: - - { resource: config_prod.yml } - - framework: - profiler: { only_exceptions: false } - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/config_benchmark.php - $loader->import('config_prod.php') - - $container->loadFromExtension('framework', array( - 'profiler' => array('only-exceptions' => false), - )); - -And with this simple addition, the application now supports a new environment -called ``benchmark``. - -This new configuration file imports the configuration from the ``prod`` environment -and modifies it. This guarantees that the new environment is identical to -the ``prod`` environment, except for any changes explicitly made here. - -Because you'll want this environment to be accessible via a browser, you -should also create a front controller for it. Copy the ``web/app.php`` file -to ``web/app_benchmark.php`` and edit the environment to be ``benchmark``: - -.. code-block:: php - - handle(Request::createFromGlobals())->send(); - -The new environment is now accessible via:: - - http://localhost/app_benchmark.php - -.. note:: - - Some environments, like the ``dev`` environment, are never meant to be - accessed on any deployed server by the general public. This is because - certain environments, for debugging purposes, may give too much information - about the application or underlying infrastructure. To be sure these environments - aren't accessible, the front controller is usually protected from external - IP addresses via the following code at the top of the controller: - - .. code-block:: php - - if (!in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1'))) { - die('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.'); - } - -.. index:: - single: Environments; Cache directory - -Environments and the Cache Directory ------------------------------------- - -Symfony2 takes advantage of caching in many ways: the application configuration, -routing configuration, Twig templates and more are cached to PHP objects -stored in files on the filesystem. - -By default, these cached files are largely stored in the ``app/cache`` directory. -However, each environment caches its own set of files: - -.. code-block:: text - - app/cache/dev - cache directory for the *dev* environment - app/cache/prod - cache directory for the *prod* environment - -Sometimes, when debugging, it may be helpful to inspect a cached file to -understand how something is working. When doing so, remember to look in -the directory of the environment you're using (most commonly ``dev`` while -developing and debugging). While it can vary, the ``app/cache/dev`` directory -includes the following: - -* ``appDevDebugProjectContainer.php`` - the cached "service container" that - represents the cached application configuration; - -* ``appdevUrlGenerator.php`` - the PHP class generated from the routing - configuration and used when generating URLs; - -* ``appdevUrlMatcher.php`` - the PHP class used for route matching - look - here to see the compiled regular expression logic used to match incoming - URLs to different routes; - -* ``twig/`` - this directory contains all the cached Twig templates. - -.. note:: - - You can easily change the directory location and name. For more information - read the article :doc:`/cookbook/configuration/override_dir_structure`. - - -Going Further -------------- - -Read the article on :doc:`/cookbook/configuration/external_parameters`. diff --git a/cookbook/configuration/external_parameters.rst b/cookbook/configuration/external_parameters.rst deleted file mode 100644 index c99dc4bab67..00000000000 --- a/cookbook/configuration/external_parameters.rst +++ /dev/null @@ -1,146 +0,0 @@ -.. index:: - single: Environments; External parameters - -How to Set External Parameters in the Service Container -======================================================= - -In the chapter :doc:`/cookbook/configuration/environments`, you learned how -to manage your application configuration. At times, it may benefit your application -to store certain credentials outside of your project code. Database configuration -is one such example. The flexibility of the Symfony service container allows -you to easily do this. - -Environment Variables ---------------------- - -Symfony will grab any environment variable prefixed with ``SYMFONY__`` and -set it as a parameter in the service container. Double underscores are replaced -with a period, as a period is not a valid character in an environment variable -name. - -For example, if you're using Apache, environment variables can be set using -the following ``VirtualHost`` configuration: - -.. code-block:: apache - - - ServerName Symfony2 - DocumentRoot "/path/to/symfony_2_app/web" - DirectoryIndex index.php index.html - SetEnv SYMFONY__DATABASE__USER user - SetEnv SYMFONY__DATABASE__PASSWORD secret - - - AllowOverride All - Allow from All - - - -.. note:: - - The example above is for an Apache configuration, using the `SetEnv`_ - directive. However, this will work for any web server which supports - the setting of environment variables. - - Also, in order for your console to work (which does not use Apache), - you must export these as shell variables. On a Unix system, you can run - the following: - - .. code-block:: bash - - $ export SYMFONY__DATABASE__USER=user - $ export SYMFONY__DATABASE__PASSWORD=secret - -Now that you have declared an environment variable, it will be present -in the PHP ``$_SERVER`` global variable. Symfony then automatically sets all -``$_SERVER`` variables prefixed with ``SYMFONY__`` as parameters in the service -container. - -You can now reference these parameters wherever you need them. - -.. configuration-block:: - - .. code-block:: yaml - - doctrine: - dbal: - driver pdo_mysql - dbname: symfony2_project - user: "%database.user%" - password: "%database.password%" - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - $container->loadFromExtension('doctrine', array( - 'dbal' => array( - 'driver' => 'pdo_mysql', - 'dbname' => 'symfony2_project', - 'user' => '%database.user%', - 'password' => '%database.password%', - ) - )); - -Constants ---------- - -The container also has support for setting PHP constants as parameters. -See :ref:`component-di-parameters-constants` for more details. - -Miscellaneous Configuration ---------------------------- - -The ``imports`` directive can be used to pull in parameters stored elsewhere. -Importing a PHP file gives you the flexibility to add whatever is needed -in the container. The following imports a file named ``parameters.php``. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - imports: - - { resource: parameters.php } - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $loader->import('parameters.php'); - -.. note:: - - A resource file can be one of many types. PHP, XML, YAML, INI, and - closure resources are all supported by the ``imports`` directive. - -In ``parameters.php``, tell the service container the parameters that you wish -to set. This is useful when important configuration is in a nonstandard -format. The example below includes a Drupal database's configuration in -the Symfony service container. - -.. code-block:: php - - // app/config/parameters.php - include_once('/path/to/drupal/sites/default/settings.php'); - $container->setParameter('drupal.database.url', $db_url); - -.. _`SetEnv`: http://httpd.apache.org/docs/current/env.html diff --git a/cookbook/configuration/front_controllers_and_kernel.rst b/cookbook/configuration/front_controllers_and_kernel.rst deleted file mode 100644 index e05abb9be75..00000000000 --- a/cookbook/configuration/front_controllers_and_kernel.rst +++ /dev/null @@ -1,171 +0,0 @@ -.. index:: - single: How front controller, ``AppKernel`` and environments - work together - -Understanding how the Front Controller, Kernel and Environments work together -============================================================================= - -The section :doc:`/cookbook/configuration/environments` explained the basics -on how Symfony uses environments to run your application with different configuration -settings. This section will explain a bit more in-depth what happens when -your application is bootstrapped. To hook into this process, you need to understand -three parts that work together: - -* `The Front Controller`_ -* `The Kernel Class`_ -* `The Environments`_ - -.. note:: - - Usually, you will not need to define your own front controller or - ``AppKernel`` class as the `Symfony2 Standard Edition`_ provides - sensible default implementations. - - This documentation section is provided to explain what is going on behind - the scenes. - -The Front Controller --------------------- - -The `front controller`_ is a well-known design pattern; it is a section of -code that *all* requests served by an application run through. - -In the `Symfony2 Standard Edition`_, this role is taken by the `app.php`_ -and `app_dev.php`_ files in the ``web/`` directory. These are the very -first PHP scripts executed when a request is processed. - -The main purpose of the front controller is to create an instance of the -``AppKernel`` (more on that in a second), make it handle the request -and return the resulting response to the browser. - -Because every request is routed through it, the front controller can be -used to perform global initializations prior to setting up the kernel or -to *`decorate`_* the kernel with additional features. Examples include: - -* Configuring the autoloader or adding additional autoloading mechanisms; -* Adding HTTP level caching by wrapping the kernel with an instance of - :ref:`AppCache`; -* Enabling the :doc:`/components/debug`. - -The front controller can be chosen by requesting URLs like: - -.. code-block:: text - - http://localhost/app_dev.php/some/path/... - -As you can see, this URL contains the PHP script to be used as the front -controller. You can use that to easily switch the front controller or use -a custom one by placing it in the ``web/`` directory (e.g. ``app_cache.php``). - -When using Apache and the `RewriteRule shipped with the Standard Edition`_, -you can omit the filename from the URL and the RewriteRule will use ``app.php`` -as the default one. - -.. note:: - - Pretty much every other web server should be able to achieve a - behavior similar to that of the RewriteRule described above. - Check your server documentation for details or see - :doc:`/cookbook/configuration/web_server_configuration`. - -.. note:: - - Make sure you appropriately secure your front controllers against unauthorized - access. For example, you don't want to make a debugging environment - available to arbitrary users in your production environment. - -Technically, the `app/console`_ script used when running Symfony on the command -line is also a front controller, only that is not used for web, but for command -line requests. - -The Kernel Class ----------------- - -The :class:`Symfony\\Component\\HttpKernel\\Kernel` is the core of -Symfony2. It is responsible for setting up all the bundles that make up -your application and providing them with the application's configuration. -It then creates the service container before serving requests in its -:method:`Symfony\\Component\\HttpKernel\\HttpKernelInterface::handle` -method. - -There are two methods declared in the -:class:`Symfony\\Component\\HttpKernel\\KernelInterface` that are -left unimplemented in :class:`Symfony\\Component\\HttpKernel\\Kernel` -and thus serve as `template methods`_: - -* :method:`Symfony\\Component\\HttpKernel\\KernelInterface::registerBundles`, - which must return an array of all bundles needed to run the - application; - -* :method:`Symfony\\Component\\HttpKernel\\KernelInterface::registerContainerConfiguration`, - which loads the application configuration. - -To fill these (small) blanks, your application needs to subclass the -Kernel and implement these methods. The resulting class is conventionally -called the ``AppKernel``. - -Again, the Symfony2 Standard Edition provides an `AppKernel`_ in the ``app/`` -directory. This class uses the name of the environment - which is passed to -the Kernel's :method:`constructor` -method and is available via :method:`Symfony\\Component\\HttpKernel\\Kernel::getEnvironment` - -to decide which bundles to create. The logic for that is in ``registerBundles()``, -a method meant to be extended by you when you start adding bundles to your -application. - -You are, of course, free to create your own, alternative or additional -``AppKernel`` variants. All you need is to adapt your (or add a new) front -controller to make use of the new kernel. - -.. note:: - - The name and location of the ``AppKernel`` is not fixed. When - putting multiple Kernels into a single application, - it might therefore make sense to add additional sub-directories, - for example ``app/admin/AdminKernel.php`` and - ``app/api/ApiKernel.php``. All that matters is that your front - controller is able to create an instance of the appropriate - kernel. - -Having different ``AppKernels`` might be useful to enable different front -controllers (on potentially different servers) to run parts of your application -independently (for example, the admin UI, the frontend UI and database migrations). - -.. note:: - - There's a lot more the ``AppKernel`` can be used for, for example - :doc:`overriding the default directory structure `. - But odds are high that you don't need to change things like this on the - fly by having several ``AppKernel`` implementations. - -The Environments ----------------- - -We just mentioned another method the ``AppKernel`` has to implement - -:method:`Symfony\\Component\\HttpKernel\\KernelInterface::registerContainerConfiguration`. -This method is responsible for loading the application's -configuration from the right *environment*. - -Environments have been covered extensively -:doc:`in the previous chapter`, -and you probably remember that the Standard Edition comes with three -of them - ``dev``, ``prod`` and ``test``. - -More technically, these names are nothing more than strings passed from the -front controller to the ``AppKernel``'s constructor. This name can then be -used in the :method:`Symfony\\Component\\HttpKernel\\KernelInterface::registerContainerConfiguration` -method to decide which configuration files to load. - -The Standard Edition's `AppKernel`_ class implements this method by simply -loading the ``app/config/config_*environment*.yml`` file. You are, of course, -free to implement this method differently if you need a more sophisticated -way of loading your configuration. - -.. _front controller: http://en.wikipedia.org/wiki/Front_Controller_pattern -.. _Symfony2 Standard Edition: https://github.com/symfony/symfony-standard -.. _app.php: https://github.com/symfony/symfony-standard/blob/master/web/app.php -.. _app_dev.php: https://github.com/symfony/symfony-standard/blob/master/web/app_dev.php -.. _app/console: https://github.com/symfony/symfony-standard/blob/master/app/console -.. _AppKernel: https://github.com/symfony/symfony-standard/blob/master/app/AppKernel.php -.. _decorate: http://en.wikipedia.org/wiki/Decorator_pattern -.. _RewriteRule shipped with the Standard Edition: https://github.com/symfony/symfony-standard/blob/master/web/.htaccess) -.. _template methods: http://en.wikipedia.org/wiki/Template_method_pattern diff --git a/cookbook/configuration/index.rst b/cookbook/configuration/index.rst deleted file mode 100644 index c3850724c88..00000000000 --- a/cookbook/configuration/index.rst +++ /dev/null @@ -1,13 +0,0 @@ -Configuration -============= - -.. toctree:: - :maxdepth: 2 - - environments - override_dir_structure - front_controllers_and_kernel - external_parameters - pdo_session_storage - apache_router - web_server_configuration diff --git a/cookbook/configuration/override_dir_structure.rst b/cookbook/configuration/override_dir_structure.rst deleted file mode 100644 index 31aeb973847..00000000000 --- a/cookbook/configuration/override_dir_structure.rst +++ /dev/null @@ -1,154 +0,0 @@ -.. index:: - single: Override Symfony - -How to override Symfony's Default Directory Structure -===================================================== - -Symfony automatically ships with a default directory structure. You can -easily override this directory structure to create your own. The default -directory structure is: - -.. code-block:: text - - app/ - cache/ - config/ - logs/ - ... - src/ - ... - vendor/ - ... - web/ - app.php - ... - -.. _override-cache-dir: - -Override the ``cache`` directory --------------------------------- - -You can override the cache directory by overriding the ``getCacheDir`` method -in the ``AppKernel`` class of you application:: - - // app/AppKernel.php - - // ... - class AppKernel extends Kernel - { - // ... - - public function getCacheDir() - { - return $this->rootDir.'/'.$this->environment.'/cache'; - } - } - -``$this->rootDir`` is the absolute path to the ``app`` directory and ``$this->environment`` -is the current environment (i.e. ``dev``). In this case you have changed -the location of the cache directory to ``app/{environment}/cache``. - -.. caution:: - - You should keep the ``cache`` directory different for each environment, - otherwise some unexpected behaviour may happen. Each environment generates - its own cached config files, and so each needs its own directory to store - those cache files. - -.. _override-logs-dir: - -Override the ``logs`` directory -------------------------------- - -Overriding the ``logs`` directory is the same as overriding the ``cache`` -directory, the only difference is that you need to override the ``getLogDir`` -method:: - - // app/AppKernel.php - - // ... - class AppKernel extends Kernel - { - // ... - - public function getLogDir() - { - return $this->rootDir.'/'.$this->environment.'/logs'; - } - } - -Here you have changed the location of the directory to ``app/{environment}/logs``. - -Override the ``web`` directory ------------------------------- - -If you need to rename or move your ``web`` directory, the only thing you -need to guarantee is that the path to the ``app`` directory is still correct -in your ``app.php`` and ``app_dev.php`` front controllers. If you simply -renamed the directory, you're fine. But if you moved it in some way, you -may need to modify the paths inside these files:: - - require_once __DIR__.'/../Symfony/app/bootstrap.php.cache'; - require_once __DIR__.'/../Symfony/app/AppKernel.php'; - -Since Symfony 2.1 (in which Composer is introduced), you also need to change -the ``extra.symfony-web-dir`` option in the ``composer.json`` file: - -.. code-block:: json - - { - ... - "extra": { - ... - "symfony-web-dir": "my_new_web_dir" - } - } - -.. tip:: - - Some shared hosts have a ``public_html`` web directory root. Renaming - your web directory from ``web`` to ``public_html`` is one way to make - your Symfony project work on your shared host. Another way is to deploy - your application to a directory outside of your web root, delete your - ``public_html`` directory, and then replace it with a symbolic link to - the ``web`` in your project. - -.. note:: - - If you use the AsseticBundle you need to configure this, so it can use - the correct ``web`` directory: - - .. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - - # ... - assetic: - # ... - read_from: "%kernel.root_dir%/../../public_html" - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - - // ... - $container->loadFromExtension('assetic', array( - // ... - 'read_from' => '%kernel.root_dir%/../../public_html', - )); - - Now you just need to dump the assets again and your application should - work: - - .. code-block:: bash - - $ php app/console assetic:dump --env=prod --no-debug diff --git a/cookbook/configuration/pdo_session_storage.rst b/cookbook/configuration/pdo_session_storage.rst deleted file mode 100644 index 79bb496761f..00000000000 --- a/cookbook/configuration/pdo_session_storage.rst +++ /dev/null @@ -1,211 +0,0 @@ -.. index:: - single: Session; Database Storage - -How to use PdoSessionHandler to store Sessions in the Database -============================================================== - -The default session storage of Symfony2 writes the session information to -file(s). Most medium to large websites use a database to store the session -values instead of files, because databases are easier to use and scale in a -multi-webserver environment. - -Symfony2 has a built-in solution for database session storage called -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\PdoSessionHandler`. -To use it, you just need to change some parameters in ``config.yml`` (or the -configuration format of your choice): - -.. versionadded:: 2.1 - In Symfony2.1 the class and namespace are slightly modified. You can now - find the session storage classes in the `Session\\Storage` namespace: - ``Symfony\Component\HttpFoundation\Session\Storage``. Also - note that in Symfony2.1 you should configure ``handler_id`` not ``storage_id`` like in Symfony2.0. - Below, you'll notice that ``%session.storage.options%`` is not used anymore. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - session: - # ... - handler_id: session.handler.pdo - - parameters: - pdo.db_options: - db_table: session - db_id_col: session_id - db_data_col: session_value - db_time_col: session_time - - services: - pdo: - class: PDO - arguments: - dsn: "mysql:dbname=mydatabase" - user: myuser - password: mypassword - - session.handler.pdo: - class: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler - arguments: ["@pdo", "%pdo.db_options%"] - - .. code-block:: xml - - - - - - - - - session - session_id - session_value - session_time - - - - - - mysql:dbname=mydatabase - myuser - mypassword - - - - - %pdo.db_options% - - - - .. code-block:: php - - // app/config/config.php - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; - - $container->loadFromExtension('framework', array( - ..., - 'session' => array( - // ..., - 'handler_id' => 'session.handler.pdo', - ), - )); - - $container->setParameter('pdo.db_options', array( - 'db_table' => 'session', - 'db_id_col' => 'session_id', - 'db_data_col' => 'session_value', - 'db_time_col' => 'session_time', - )); - - $pdoDefinition = new Definition('PDO', array( - 'mysql:dbname=mydatabase', - 'myuser', - 'mypassword', - )); - $container->setDefinition('pdo', $pdoDefinition); - - $storageDefinition = new Definition('Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler', array( - new Reference('pdo'), - '%pdo.db_options%', - )); - $container->setDefinition('session.handler.pdo', $storageDefinition); - -* ``db_table``: The name of the session table in your database -* ``db_id_col``: The name of the id column in your session table (VARCHAR(255) or larger) -* ``db_data_col``: The name of the value column in your session table (TEXT or CLOB) -* ``db_time_col``: The name of the time column in your session table (INTEGER) - -Sharing your Database Connection Information --------------------------------------------- - -With the given configuration, the database connection settings are defined for -the session storage connection only. This is OK when you use a separate -database for the session data. - -But if you'd like to store the session data in the same database as the rest -of your project's data, you can use the connection settings from the -parameter.ini by referencing the database-related parameters defined there: - -.. configuration-block:: - - .. code-block:: yaml - - pdo: - class: PDO - arguments: - - "mysql:host=%database_host%;port=%database_port%;dbname=%database_name%" - - "%database_user%" - - "%database_password%" - - .. code-block:: xml - - - mysql:host=%database_host%;port=%database_port%;dbname=%database_name% - %database_user% - %database_password% - - - .. code-block:: php - - $pdoDefinition = new Definition('PDO', array( - 'mysql:host=%database_host%;port=%database_port%;dbname=%database_name%', - '%database_user%', - '%database_password%', - )); - -Example SQL Statements ----------------------- - -MySQL -~~~~~ - -The SQL statement for creating the needed database table might look like the -following (MySQL): - -.. code-block:: sql - - CREATE TABLE `session` ( - `session_id` varchar(255) NOT NULL, - `session_value` text NOT NULL, - `session_time` int(11) NOT NULL, - PRIMARY KEY (`session_id`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -PostgreSQL -~~~~~~~~~~ - -For PostgreSQL, the statement should look like this: - -.. code-block:: sql - - CREATE TABLE session ( - session_id character varying(255) NOT NULL, - session_value text NOT NULL, - session_time integer NOT NULL, - CONSTRAINT session_pkey PRIMARY KEY (session_id) - ); - -Microsoft SQL Server -~~~~~~~~~~~~~~~~~~~~ - -For MSSQL, the statement might look like the following: - -.. code-block:: sql - - CREATE TABLE [dbo].[session]( - [session_id] [nvarchar](255) NOT NULL, - [session_value] [ntext] NOT NULL, - [session_time] [int] NOT NULL, - PRIMARY KEY CLUSTERED( - [session_id] ASC - ) WITH ( - PAD_INDEX = OFF, - STATISTICS_NORECOMPUTE = OFF, - IGNORE_DUP_KEY = OFF, - ALLOW_ROW_LOCKS = ON, - ALLOW_PAGE_LOCKS = ON - ) ON [PRIMARY] - ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] diff --git a/cookbook/configuration/web_server_configuration.rst b/cookbook/configuration/web_server_configuration.rst deleted file mode 100644 index 66640e3dbd5..00000000000 --- a/cookbook/configuration/web_server_configuration.rst +++ /dev/null @@ -1,100 +0,0 @@ -.. index:: - single: Web Server - -Configuring a web server -======================== - -The web directory is the home of all of your application's public and static -files. Including images, stylesheets and JavaScript files. It is also where the -front controllers live. For more details, see the :ref:`the-web-directory`. - -The web directory services as the document root when configuring your web -server. In the examples below, this directory is in ``/var/www/project/web/``. - -Apache2 -------- - -For advanced Apache configuration options, see the official `Apache`_ -documentation. The minimum basics to get your application running under Apache2 -are: - -.. code-block:: apache - - - ServerName www.domain.tld - - DocumentRoot /var/www/project/web - - # enable the .htaccess rewrites - AllowOverride All - Order allow,deny - Allow from All - - - ErrorLog /var/log/apache2/project_error.log - CustomLog /var/log/apache2/project_access.log combined - - -.. note:: - - For performance reasons, you will probably want to set - ``AllowOverride None`` and implement the rewrite rules in the ``web/.htaccess`` - into the virtualhost config. - -If you are using **php-cgi**, Apache does not pass HTTP basic username and -password to PHP by default. To work around this limitation, you should use the -following configuration snippet: - -.. code-block:: apache - - RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] - -Nginx ------ - -For advanced Nginx configuration options, see the official `Nginx`_ -documentation. The minimum basics to get your application running under Nginx -are: - -.. code-block:: nginx - - server { - server_name www.domain.tld; - root /var/www/project/web; - - location / { - # try to serve file directly, fallback to rewrite - try_files $uri @rewriteapp; - } - - location @rewriteapp { - # rewrite all to app.php - rewrite ^(.*)$ /app.php/$1 last; - } - - location ~ ^/(app|app_dev)\.php(/|$) { - fastcgi_pass unix:/var/run/php5-fpm.sock; - fastcgi_split_path_info ^(.+\.php)(/.*)$; - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param HTTPS off; - } - - error_log /var/log/nginx/project_error.log; - access_log /var/log/nginx/project_access.log; - } - -.. note:: - - Depending on your PHP-FPM config, the ``fastcgi_pass`` can also be - ``fastcgi_pass 127.0.0.1:9000``. - -.. tip:: - - This executes **only** ``app.php`` and ``app_dev.php`` in the web directory. - All other files will be served as text. If you have other PHP files in - your web directory, be sure to include them in the ``location`` block - above. - -.. _`Apache`: http://httpd.apache.org/docs/current/mod/core.html#documentroot -.. _`Nginx`: http://wiki.nginx.org/Symfony diff --git a/cookbook/console/console_command.rst b/cookbook/console/console_command.rst deleted file mode 100644 index 41aba0f58ff..00000000000 --- a/cookbook/console/console_command.rst +++ /dev/null @@ -1,140 +0,0 @@ -.. index:: - single: Console; Create commands - -How to create a Console Command -=============================== - -The Console page of the Components section (:doc:`/components/console/introduction`) covers -how to create a Console command. This cookbook article covers the differences -when creating Console commands within the Symfony2 framework. - -Automatically Registering Commands ----------------------------------- - -To make the console commands available automatically with Symfony2, create a -``Command`` directory inside your bundle and create a php file suffixed with -``Command.php`` for each command that you want to provide. For example, if you -want to extend the ``AcmeDemoBundle`` (available in the Symfony Standard -Edition) to greet you from the command line, create ``GreetCommand.php`` and -add the following to it:: - - // src/Acme/DemoBundle/Command/GreetCommand.php - namespace Acme\DemoBundle\Command; - - use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; - use Symfony\Component\Console\Input\InputArgument; - use Symfony\Component\Console\Input\InputInterface; - use Symfony\Component\Console\Input\InputOption; - use Symfony\Component\Console\Output\OutputInterface; - - class GreetCommand extends ContainerAwareCommand - { - protected function configure() - { - $this - ->setName('demo:greet') - ->setDescription('Greet someone') - ->addArgument('name', InputArgument::OPTIONAL, 'Who do you want to greet?') - ->addOption('yell', null, InputOption::VALUE_NONE, 'If set, the task will yell in uppercase letters') - ; - } - - protected function execute(InputInterface $input, OutputInterface $output) - { - $name = $input->getArgument('name'); - if ($name) { - $text = 'Hello '.$name; - } else { - $text = 'Hello'; - } - - if ($input->getOption('yell')) { - $text = strtoupper($text); - } - - $output->writeln($text); - } - } - -This command will now automatically be available to run: - -.. code-block:: bash - - $ app/console demo:greet Fabien - -Getting Services from the Service Container -------------------------------------------- - -By using :class:`Symfony\\Bundle\\FrameworkBundle\\Command\\ContainerAwareCommand` -as the base class for the command (instead of the more basic -:class:`Symfony\\Component\\Console\\Command\\Command`), you have access to the -service container. In other words, you have access to any configured service. -For example, you could easily extend the task to be translatable:: - - protected function execute(InputInterface $input, OutputInterface $output) - { - $name = $input->getArgument('name'); - $translator = $this->getContainer()->get('translator'); - if ($name) { - $output->writeln($translator->trans('Hello %name%!', array('%name%' => $name))); - } else { - $output->writeln($translator->trans('Hello!')); - } - } - -Testing Commands ----------------- - -When testing commands used as part of the full framework :class:`Symfony\\Bundle\\FrameworkBundle\\Console\\Application` -should be used instead of :class:`Symfony\\Component\\Console\\Application`:: - - use Symfony\Component\Console\Tester\CommandTester; - use Symfony\Bundle\FrameworkBundle\Console\Application; - use Acme\DemoBundle\Command\GreetCommand; - - class ListCommandTest extends \PHPUnit_Framework_TestCase - { - public function testExecute() - { - // mock the Kernel or create one depending on your needs - $application = new Application($kernel); - $application->add(new GreetCommand()); - - $command = $application->find('demo:greet'); - $commandTester = new CommandTester($command); - $commandTester->execute(array('command' => $command->getName())); - - $this->assertRegExp('/.../', $commandTester->getDisplay()); - - // ... - } - } - -To be able to use the fully set up service container for your console tests -you can extend your test from -:class:`Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase`:: - - use Symfony\Component\Console\Tester\CommandTester; - use Symfony\Bundle\FrameworkBundle\Console\Application; - use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - use Acme\DemoBundle\Command\GreetCommand; - - class ListCommandTest extends WebTestCase - { - public function testExecute() - { - $kernel = $this->createKernel(); - $kernel->boot(); - - $application = new Application($kernel); - $application->add(new GreetCommand()); - - $command = $application->find('demo:greet'); - $commandTester = new CommandTester($command); - $commandTester->execute(array('command' => $command->getName())); - - $this->assertRegExp('/.../', $commandTester->getDisplay()); - - // ... - } - } diff --git a/cookbook/console/index.rst b/cookbook/console/index.rst deleted file mode 100644 index 878d1fc862a..00000000000 --- a/cookbook/console/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Console -======= - -.. toctree:: - :maxdepth: 2 - - console_command - usage - sending_emails - logging diff --git a/cookbook/console/logging.rst b/cookbook/console/logging.rst deleted file mode 100644 index b332c149fdd..00000000000 --- a/cookbook/console/logging.rst +++ /dev/null @@ -1,254 +0,0 @@ -.. index:: - single: Console; Enabling logging - -How to enable logging in Console Commands -========================================= - -The Console component doesn't provide any logging capabilities out of the box. -Normally, you run console commands manually and observe the output, which is -why logging is not provided. However, there are cases when you might need -logging. For example, if you are running console commands unattended, such -as from cron jobs or deployment scripts, it may be easier to use Symfony's -logging capabilities instead of configuring other tools to gather console -output and process it. This can be especially handful if you already have -some existing setup for aggregating and analyzing Symfony logs. - -There are basically two logging cases you would need: - * Manually logging some information from your command; - * Logging uncaught Exceptions. - -Manually logging from a console Command ---------------------------------------- - -This one is really simple. When you create a console command within the full -framework as described in ":doc:`/cookbook/console/console_command`", your command -extends :class:`Symfony\\Bundle\\FrameworkBundle\\Command\\ContainerAwareCommand`. -This means that you can simply access the standard logger service through the -container and use it to do the logging:: - - // src/Acme/DemoBundle/Command/GreetCommand.php - namespace Acme\DemoBundle\Command; - - use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; - use Symfony\Component\Console\Input\InputArgument; - use Symfony\Component\Console\Input\InputInterface; - use Symfony\Component\Console\Input\InputOption; - use Symfony\Component\Console\Output\OutputInterface; - use Symfony\Component\HttpKernel\Log\LoggerInterface; - - class GreetCommand extends ContainerAwareCommand - { - // ... - - protected function execute(InputInterface $input, OutputInterface $output) - { - /** @var $logger LoggerInterface */ - $logger = $this->getContainer()->get('logger'); - - $name = $input->getArgument('name'); - if ($name) { - $text = 'Hello '.$name; - } else { - $text = 'Hello'; - } - - if ($input->getOption('yell')) { - $text = strtoupper($text); - $logger->warn('Yelled: '.$text); - } - else { - $logger->info('Greeted: '.$text); - } - - $output->writeln($text); - } - } - -Depending on the environment in which you run your command (and your logging -setup), you should see the logged entries in ``app/logs/dev.log`` or ``app/logs/prod.log``. - -Enabling automatic Exceptions logging -------------------------------------- - -To get your console application to automatically log uncaught exceptions -for all of your commands, you'll need to do a little bit more work. - -First, create a new sub-class of :class:`Symfony\\Bundle\\FrameworkBundle\\Console\\Application` -and override its :method:`Symfony\\Bundle\\FrameworkBundle\\Console\\Application::run` -method, where exception handling should happen: - -.. caution:: - - Due to the nature of the core :class:`Symfony\\Component\\Console\\Application` - class, much of the :method:`run` - method has to be duplicated and even a private property ``originalAutoExit`` - re-implemented. This serves as an example of what you *could* do in your - code, though there is a high risk that something may break when upgrading - to future versions of Symfony. - - -.. code-block:: php - - // src/Acme/DemoBundle/Console/Application.php - namespace Acme\DemoBundle\Console; - - use Symfony\Bundle\FrameworkBundle\Console\Application as BaseApplication; - use Symfony\Component\Console\Input\InputInterface; - use Symfony\Component\Console\Output\OutputInterface; - use Symfony\Component\Console\Output\ConsoleOutputInterface; - use Symfony\Component\HttpKernel\Log\LoggerInterface; - use Symfony\Component\HttpKernel\KernelInterface; - use Symfony\Component\Console\Output\ConsoleOutput; - use Symfony\Component\Console\Input\ArgvInput; - - class Application extends BaseApplication - { - private $originalAutoExit; - - public function __construct(KernelInterface $kernel) - { - parent::__construct($kernel); - $this->originalAutoExit = true; - } - - /** - * Runs the current application. - * - * @param InputInterface $input An Input instance - * @param OutputInterface $output An Output instance - * - * @return integer 0 if everything went fine, or an error code - * - * @throws \Exception When doRun returns Exception - * - * @api - */ - public function run(InputInterface $input = null, OutputInterface $output = null) - { - // make the parent method throw exceptions, so you can log it - $this->setCatchExceptions(false); - - if (null === $input) { - $input = new ArgvInput(); - } - - if (null === $output) { - $output = new ConsoleOutput(); - } - - try { - $statusCode = parent::run($input, $output); - } catch (\Exception $e) { - - /** @var $logger LoggerInterface */ - $logger = $this->getKernel()->getContainer()->get('logger'); - - $message = sprintf( - '%s: %s (uncaught exception) at %s line %s while running console command `%s`', - get_class($e), - $e->getMessage(), - $e->getFile(), - $e->getLine(), - $this->getCommandName($input) - ); - $logger->crit($message); - - if ($output instanceof ConsoleOutputInterface) { - $this->renderException($e, $output->getErrorOutput()); - } else { - $this->renderException($e, $output); - } - $statusCode = $e->getCode(); - - $statusCode = is_numeric($statusCode) && $statusCode ? $statusCode : 1; - } - - if ($this->originalAutoExit) { - if ($statusCode > 255) { - $statusCode = 255; - } - // @codeCoverageIgnoreStart - exit($statusCode); - // @codeCoverageIgnoreEnd - } - - return $statusCode; - } - - public function setAutoExit($bool) - { - // parent property is private, so we need to intercept it in a setter - $this->originalAutoExit = (Boolean) $bool; - parent::setAutoExit($bool); - } - - } - -In the code above, you disable exception catching so the parent ``run`` method -will throw all exceptions. When an exception is caught, you simple log it by -accessing the ``logger`` service from the service container and then handle -the rest of the logic in the same way that the parent ``run`` method does -(specifically, since the parent :method:`run` -method will not handle exceptions rendering and status code handling when -``catchExceptions`` is set to false, it has to be done in the overridden -method). - -For the extended Application class to work properly with in console shell mode, -you have to do a small trick to intercept the ``autoExit`` setter and store the -setting in a different property, since the parent property is private. - -Now to be able to use your extended ``Application`` class you need to adjust -the ``app/console`` script to use the new class instead of the default:: - - // app/console - - // ... - // replace the following line: - // use Symfony\Bundle\FrameworkBundle\Console\Application; - use Acme\DemoBundle\Console\Application; - - // ... - -That's it! Thanks to autoloader, your class will now be used instead of original -one. - -Logging non-0 exit statuses ---------------------------- - -The logging capabilities of the console can be further extended by logging -non-0 exit statuses. This way you will know if a command had any errors, even -if no exceptions were thrown. - -In order to do that, you'd have to modify the ``run()`` method of your extended -``Application`` class in the following way:: - - public function run(InputInterface $input = null, OutputInterface $output = null) - { - // make the parent method throw exceptions, so you can log it - $this->setCatchExceptions(false); - - // store the autoExit value before resetting it - you'll need it later - $autoExit = $this->originalAutoExit; - $this->setAutoExit(false); - - // ... - - if ($autoExit) { - if ($statusCode > 255) { - $statusCode = 255; - } - - // log non-0 exit codes along with command name - if ($statusCode !== 0) { - /** @var $logger LoggerInterface */ - $logger = $this->getKernel()->getContainer()->get('logger'); - $logger->warn(sprintf('Command `%s` exited with status code %d', $this->getCommandName($input), $statusCode)); - } - - // @codeCoverageIgnoreStart - exit($statusCode); - // @codeCoverageIgnoreEnd - } - - return $statusCode; - } diff --git a/cookbook/console/sending_emails.rst b/cookbook/console/sending_emails.rst deleted file mode 100644 index c184e915326..00000000000 --- a/cookbook/console/sending_emails.rst +++ /dev/null @@ -1,118 +0,0 @@ -.. index:: - single: Console; Sending emails - single: Console; Generating URLs - -How to generate URLs and send Emails from the Console -===================================================== - -Unfortunately, the command line context does not know about your VirtualHost -or domain name. This means that if 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 ----------------------------------------- - -.. versionadded:: 2.1 - The ``host`` and ``scheme`` parameters are available since Symfony 2.1 - -.. versionadded: 2.2 - The ``base_url`` parameter is available since Symfony 2.2 - -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). Starting with Symfony 2.2 you can also configure -the base path 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 - - # app/config/parameters.yml - parameters: - router.request_context.host: example.org - router.request_context.scheme: https - router.request_context.base_url: my/path - - .. code-block:: xml - - - - - - - - example.org - https - my/path - - - - .. code-block:: php - - // app/config/config_test.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'); - -Configuring the Request Context per Command -------------------------------------------- - -To change it only in one command you can simply fetch the Request Context -service and override its settings:: - - // src/Acme/DemoBundle/Command/DemoCommand.php - - // ... - class DemoCommand extends ContainerAwareCommand - { - protected function execute(InputInterface $input, OutputInterface $output) - { - $context = $this->getContainer()->get('router')->getContext(); - $context->setHost('example.com'); - $context->setScheme('https'); - $context->setBaseUrl('my/path'); - - // ... your code here - } - } - -Using Memory Spooling ---------------------- - -Sending emails in a console command works the same way as described in the -:doc:`/cookbook/email/email` cookbook except if memory spooling is used. - -When using memory spooling (see the :doc:`/cookbook/email/spool` cookbook for more -information), you must be aware that because of how symfony handles console -commands, emails are not sent automatically. You must take care of flushing -the queue yourself. Use the following code to send emails inside your -console command:: - - $container = $this->getContainer(); - $mailer = $container->get('mailer'); - $spool = $mailer->getTransport()->getSpool(); - $transport = $container->get('swiftmailer.transport.real'); - - $spool->flushQueue($transport); - -Another option is to create an environment which is only used by console -commands and uses a different spooling method. - -.. note:: - - Taking care of the spooling is only needed when memory spooling is used. - If you are using file spooling (or no spooling at all), there is no need - to flush the queue manually within the command. - diff --git a/cookbook/console/usage.rst b/cookbook/console/usage.rst deleted file mode 100644 index 87ea793f688..00000000000 --- a/cookbook/console/usage.rst +++ /dev/null @@ -1,65 +0,0 @@ -.. index:: - single: Console; Usage - -How to use the Console -====================== - -The :doc:`/components/console/usage` page of the components documentation looks -at the global console options. When you use the console as part of the full -stack framework, some additional global options are available as well. - -By default, console commands run in the ``dev`` environment and you may want -to change this for some commands. For example, you may want to run some commands -in the ``prod`` environment for performance reasons. Also, the result of some commands -will be different depending on the environment. for example, the ``cache:clear`` -command will clear and warm the cache for the specified environment only. To -clear and warm the ``prod`` cache you need to run: - -.. code-block:: bash - - $ php app/console cache:clear --env=prod - -or the equivalent: - -.. code-block:: bash - - $ php app/console cache:clear -e=prod - -In addition to changing the environment, you can also choose to disable debug mode. -This can be useful where you want to run commands in the ``dev`` environment -but avoid the performance hit of collecting debug data: - -.. code-block:: bash - - $ php app/console list --no-debug - -There is an interactive shell which allows you to enter commands without having to -specify ``php app/console`` each time, which is useful if you need to run several -commands. To enter the shell run: - -.. code-block:: bash - - $ php app/console --shell - $ php app/console -s - -You can now just run commands with the command name: - -.. code-block:: bash - - Symfony > list - -When using the shell you can choose to run each command in a separate process: - -.. code-block:: bash - - $ php app/console --shell --process-isolation - $ php app/console -s --process-isolation - -When you do this, the output will not be colorized and interactivity is not -supported so you will need to pass all command params explicitly. - -.. note:: - - Unless you are using isolated processes, clearing the cache in the shell - will not have an effect on subsequent commands you run. This is because - the original cached files are still being used. \ No newline at end of file diff --git a/cookbook/controller/error_pages.rst b/cookbook/controller/error_pages.rst deleted file mode 100644 index 9e333210146..00000000000 --- a/cookbook/controller/error_pages.rst +++ /dev/null @@ -1,110 +0,0 @@ -.. index:: - single: Controller; Customize error pages - single: Error pages - -How to customize Error Pages -============================ - -When any exception is thrown in Symfony2, the exception is caught inside the -``Kernel`` class and eventually forwarded to a special controller, -``TwigBundle:Exception:show`` for handling. This controller, which lives -inside the core ``TwigBundle``, determines which error template to display and -the status code that should be set for the given exception. - -Error pages can be customized in two different ways, depending on how much -control you need: - -1. Customize the error templates of the different error pages (explained below); - -2. Replace the default exception controller ``TwigBundle::Exception:show`` - with your own controller and handle it however you want (see - :ref:`exception_controller in the Twig reference`); - -.. tip:: - - The customization of exception handling is actually much more powerful - than what's written here. An internal event, ``kernel.exception``, is thrown - which allows complete control over exception handling. For more - information, see :ref:`kernel-kernel.exception`. - -All of the error templates live inside ``TwigBundle``. To override the -templates, simply rely on the standard method for overriding templates that -live inside a bundle. For more information, see -:ref:`overriding-bundle-templates`. - -For example, to override the default error template that's shown to the -end-user, create a new template located at -``app/Resources/TwigBundle/views/Exception/error.html.twig``: - -.. code-block:: html+jinja - - - - - - An Error Occurred: {{ status_text }} - - -

Oops! An Error Occurred

-

The server returned a "{{ status_code }} {{ status_text }}".

- - - -.. caution:: - - You **must not** use ``is_granted`` in your error pages (or layout used - by your error pages), because the router runs before the firewall. If - the router throws an exception (for instance, when the route does not - match), then using ``is_granted`` will throw a further exception. You - can use ``is_granted`` safely by saying ``{% if app.user and is_granted('...') %}``. - -.. tip:: - - If you're not familiar with Twig, don't worry. Twig is a simple, powerful - and optional templating engine that integrates with ``Symfony2``. For more - information about Twig see :doc:`/book/templating`. - -In addition to the standard HTML error page, Symfony provides a default error -page for many of the most common response formats, including JSON -(``error.json.twig``), XML (``error.xml.twig``) and even Javascript -(``error.js.twig``), to name a few. To override any of these templates, just -create a new file with the same name in the -``app/Resources/TwigBundle/views/Exception`` directory. This is the standard -way of overriding any template that lives inside a bundle. - -.. _cookbook-error-pages-by-status-code: - -Customizing the 404 Page and other Error Pages ----------------------------------------------- - -You can also customize specific error templates according to the HTTP status -code. For instance, create a -``app/Resources/TwigBundle/views/Exception/error404.html.twig`` template to -display a special page for 404 (page not found) errors. - -Symfony uses the following algorithm to determine which template to use: - -* First, it looks for a template for the given format and status code (like - ``error404.json.twig``); - -* If it does not exist, it looks for a template for the given format (like - ``error.json.twig``); - -* If it does not exist, it falls back to the HTML template (like - ``error.html.twig``). - -.. tip:: - - To see the full list of default error templates, see the - ``Resources/views/Exception`` directory of the ``TwigBundle``. In a - standard Symfony2 installation, the ``TwigBundle`` can be found at - ``vendor/symfony/symfony/src/Symfony/Bundle/TwigBundle``. Often, the easiest way - to customize an error page is to copy it from the ``TwigBundle`` into - ``app/Resources/TwigBundle/views/Exception`` and then modify it. - -.. note:: - - The debug-friendly exception pages shown to the developer can even be - customized in the same way by creating templates such as - ``exception.html.twig`` for the standard HTML exception page or - ``exception.json.twig`` for the JSON exception page. diff --git a/cookbook/controller/index.rst b/cookbook/controller/index.rst deleted file mode 100644 index fc4041abf25..00000000000 --- a/cookbook/controller/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Controller -========== - -.. toctree:: - :maxdepth: 2 - - error_pages - service diff --git a/cookbook/controller/service.rst b/cookbook/controller/service.rst deleted file mode 100644 index 8466bc802ab..00000000000 --- a/cookbook/controller/service.rst +++ /dev/null @@ -1,65 +0,0 @@ -.. index:: - single: Controller; As Services - -How to define Controllers as Services -===================================== - -In the book, you've learned how easily a controller can be used when it -extends the base -:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` class. While -this works fine, controllers can also be specified as services. - -To refer to a controller that's defined as a service, use the single colon (:) -notation. For example, suppose you've defined a service called -``my_controller`` and you want to forward to a method called ``indexAction()`` -inside the service:: - - $this->forward('my_controller:indexAction', array('foo' => $bar)); - -You need to use the same notation when defining the route ``_controller`` -value: - -.. code-block:: yaml - - my_controller: - path: / - defaults: { _controller: my_controller:indexAction } - -To use a controller in this way, it must be defined in the service container -configuration. For more information, see the :doc:`Service Container -` chapter. - -When using a controller defined as a service, it will most likely not extend -the base ``Controller`` class. Instead of relying on its shortcut methods, -you'll interact directly with the services that you need. Fortunately, this is -usually pretty easy and the base ``Controller`` class itself is a great source -on how to perform many common tasks. - -.. note:: - - Specifying a controller as a service takes a little bit more work. The - primary advantage is that the entire controller or any services passed to - the controller can be modified via the service container configuration. - This is especially useful when developing an open-source bundle or any - bundle that will be used in many different projects. So, even if you don't - specify your controllers as services, you'll likely see this done in some - open-source Symfony2 bundles. - -Using Annotation Routing ------------------------- - -When using annotations to setup routing when using a controller defined as a -service, you need to specify your service as follows:: - - /** - * @Route("/blog", service="my_bundle.annot_controller") - * @Cache(expires="tomorrow") - */ - class AnnotController extends Controller - { - } - -In this example, ``my_bundle.annot_controller`` should be the id of the -``AnnotController`` instance defined in the service container. This is -documented in the :doc:`/bundles/SensioFrameworkExtraBundle/annotations/routing` -chapter. diff --git a/cookbook/debugging.rst b/cookbook/debugging.rst deleted file mode 100644 index cadd1303667..00000000000 --- a/cookbook/debugging.rst +++ /dev/null @@ -1,65 +0,0 @@ -.. index:: - single: Debugging - -How to optimize your development Environment for debugging -========================================================== - -When you work on a Symfony project on your local machine, you should use the -``dev`` environment (``app_dev.php`` front controller). This environment -configuration is optimized for two main purposes: - -* Give the developer accurate feedback whenever something goes wrong (web - debug toolbar, nice exception pages, profiler, ...); - -* Be as similar as possible as the production environment to avoid problems - when deploying the project. - -.. _cookbook-debugging-disable-bootstrap: - -Disabling the Bootstrap File and Class Caching ----------------------------------------------- - -And to make the production environment as fast as possible, Symfony creates -big PHP files in your cache containing the aggregation of PHP classes your -project needs for every request. However, this behavior can confuse your IDE -or your debugger. This recipe shows you how you can tweak this caching -mechanism to make it friendlier when you need to debug code that involves -Symfony classes. - -The ``app_dev.php`` front controller reads as follows by default:: - - // ... - - $loader = require_once __DIR__.'/../app/bootstrap.php.cache'; - require_once __DIR__.'/../app/AppKernel.php'; - - $kernel = new AppKernel('dev', true); - $kernel->loadClassCache(); - $request = Request::createFromGlobals(); - -To make your debugger happier, disable all PHP class caches by removing the -call to ``loadClassCache()`` and by replacing the require statements like -below:: - - // ... - - // $loader = require_once __DIR__.'/../app/bootstrap.php.cache'; - $loader = require_once __DIR__.'/../app/autoload.php'; - require_once __DIR__.'/../app/AppKernel.php'; - - use Symfony\Component\HttpFoundation\Request; - - $kernel = new AppKernel('dev', true); - // $kernel->loadClassCache(); - $request = Request::createFromGlobals(); - -.. tip:: - - If you disable the PHP caches, don't forget to revert after your debugging - session. - -Some IDEs do not like the fact that some classes are stored in different -locations. To avoid problems, you can either tell your IDE to ignore the PHP -cache files, or you can change the extension used by Symfony for these files:: - - $kernel->loadClassCache('classes', '.php.cache'); diff --git a/cookbook/deployment-tools.rst b/cookbook/deployment-tools.rst deleted file mode 100644 index 237ba05e9e8..00000000000 --- a/cookbook/deployment-tools.rst +++ /dev/null @@ -1,192 +0,0 @@ -.. index:: - single: Deployment - -How to deploy a Symfony2 application -==================================== - -.. note:: - - Deploying can be a complex and varied task depending on your setup and needs. - This entry doesn't try to explain everything, but rather offers the most - common requirements and ideas for deployment. - -Symfony2 Deployment Basics --------------------------- - -The typical steps taken while deploying a Symfony2 application include: - -#. Upload your modified code to the live server; -#. Update your vendor dependencies (typically done via Composer, and may - be done before uploading); -#. Running database migrations or similar tasks to update any changed data structures; -#. Clearing (and perhaps more importantly, warming up) your cache. - -A deployment may also include other things, such as: - -* Tagging a particular version of of your code as a release in your source control repository; -* Creating a temporary staging area to build your updated setup "offline"; -* Running any tests available to ensure code and/or server stability; -* Removal of any unnecessary files from ``web`` to keep your production environment clean; -* Clearing of external cache systems (like `Memcached`_ or `Redis`_). - -How to deploy a Symfony2 application ------------------------------------- - -There are several ways you can deploy a Symfony2 application. - -Let's start with a few basic deployment strategies and build up from there. - -Basic File Transfer -~~~~~~~~~~~~~~~~~~~ - -The most basic way of deploying an application is copying the files manually -via ftp/scp (or similar method). This has its disadvantages as you lack control -over the system as the upgrade progresses. This method also requires you -to take some manual steps after transferring the files (see `Common Post-Deployment Tasks`_) - -Using Source Control -~~~~~~~~~~~~~~~~~~~~ - -If you're using source control (e.g. git or svn), you can simplify by having -your live installation also be a copy of your repository. When you're ready -to upgrade it is as simple as fetching the latest updates from your source -control system. - -This makes updating your files *easier*, but you still need to worry about -manually taking other steps (see `Common Post-Deployment Tasks`_). - -Using Build scripts and other Tools -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There are also high-quality tools to help ease the pain of deployment. There -are even a few tools which have been specifically tailored to the requirements of -Symfony2, and which take special care to ensure that everything before, during, -and after a deployment has gone correctly. - -See `The Tools`_ for a list of tools that can help with deployment. - -Common Post-Deployment Tasks ----------------------------- - -After deploying your actual source code, there are a number of common things -you'll need to do: - -A) Configure your ``app/config/parameters.yml`` file -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This file should be customized on each system. The method you use to -deploy your source code should *not* deploy this file. Instead, you should -set it up manually (or via some build process) on your server(s). - -B) Update your vendors -~~~~~~~~~~~~~~~~~~~~~~ - -Your vendors can be updated before transferring your source code (i.e. -update the ``vendor/`` directory, then transfer that with your source -code) or afterwards on the server. Either way, just update your vendors -as your normally do: - -.. code-block:: bash - - $ php composer.phar install --optimize-autoloader - -.. tip:: - - The ``--optimize-autoloader`` flag makes Composer's autoloader more - performant by building a "class map". - -C) Clear your Symfony cache -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Make sure you clear (and warm-up) your Symfony cache: - -.. code-block:: bash - - $ php app/console cache:clear --env=prod --no-debug - -D) Dump your Assetic assets -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you're using Assetic, you'll also want to dump your assets: - -.. code-block:: bash - - $ php app/console assetic:dump --env=prod --no-debug - -E) Other things! -~~~~~~~~~~~~~~~~ - -There may be lots of other things that you need to do, depending on your -setup: - -* Running any database migrations -* Clearing your APC cache -* Running ``assets:install`` (taken care of already in ``composer.phar install``) -* Add/edit CRON jobs -* Pushing assets to a CDN -* ... - -Application Lifecycle: Continuous Integration, QA, etc ------------------------------------------------------- - -While this entry covers the technical details of deploying, the full lifecycle -of taking code from development up to production may have a lot more steps -(think deploying to staging, QA, running tests, etc). - -The use of staging, testing, QA, continuous integration, database migrations -and the capability to roll back in case of failure are all strongly advised. There -are simple and more complex tools and one can make the deployment as easy -(or sophisticated) as your environment requires. - -Don't forget that deploying your application also involves updating any dependency -(typically via Composer), migrating your database, clearing your cache and -other potential things like pushing assets to a CDN (see `Common Post-Deployment Tasks`_). - -The Tools ---------- - -`Capifony`_: - - This tool provides a specialized set of tools on top of Capistrano, tailored - specifically to symfony and Symfony2 projects. - -`sf2debpkg`_: - - This tool helps you build a native Debian package for your Symfony2 project. - -`Magallanes`_: - - This Capistrano-like deployment tool is built in PHP, and may be easier - for PHP developers to extend for their needs. - -Bundles: - - There are many `bundles that add deployment features`_ directly into your - Symfony2 console. - -Basic scripting: - - You can of course use shell, `Ant`_, or any other build tool to script - the deploying of your project. - -Platform as a Service Providers: - - PaaS is a relatively new way to deploy your application. Typically a PaaS - will use a single configuration file in your project's root directory to - determine how to build an environment on the fly that supports your software. - One provider with confirmed Symfony2 support is `PagodaBox`_. - -.. tip:: - - Looking for more? Talk to the community on the `Symfony IRC channel`_ #symfony - (on freenode) for more information. - -.. _`Capifony`: http://capifony.org/ -.. _`sf2debpkg`: https://github.com/liip/sf2debpkg -.. _`Ant`: http://blog.sznapka.pl/deploying-symfony2-applications-with-ant -.. _`PagodaBox`: https://github.com/jmather/pagoda-symfony-sonata-distribution/blob/master/Boxfile -.. _`Magallanes`: https://github.com/andres-montanez/Magallanes -.. _`bundles that add deployment features`: http://knpbundles.com/search?q=deploy -.. _`Symfony IRC channel`: http://webchat.freenode.net/?channels=symfony -.. _`Memcached`: http://memcached.org/ -.. _`Redis`: http://redis.io/ diff --git a/cookbook/doctrine/common_extensions.rst b/cookbook/doctrine/common_extensions.rst deleted file mode 100644 index 89954c4ce92..00000000000 --- a/cookbook/doctrine/common_extensions.rst +++ /dev/null @@ -1,33 +0,0 @@ -.. index:: - single: Doctrine; Common extensions - -How to use Doctrine Extensions: Timestampable, Sluggable, Translatable, etc. -============================================================================ - -Doctrine2 is very flexible, and the community has already created a series -of useful Doctrine extensions to help you with common entity-related tasks. - -One library in particular - the `DoctrineExtensions`_ library - provides integration -functionality for `Sluggable`_, `Translatable`_, `Timestampable`_, `Loggable`_, -`Tree`_ and `Sortable`_ behaviors. - -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: - -#. Use the `StofDoctrineExtensionsBundle`_, which integrates the above library. - -#. Implement this services directly by following the documentation for integration - with Symfony2: `Install Gedmo Doctrine2 extensions in Symfony2`_ - -.. _`DoctrineExtensions`: https://github.com/l3pp4rd/DoctrineExtensions -.. _`StofDoctrineExtensionsBundle`: https://github.com/stof/StofDoctrineExtensionsBundle -.. _`Sluggable`: https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/sluggable.md -.. _`Translatable`: https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/translatable.md -.. _`Timestampable`: https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/timestampable.md -.. _`Loggable`: https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/loggable.md -.. _`Tree`: https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/tree.md -.. _`Sortable`: https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/sortable.md -.. _`Install Gedmo Doctrine2 extensions in Symfony2`: https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/symfony2.md \ No newline at end of file diff --git a/cookbook/doctrine/custom_dql_functions.rst b/cookbook/doctrine/custom_dql_functions.rst deleted file mode 100644 index 9c863055c08..00000000000 --- a/cookbook/doctrine/custom_dql_functions.rst +++ /dev/null @@ -1,85 +0,0 @@ -.. index:: - single: Doctrine; Custom DQL functions - -How to Register Custom DQL Functions -==================================== - -Doctrine allows you to specify custom DQL functions. For more information -on this topic, read Doctrine's cookbook article "`DQL User Defined Functions`_". - -In Symfony, you can register your custom DQL functions as follows: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - doctrine: - orm: - # ... - entity_managers: - default: - # ... - dql: - string_functions: - test_string: Acme\HelloBundle\DQL\StringFunction - second_string: Acme\HelloBundle\DQL\SecondStringFunction - numeric_functions: - test_numeric: Acme\HelloBundle\DQL\NumericFunction - datetime_functions: - test_datetime: Acme\HelloBundle\DQL\DatetimeFunction - - .. code-block:: xml - - - - - - - - - - - Acme\HelloBundle\DQL\SecondStringFunction - Acme\HelloBundle\DQL\DatetimeFunction - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('doctrine', array( - 'orm' => array( - // ... - - 'entity_managers' => array( - 'default' => array( - // ... - - 'dql' => array( - 'string_functions' => array( - 'test_string' => 'Acme\HelloBundle\DQL\StringFunction', - 'second_string' => 'Acme\HelloBundle\DQL\SecondStringFunction', - ), - 'numeric_functions' => array( - 'test_numeric' => 'Acme\HelloBundle\DQL\NumericFunction', - ), - 'datetime_functions' => array( - 'test_datetime' => 'Acme\HelloBundle\DQL\DatetimeFunction', - ), - ), - ), - ), - ), - )); - -.. _`DQL User Defined Functions`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/dql-user-defined-functions.html diff --git a/cookbook/doctrine/dbal.rst b/cookbook/doctrine/dbal.rst deleted file mode 100644 index 2dbcb9d3823..00000000000 --- a/cookbook/doctrine/dbal.rst +++ /dev/null @@ -1,187 +0,0 @@ -.. index:: - pair: Doctrine; DBAL - -How to use Doctrine's DBAL Layer -================================ - -.. note:: - - This article is about Doctrine DBAL's layer. Typically, you'll work with - the higher level Doctrine ORM layer, which simply uses the DBAL behind - the scenes to actually communicate with the database. To read more about - the Doctrine ORM, see ":doc:`/book/doctrine`". - -The `Doctrine`_ Database Abstraction Layer (DBAL) is an abstraction layer that -sits on top of `PDO`_ and offers an intuitive and flexible API for communicating -with the most popular relational databases. In other words, the DBAL library -makes it easy to execute queries and perform other database actions. - -.. tip:: - - Read the official Doctrine `DBAL Documentation`_ to learn all the details - and capabilities of Doctrine's DBAL library. - -To get started, configure the database connection parameters: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - doctrine: - dbal: - driver: pdo_mysql - dbname: Symfony2 - user: root - password: null - charset: UTF8 - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('doctrine', array( - 'dbal' => array( - 'driver' => 'pdo_mysql', - 'dbname' => 'Symfony2', - 'user' => 'root', - 'password' => null, - ), - )); - -For full DBAL configuration options, see :ref:`reference-dbal-configuration`. - -You can then access the Doctrine DBAL connection by accessing the -``database_connection`` service:: - - class UserController extends Controller - { - public function indexAction() - { - $conn = $this->get('database_connection'); - $users = $conn->fetchAll('SELECT * FROM users'); - - // ... - } - } - -Registering Custom Mapping Types --------------------------------- - -You can register custom mapping types through Symfony's configuration. They -will be added to all configured connections. For more information on custom -mapping types, read Doctrine's `Custom Mapping Types`_ section of their documentation. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - doctrine: - dbal: - types: - custom_first: Acme\HelloBundle\Type\CustomFirst - custom_second: Acme\HelloBundle\Type\CustomSecond - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('doctrine', array( - 'dbal' => array( - 'types' => array( - 'custom_first' => 'Acme\HelloBundle\Type\CustomFirst', - 'custom_second' => 'Acme\HelloBundle\Type\CustomSecond', - ), - ), - )); - -Registering Custom Mapping Types in the SchemaTool --------------------------------------------------- - -The SchemaTool is used to inspect the database to compare the schema. To -achieve this task, it needs to know which mapping type needs to be used -for each database types. Registering new ones can be done through the configuration. - -Let's map the ENUM type (not supported by DBAL by default) to a the ``string`` -mapping type: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - doctrine: - dbal: - connections: - default: - // Other connections parameters - mapping_types: - enum: string - - .. code-block:: xml - - - - - - - - - string - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('doctrine', array( - 'dbal' => array( - 'connections' => array( - 'default' => array( - 'mapping_types' => array( - 'enum' => 'string', - ), - ), - ), - ), - )); - -.. _`PDO`: http://www.php.net/pdo -.. _`Doctrine`: http://www.doctrine-project.org -.. _`DBAL Documentation`: http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/index.html -.. _`Custom Mapping Types`: http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#custom-mapping-types diff --git a/cookbook/doctrine/event_listeners_subscribers.rst b/cookbook/doctrine/event_listeners_subscribers.rst deleted file mode 100644 index 99b6a947571..00000000000 --- a/cookbook/doctrine/event_listeners_subscribers.rst +++ /dev/null @@ -1,213 +0,0 @@ -.. index:: - single: Doctrine; Event listeners and subscribers - -.. _doctrine-event-config: - -How to Register Event Listeners and Subscribers -=============================================== - -Doctrine packages 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 website. - -The Doctrine website also explains all existing events that can be listened to. - -Configuring the Listener/Subscriber ------------------------------------ - -To register a service to act as an event listener or subscriber you just have -to :ref:`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 - - doctrine: - dbal: - default_connection: default - connections: - default: - driver: pdo_sqlite - memory: true - - services: - my.listener: - class: Acme\SearchBundle\EventListener\SearchIndexer - tags: - - { name: doctrine.event_listener, event: postPersist } - my.listener2: - class: Acme\SearchBundle\EventListener\SearchIndexer2 - tags: - - { name: doctrine.event_listener, event: postPersist, connection: default } - my.subscriber: - class: Acme\SearchBundle\EventListener\SearchIndexerSubscriber - tags: - - { name: doctrine.event_subscriber, connection: default } - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Definition; - - $container->loadFromExtension('doctrine', array( - 'dbal' => array( - 'default_connection' => 'default', - 'connections' => array( - 'default' => array( - 'driver' => 'pdo_sqlite', - 'memory' => true, - ), - ), - ), - )); - - $container - ->setDefinition( - 'my.listener', - new Definition('Acme\SearchBundle\EventListener\SearchIndexer') - ) - ->addTag('doctrine.event_listener', array('event' => 'postPersist')) - ; - $container - ->setDefinition( - 'my.listener2', - new Definition('Acme\SearchBundle\EventListener\SearchIndexer2') - ) - ->addTag('doctrine.event_listener', array('event' => 'postPersist', 'connection' => 'default')) - ; - $container - ->setDefinition( - 'my.subscriber', - new Definition('Acme\SearchBundle\EventListener\SearchIndexerSubscriber') - ) - ->addTag('doctrine.event_subscriber', array('connection' => 'default')) - ; - -Creating the Listener Class ---------------------------- - -In the previous example, a service ``my.listener`` 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/Acme/SearchBundle/EventListener/SearchIndexer.php - namespace Acme\SearchBundle\EventListener; - - use Doctrine\ORM\Event\LifecycleEventArgs; - use Acme\StoreBundle\Entity\Product; - - class SearchIndexer - { - public function postPersist(LifecycleEventArgs $args) - { - $entity = $args->getEntity(); - $entityManager = $args->getEntityManager(); - - // perhaps you only want to act on some "Product" entity - if ($entity instanceof Product) { - // ... 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). - -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/Acme/SearchBundle/EventListener/SearchIndexerSubscriber.php - namespace Acme\SearchBundle\EventListener; - - use Doctrine\Common\EventSubscriber; - use Doctrine\ORM\Event\LifecycleEventArgs; - // for doctrine 2.4: Doctrine\Common\Persistence\Event\LifecycleEventArgs; - use Acme\StoreBundle\Entity\Product; - - class SearchIndexerSubscriber implements EventSubscriber - { - public function getSubscribedEvents() - { - return array( - 'postPersist', - 'postUpdate', - ); - } - - public function postUpdate(LifecycleEventArgs $args) - { - $this->index($args); - } - - public function postPersist(LifecycleEventArgs $args) - { - $this->index($args); - } - - public function index(LifecycleEventArgs $args) - { - $entity = $args->getEntity(); - $entityManager = $args->getEntityManager(); - - // perhaps you only want to act on some "Product" entity - if ($entity instanceof Product) { - // ... do something with the Product - } - } - } - -.. tip:: - - Doctrine event subscribers can not 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. - -.. _`The Event System`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html diff --git a/cookbook/doctrine/file_uploads.rst b/cookbook/doctrine/file_uploads.rst deleted file mode 100644 index a18d47ddcf9..00000000000 --- a/cookbook/doctrine/file_uploads.rst +++ /dev/null @@ -1,539 +0,0 @@ -.. index:: - single: Doctrine; File uploads - -How to handle File Uploads with Doctrine -======================================== - -Handling file uploads with Doctrine entities is no different than handling -any other file upload. In other words, you're free to move the file in your -controller after handling a form submission. For examples of how to do this, -see the :doc:`file type reference` page. - -If you choose to, you can also integrate the file upload into your entity -lifecycle (i.e. creation, update and removal). In this case, as your entity -is created, updated, and removed from Doctrine, the file uploading and removal -processing will take place automatically (without needing to do anything in -your controller); - -To make this work, you'll need to take care of a number of details, which -will be covered in this cookbook entry. - -Basic Setup ------------ - -First, create a simple ``Doctrine`` Entity class to work with:: - - // src/Acme/DemoBundle/Entity/Document.php - namespace Acme\DemoBundle\Entity; - - use Doctrine\ORM\Mapping as ORM; - use Symfony\Component\Validator\Constraints as Assert; - - /** - * @ORM\Entity - */ - class Document - { - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue(strategy="AUTO") - */ - public $id; - - /** - * @ORM\Column(type="string", length=255) - * @Assert\NotBlank - */ - public $name; - - /** - * @ORM\Column(type="string", length=255, nullable=true) - */ - public $path; - - public function getAbsolutePath() - { - return null === $this->path - ? null - : $this->getUploadRootDir().'/'.$this->path; - } - - public function getWebPath() - { - return null === $this->path - ? null - : $this->getUploadDir().'/'.$this->path; - } - - protected function getUploadRootDir() - { - // the absolute directory path where uploaded - // documents should be saved - return __DIR__.'/../../../../web/'.$this->getUploadDir(); - } - - protected function getUploadDir() - { - // get rid of the __DIR__ so it doesn't screw up - // when displaying uploaded doc/image in the view. - return 'uploads/documents'; - } - } - -The ``Document`` entity has a name and it is associated with a file. The ``path`` -property stores the relative path to the file and is persisted to the database. -The ``getAbsolutePath()`` is a convenience method that returns the absolute -path to the file while the ``getWebPath()`` is a convenience method that -returns the web path, which can be used in a template to link to the uploaded -file. - -.. tip:: - - If you have not done so already, you should probably read the - :doc:`file` type documentation first to - understand how the basic upload process works. - -.. note:: - - If you're using annotations to specify your validation rules (as shown - in this example), be sure that you've enabled validation by annotation - (see :ref:`validation configuration`). - -To handle the actual file upload in the form, use a "virtual" ``file`` field. -For example, if you're building your form directly in a controller, it might -look like this:: - - public function uploadAction() - { - // ... - - $form = $this->createFormBuilder($document) - ->add('name') - ->add('file') - ->getForm(); - - // ... - } - -Next, create this property on your ``Document`` class and add some validation -rules:: - - use Symfony\Component\HttpFoundation\File\UploadedFile; - - // ... - class Document - { - /** - * @Assert\File(maxSize="6000000") - */ - private $file; - - /** - * Sets file. - * - * @param UploadedFile $file - */ - public function setFile(UploadedFile $file = null) - { - $this->file = $file; - } - - /** - * Get file. - * - * @return UploadedFile - */ - public function getFile() - { - return $this->file; - } - } - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/DemoBundle/Resources/config/validation.yml - Acme\DemoBundle\Entity\Document: - properties: - file: - - File: - maxSize: 6000000 - - .. code-block:: php-annotations - - // src/Acme/DemoBundle/Entity/Document.php - namespace Acme\DemoBundle\Entity; - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Document - { - /** - * @Assert\File(maxSize="6000000") - */ - private $file; - - // ... - } - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // src/Acme/DemoBundle/Entity/Document.php - namespace Acme\DemoBundle\Entity; - - // ... - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints as Assert; - - class Document - { - // ... - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('file', new Assert\File(array( - 'maxSize' => 6000000, - ))); - } - } - -.. note:: - - As you are using the ``File`` constraint, Symfony2 will automatically guess - that the form field is a file upload input. That's why you did not have - to set it explicitly when creating the form above (``->add('file')``). - -The following controller shows you how to handle the entire process:: - - // ... - use Acme\DemoBundle\Entity\Document; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; - use Symfony\Component\HttpFoundation\Request; - // ... - - /** - * @Template() - */ - public function uploadAction(Request $request) - { - $document = new Document(); - $form = $this->createFormBuilder($document) - ->add('name') - ->add('file') - ->getForm(); - - $form->handleRequest($request); - - if ($form->isValid()) { - $em = $this->getDoctrine()->getManager(); - - $em->persist($document); - $em->flush(); - - return $this->redirect($this->generateUrl(...)); - } - - return array('form' => $form->createView()); - } - -The previous controller will automatically persist the ``Document`` entity -with the submitted name, but it will do nothing about the file and the ``path`` -property will be blank. - -An easy way to handle the file upload is to move it just before the entity is -persisted and then set the ``path`` property accordingly. Start by calling -a new ``upload()`` method on the ``Document`` class, which you'll create -in a moment to handle the file upload:: - - if ($form->isValid()) { - $em = $this->getDoctrine()->getManager(); - - $document->upload(); - - $em->persist($document); - $em->flush(); - - return $this->redirect(...); - } - -The ``upload()`` method will take advantage of the :class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile` -object, which is what's returned after a ``file`` field is submitted:: - - public function upload() - { - // the file property can be empty if the field is not required - if (null === $this->getFile()) { - return; - } - - // use the original file name here but you should - // sanitize it at least to avoid any security issues - - // move takes the target directory and then the - // target filename to move to - $this->getFile()->move( - $this->getUploadRootDir(), - $this->getFile()->getClientOriginalName() - ); - - // set the path property to the filename where you've saved the file - $this->path = $this->getFile()->getClientOriginalName(); - - // clean up the file property as you won't need it anymore - $this->file = null; - } - -Using Lifecycle Callbacks -------------------------- - -Even if this implementation works, it suffers from a major flaw: What if there -is a problem when the entity is persisted? The file would have already moved -to its final location even though the entity's ``path`` property didn't -persist correctly. - -To avoid these issues, you should change the implementation so that the database -operation and the moving of the file become atomic: if there is a problem -persisting the entity or if the file cannot be moved, then *nothing* should -happen. - -To do this, you need to move the file right as Doctrine persists the entity -to the database. This can be accomplished by hooking into an entity lifecycle -callback:: - - /** - * @ORM\Entity - * @ORM\HasLifecycleCallbacks - */ - class Document - { - } - -Next, refactor the ``Document`` class to take advantage of these callbacks:: - - use Symfony\Component\HttpFoundation\File\UploadedFile; - - /** - * @ORM\Entity - * @ORM\HasLifecycleCallbacks - */ - class Document - { - private $temp; - - /** - * Sets file. - * - * @param UploadedFile $file - */ - public function setFile(UploadedFile $file = null) - { - $this->file = $file; - // check if we have an old image path - if (isset($this->path)) { - // store the old name to delete after the update - $this->temp = $this->path; - $this->path = null; - } else { - $this->path = 'initial'; - } - } - - /** - * @ORM\PrePersist() - * @ORM\PreUpdate() - */ - public function preUpload() - { - if (null !== $this->getFile()) { - // do whatever you want to generate a unique name - $filename = sha1(uniqid(mt_rand(), true)); - $this->path = $filename.'.'.$this->getFile()->guessExtension(); - } - } - - /** - * @ORM\PostPersist() - * @ORM\PostUpdate() - */ - public function upload() - { - if (null === $this->getFile()) { - return; - } - - // if there is an error when moving the file, an exception will - // be automatically thrown by move(). This will properly prevent - // the entity from being persisted to the database on error - $this->getFile()->move($this->getUploadRootDir(), $this->path); - - // check if we have an old image - if (isset($this->temp)) { - // delete the old image - unlink($this->getUploadRootDir().'/'.$this->temp); - // clear the temp image path - $this->temp = null; - } - $this->file = null; - } - - /** - * @ORM\PostRemove() - */ - public function removeUpload() - { - if ($file = $this->getAbsolutePath()) { - unlink($file); - } - } - } - -The class now does everything you need: it generates a unique filename before -persisting, moves the file after persisting, and removes the file if the -entity is ever deleted. - -Now that the moving of the file is handled atomically by the entity, the -call to ``$document->upload()`` should be removed from the controller:: - - if ($form->isValid()) { - $em = $this->getDoctrine()->getManager(); - - $em->persist($document); - $em->flush(); - - return $this->redirect(...); - } - -.. note:: - - The ``@ORM\PrePersist()`` and ``@ORM\PostPersist()`` event callbacks are - triggered before and after the entity is persisted to the database. On the - other hand, the ``@ORM\PreUpdate()`` and ``@ORM\PostUpdate()`` event - callbacks are called when the entity is updated. - -.. caution:: - - The ``PreUpdate`` and ``PostUpdate`` callbacks are only triggered if there - is a change in one of the entity's field that are persisted. This means - that, by default, if you modify only the ``$file`` property, these events - will not be triggered, as the property itself is not directly persisted - via Doctrine. One solution would be to use an ``updated`` field that's - persisted to Doctrine, and to modify it manually when changing the file. - -Using the ``id`` as the filename --------------------------------- - -If you want to use the ``id`` as the name of the file, the implementation is -slightly different as you need to save the extension under the ``path`` -property, instead of the actual filename:: - - use Symfony\Component\HttpFoundation\File\UploadedFile; - - /** - * @ORM\Entity - * @ORM\HasLifecycleCallbacks - */ - class Document - { - private $temp; - - /** - * Sets file. - * - * @param UploadedFile $file - */ - public function setFile(UploadedFile $file = null) - { - $this->file = $file; - // check if we have an old image path - if (is_file($this->getAbsolutePath())) { - // store the old name to delete after the update - $this->temp = $this->getAbsolutePath(); - } else { - $this->path = 'initial'; - } - } - - /** - * @ORM\PrePersist() - * @ORM\PreUpdate() - */ - public function preUpload() - { - if (null !== $this->getFile()) { - $this->path = $this->getFile()->guessExtension(); - } - } - - /** - * @ORM\PostPersist() - * @ORM\PostUpdate() - */ - public function upload() - { - if (null === $this->getFile()) { - return; - } - - // check if we have an old image - if (isset($this->temp)) { - // delete the old image - unlink($this->temp); - // clear the temp image path - $this->temp = null; - } - - // you must throw an exception here if the file cannot be moved - // so that the entity is not persisted to the database - // which the UploadedFile move() method does - $this->getFile()->move( - $this->getUploadRootDir(), - $this->id.'.'.$this->getFile()->guessExtension() - ); - - $this->setFile(null); - } - - /** - * @ORM\PreRemove() - */ - public function storeFilenameForRemove() - { - $this->temp = $this->getAbsolutePath(); - } - - /** - * @ORM\PostRemove() - */ - public function removeUpload() - { - if (isset($this->temp)) { - unlink($this->temp); - } - } - - public function getAbsolutePath() - { - return null === $this->path - ? null - : $this->getUploadRootDir().'/'.$this->id.'.'.$this->path; - } - } - -You'll notice in this case that you need to do a little bit more work in -order to remove the file. Before it's removed, you must store the file path -(since it depends on the id). Then, once the object has been fully removed -from the database, you can safely delete the file (in ``PostRemove``). diff --git a/cookbook/doctrine/index.rst b/cookbook/doctrine/index.rst deleted file mode 100644 index 170a5f2d718..00000000000 --- a/cookbook/doctrine/index.rst +++ /dev/null @@ -1,15 +0,0 @@ -Doctrine -======== - -.. toctree:: - :maxdepth: 2 - - file_uploads - common_extensions - event_listeners_subscribers - dbal - reverse_engineering - multiple_entity_managers - custom_dql_functions - resolve_target_entity - registration_form diff --git a/cookbook/doctrine/multiple_entity_managers.rst b/cookbook/doctrine/multiple_entity_managers.rst deleted file mode 100644 index 3fc5c5c46df..00000000000 --- a/cookbook/doctrine/multiple_entity_managers.rst +++ /dev/null @@ -1,227 +0,0 @@ -.. index:: - single: Doctrine; Multiple entity managers - -How to work with Multiple Entity Managers and Connections -========================================================= - -You can use multiple Doctrine entity managers or connections in a Symfony2 -application. This is necessary if you are using different databases or even -vendors with entirely different sets of entities. In other words, one entity -manager that connects to one database will handle some entities while another -entity manager that connects to another database might handle the rest. - -.. note:: - - Using multiple entity managers is pretty easy, but more advanced and not - usually required. Be sure you actually need multiple entity managers before - adding in this layer of complexity. - -The following configuration code shows how you can configure two entity managers: - -.. configuration-block:: - - .. code-block:: yaml - - doctrine: - dbal: - default_connection: default - connections: - default: - driver: "%database_driver%" - host: "%database_host%" - port: "%database_port%" - dbname: "%database_name%" - user: "%database_user%" - password: "%database_password%" - charset: UTF8 - customer: - driver: "%database_driver2%" - host: "%database_host2%" - port: "%database_port2%" - dbname: "%database_name2%" - user: "%database_user2%" - password: "%database_password2%" - charset: UTF8 - - orm: - default_entity_manager: default - entity_managers: - default: - connection: default - mappings: - AcmeDemoBundle: ~ - AcmeStoreBundle: ~ - customer: - connection: customer - mappings: - AcmeCustomerBundle: ~ - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - - - - - - - - .. code-block:: php - - $container->loadFromExtension('doctrine', array( - 'dbal' => array( - 'default_connection' => 'default', - 'connections' => array( - 'default' => array( - 'driver' => '%database_driver%', - 'host' => '%database_host%', - 'port' => '%database_port%', - 'dbname' => '%database_name%', - 'user' => '%database_user%', - 'password' => '%database_password%', - 'charset' => 'UTF8', - ), - 'customer' => array( - 'driver' => '%database_driver2%', - 'host' => '%database_host2%', - 'port' => '%database_port2%', - 'dbname' => '%database_name2%', - 'user' => '%database_user2%', - 'password' => '%database_password2%', - 'charset' => 'UTF8', - ), - ), - ), - - 'orm' => array( - 'default_entity_manager' => 'default', - 'entity_managers' => array( - 'default' => array( - 'connection' => 'default', - 'mappings' => array( - 'AcmeDemoBundle' => null, - 'AcmeStoreBundle' => null, - ), - ), - 'customer' => array( - 'connection' => 'customer', - 'mappings' => array( - 'AcmeCustomerBundle' => null, - ), - ), - ), - ), - )); - -In this case, you've defined two entity managers and called them ``default`` -and ``customer``. The ``default`` entity manager manages entities in the -``AcmeDemoBundle`` and ``AcmeStoreBundle``, while the ``customer`` entity -manager manages entities in the ``AcmeCustomerBundle``. You've also defined -two connections, one for each entity manager. - -.. note:: - - When working with multiple connections and entity managers, you should be - explicit about which configuration you want. If you *do* omit the name of - the connection or entity manager, the default (i.e. ``default``) is used. - -When working with multiple connections to create your databases: - -.. code-block:: bash - - # Play only with "default" connection - $ php app/console doctrine:database:create - - # Play only with "customer" connection - $ php app/console doctrine:database:create --connection=customer - -When working with multiple entity managers to update your schema: - -.. code-block:: bash - - # Play only with "default" mappings - $ php app/console doctrine:schema:update --force - - # Play only with "customer" mappings - $ php app/console doctrine:schema:update --force --em=customer - -If you *do* omit the entity manager's name when asking for it, -the default entity manager (i.e. ``default``) is returned:: - - class UserController extends Controller - { - public function indexAction() - { - // both return the "default" em - $em = $this->get('doctrine')->getManager(); - $em = $this->get('doctrine')->getManager('default'); - - $customerEm = $this->get('doctrine')->getManager('customer'); - } - } - -You can now use Doctrine just as you did before - using the ``default`` entity -manager to persist and fetch entities that it manages and the ``customer`` -entity manager to persist and fetch its entities. - -The same applies to repository call:: - - class UserController extends Controller - { - public function indexAction() - { - // Retrieves a repository managed by the "default" em - $products = $this->get('doctrine') - ->getRepository('AcmeStoreBundle:Product') - ->findAll() - ; - - // Explicit way to deal with the "default" em - $products = $this->get('doctrine') - ->getRepository('AcmeStoreBundle:Product', 'default') - ->findAll() - ; - - // Retrieves a repository managed by the "customer" em - $customers = $this->get('doctrine') - ->getRepository('AcmeCustomerBundle:Customer', 'customer') - ->findAll() - ; - } - } diff --git a/cookbook/doctrine/registration_form.rst b/cookbook/doctrine/registration_form.rst deleted file mode 100644 index 3398137a46b..00000000000 --- a/cookbook/doctrine/registration_form.rst +++ /dev/null @@ -1,284 +0,0 @@ -.. index:: - single: Doctrine; Simple Registration Form - single: Form; Simple Registration Form - -How to implement a simple Registration Form -=========================================== - -Some forms have extra fields whose values don't need to be stored in the -database. For example, you may want to create a registration form with some -extra fields (like a "terms accepted" checkbox field) and embed the form -that actually stores the account information. - -The simple User model ---------------------- - -You have a simple ``User`` entity mapped to the database:: - - // src/Acme/AccountBundle/Entity/User.php - namespace Acme\AccountBundle\Entity; - - use Doctrine\ORM\Mapping as ORM; - use Symfony\Component\Validator\Constraints as Assert; - use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; - - /** - * @ORM\Entity - * @UniqueEntity(fields="email", message="Email already taken") - */ - class User - { - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue(strategy="AUTO") - */ - protected $id; - - /** - * @ORM\Column(type="string", length=255) - * @Assert\NotBlank() - * @Assert\Email() - */ - protected $email; - - /** - * @ORM\Column(type="string", length=255) - * @Assert\NotBlank() - */ - protected $plainPassword; - - public function getId() - { - return $this->id; - } - - public function getEmail() - { - return $this->email; - } - - public function setEmail($email) - { - $this->email = $email; - } - - public function getPlainPassword() - { - return $this->plainPassword; - } - - public function setPlainPassword($password) - { - $this->plainPassword = $password; - } - } - -This ``User`` entity contains three fields and two of them (``email`` and -``plainPassword``) should display on the form. The email property must be unique -in the database, this is enforced by adding this validation at the top of -the class. - -.. note:: - - If you want to integrate this User within the security system, you need - to implement the :ref:`UserInterface` of the - security component. - -Create a Form for the Model ---------------------------- - -Next, create the form for the ``User`` model:: - - // src/Acme/AccountBundle/Form/Type/UserType.php - namespace Acme\AccountBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - class UserType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('email', 'email'); - $builder->add('plainPassword', 'repeated', array( - 'first_name' => 'password', - 'second_name' => 'confirm', - 'type' => 'password', - )); - } - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'data_class' => 'Acme\AccountBundle\Entity\User' - )); - } - - public function getName() - { - return 'user'; - } - } - -There are just two fields: ``email`` and ``plainPassword`` (repeated to confirm -the entered password). The ``data_class`` option tells the form the name of -data class (i.e. your ``User`` entity). - -.. tip:: - - To explore more things about the form component, read :doc:`/book/forms`. - -Embedding the User form into a Registration Form ------------------------------------------------- - -The form that you'll use for the registration page is not the same as the -form used to simply modify the ``User`` (i.e. ``UserType``). The registration -form will contain further fields like "accept the terms", whose value won't -be stored in the database. - -Start by creating a simple class which represents the "registration":: - - // src/Acme/AccountBundle/Form/Model/Registration.php - namespace Acme\AccountBundle\Form\Model; - - use Symfony\Component\Validator\Constraints as Assert; - - use Acme\AccountBundle\Entity\User; - - class Registration - { - /** - * @Assert\Type(type="Acme\AccountBundle\Entity\User") - * @Assert\Valid() - */ - protected $user; - - /** - * @Assert\NotBlank() - * @Assert\True() - */ - protected $termsAccepted; - - public function setUser(User $user) - { - $this->user = $user; - } - - public function getUser() - { - return $this->user; - } - - public function getTermsAccepted() - { - return $this->termsAccepted; - } - - public function setTermsAccepted($termsAccepted) - { - $this->termsAccepted = (Boolean) $termsAccepted; - } - } - -Next, create the form for this ``Registration`` model:: - - // src/Acme/AccountBundle/Form/Type/RegistrationType.php - namespace Acme\AccountBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - - class RegistrationType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('user', new UserType()); - $builder->add( - 'terms', - 'checkbox', - array('property_path' => 'termsAccepted') - ); - } - - public function getName() - { - return 'registration'; - } - } - -You don't need to use special method for embedding the ``UserType`` form. -A form is a field, too - so you can add this like any other field, with the -expectation that the ``Registration.user`` property will hold an instance -of the ``User`` class. - -Handling the Form Submission ----------------------------- - -Next, you need a controller to handle the form. Start by creating a simple -controller for displaying the registration form:: - - // src/Acme/AccountBundle/Controller/AccountController.php - namespace Acme\AccountBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\HttpFoundation\Response; - - use Acme\AccountBundle\Form\Type\RegistrationType; - use Acme\AccountBundle\Form\Model\Registration; - - class AccountController extends Controller - { - public function registerAction() - { - $registration = new Registration(); - $form = $this->createForm(new RegistrationType(), $registration, array( - 'action' => $this->generateUrl('account_create'), - )); - - return $this->render( - 'AcmeAccountBundle:Account:register.html.twig', - array('form' => $form->createView()) - ); - } - } - -and its template: - -.. code-block:: html+jinja - - {# src/Acme/AccountBundle/Resources/views/Account/register.html.twig #} - {{ form(form) }} - -Finally, create the controller (and the corresponding route ``account_create``) -which handles the form submission. This performs the validation and saves -the data into the database:: - - public function createAction(Request $request) - { - $em = $this->getDoctrine()->getEntityManager(); - - $form = $this->createForm(new RegistrationType(), new Registration()); - - $form->handleRequest($request); - - if ($form->isValid()) { - $registration = $form->getData(); - - $em->persist($registration->getUser()); - $em->flush(); - - return $this->redirect(...); - } - - return $this->render( - 'AcmeAccountBundle:Account:register.html.twig', - array('form' => $form->createView()) - ); - } - -That's it! Your form now validates, and allows you to save the ``User`` -object to the database. The extra ``terms`` checkbox on the ``Registration`` -model class is used during validation, but not actually used afterwards when -saving the User to the database. diff --git a/cookbook/doctrine/reverse_engineering.rst b/cookbook/doctrine/reverse_engineering.rst deleted file mode 100644 index 7ac4b458208..00000000000 --- a/cookbook/doctrine/reverse_engineering.rst +++ /dev/null @@ -1,180 +0,0 @@ -.. index:: - single: Doctrine; Generating entities from existing database - -How to generate Entities from an Existing Database -================================================== - -When starting work on a brand new project that uses a database, two different -situations comes naturally. In most cases, the database model is designed -and built from scratch. Sometimes, however, you'll start with an existing and -probably unchangeable database model. Fortunately, Doctrine comes with a bunch -of tools to help generate model classes from your existing database. - -.. note:: - - As the `Doctrine tools documentation`_ says, reverse engineering is a - one-time process to get started on a project. Doctrine is able to convert - approximately 70-80% of the necessary mapping information based on fields, - indexes and foreign key constraints. Doctrine can't discover inverse - associations, inheritance types, entities with foreign keys as primary keys - or semantical operations on associations such as cascade or lifecycle - events. Some additional work on the generated entities will be necessary - afterwards to design each to fit your domain model specificities. - -This tutorial assumes you're using a simple blog application with the following -two tables: ``blog_post`` and ``blog_comment``. A comment record is linked -to a post record thanks to a foreign key constraint. - -.. code-block:: sql - - CREATE TABLE `blog_post` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT, - `title` varchar(100) COLLATE utf8_unicode_ci NOT NULL, - `content` longtext COLLATE utf8_unicode_ci NOT NULL, - `created_at` datetime NOT NULL, - PRIMARY KEY (`id`) - ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; - - CREATE TABLE `blog_comment` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT, - `post_id` bigint(20) NOT NULL, - `author` varchar(20) COLLATE utf8_unicode_ci NOT NULL, - `content` longtext COLLATE utf8_unicode_ci NOT NULL, - `created_at` datetime NOT NULL, - PRIMARY KEY (`id`), - KEY `blog_comment_post_id_idx` (`post_id`), - CONSTRAINT `blog_post_id` FOREIGN KEY (`post_id`) REFERENCES `blog_post` (`id`) ON DELETE CASCADE - ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; - -Before diving into the recipe, be sure your database connection parameters are -correctly setup in the ``app/config/parameters.yml`` file (or wherever your -database configuration is kept) and that you have initialized a bundle that -will host your future entity class. In this tutorial it's assumed that -an ``AcmeBlogBundle`` exists and is located under the ``src/Acme/BlogBundle`` -folder. - -The first step towards building entity classes from an existing database -is to ask Doctrine to introspect the database and generate the corresponding -metadata files. Metadata files describe the entity class to generate based on -tables fields. - -.. code-block:: bash - - $ php app/console doctrine:mapping:convert xml ./src/Acme/BlogBundle/Resources/config/doctrine/metadata/orm --from-database --force - -This command line tool asks Doctrine to introspect the database and generate -the XML metadata files under the ``src/Acme/BlogBundle/Resources/config/doctrine/metadata/orm`` -folder of your bundle. - -.. tip:: - - It's also possible to generate metadata class in YAML format by changing the - first argument to `yml`. - -The generated ``BlogPost.dcm.xml`` metadata file looks as follows: - -.. code-block:: xml - - - - - DEFERRED_IMPLICIT - - - - - - - - - - - - - -.. note:: - - If you have ``oneToMany`` relationships between your entities, - you will need to edit the generated ``xml`` or ``yml`` files to add - a section on the specific entities for ``oneToMany`` defining the - ``inversedBy`` and the ``mappedBy`` pieces. - -Once the metadata files are generated, you can ask Doctrine to import the -schema and build related entity classes by executing the following two commands. - -.. code-block:: bash - - $ php app/console doctrine:mapping:import AcmeBlogBundle annotation - $ php app/console doctrine:generate:entities AcmeBlogBundle - -The first command generates entity classes with an annotations mapping, but -you can of course change the ``annotation`` argument to ``xml`` or ``yml``. -The newly created ``BlogComment`` entity class looks as follow: - -.. code-block:: php - - - - - - - - .. code-block:: php - - // app/config/config_test.php - $container->loadFromExtension('swiftmailer', array( - 'disable_delivery' => "true", - )); - -If you'd also like to disable deliver in the ``dev`` environment, simply -add this same configuration to the ``config_dev.yml`` file. - -Sending to a Specified Address ------------------------------- - -You can also choose to have all email sent to a specific address, instead -of the address actually specified when sending the message. This can be done -via the ``delivery_address`` option: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_dev.yml - swiftmailer: - delivery_address: dev@example.com - - .. code-block:: xml - - - - - - - - .. code-block:: php - - // app/config/config_dev.php - $container->loadFromExtension('swiftmailer', array( - 'delivery_address' => "dev@example.com", - )); - -Now, suppose you're sending an email to ``recipient@example.com``. - -.. code-block:: php - - public function indexAction($name) - { - $message = \Swift_Message::newInstance() - ->setSubject('Hello Email') - ->setFrom('send@example.com') - ->setTo('recipient@example.com') - ->setBody( - $this->renderView( - 'HelloBundle:Hello:email.txt.twig', - array('name' => $name) - ) - ) - ; - $this->get('mailer')->send($message); - - return $this->render(...); - } - -In the ``dev`` environment, the email will instead be sent to ``dev@example.com``. -Swiftmailer 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. Swiftmailer 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. - -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 -``config_dev.yml`` file, which will cause the redirect to stop and allow -you to open the report with details of the sent emails. - -.. tip:: - - Alternatively, you can open the profiler after the redirect and search - by the submit URL used on previous request (e.g. ``/contact/handle``). - The profiler's search feature allows you to load the profiler information - for any past requests. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_dev.yml - web_profiler: - intercept_redirects: true - - .. code-block:: xml - - - - - - - - .. code-block:: php - - // app/config/config_dev.php - $container->loadFromExtension('web_profiler', array( - 'intercept_redirects' => 'true', - )); diff --git a/cookbook/email/email.rst b/cookbook/email/email.rst deleted file mode 100644 index 9718aa67f14..00000000000 --- a/cookbook/email/email.rst +++ /dev/null @@ -1,139 +0,0 @@ -.. index:: - single: Emails - -How to send an Email -==================== - -Sending emails is a classic task for any web application and one that has -special complications and potential pitfalls. Instead of recreating the wheel, -one solution to send emails is to use the ``SwiftmailerBundle``, which leverages -the power of the `Swiftmailer`_ library. - -.. note:: - - Don't forget to enable the bundle in your kernel before using it:: - - public function registerBundles() - { - $bundles = array( - // ... - - new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(), - ); - - // ... - } - -.. _swift-mailer-configuration: - -Configuration -------------- - -Before using Swiftmailer, be sure to include its configuration. The only -mandatory configuration parameter is ``transport``: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - swiftmailer: - transport: smtp - encryption: ssl - auth_mode: login - host: smtp.gmail.com - username: your_username - password: your_password - - .. code-block:: xml - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('swiftmailer', array( - 'transport' => "smtp", - 'encryption' => "ssl", - 'auth_mode' => "login", - 'host' => "smtp.gmail.com", - 'username' => "your_username", - 'password' => "your_password", - )); - -The majority of the Swiftmailer configuration deals with how the messages -themselves should be delivered. - -The following configuration attributes are available: - -* ``transport`` (``smtp``, ``mail``, ``sendmail``, or ``gmail``) -* ``username`` -* ``password`` -* ``host`` -* ``port`` -* ``encryption`` (``tls``, or ``ssl``) -* ``auth_mode`` (``plain``, ``login``, or ``cram-md5``) -* ``spool`` - - * ``type`` (how to queue the messages, ``file`` or ``memory`` is supported, see :doc:`/cookbook/email/spool`) - * ``path`` (where to store the messages) -* ``delivery_address`` (an email address where to send ALL emails) -* ``disable_delivery`` (set to true to disable delivery completely) - -Sending Emails --------------- - -The Swiftmailer library works by creating, configuring and then sending -``Swift_Message`` objects. The "mailer" is responsible for the actual delivery -of the message and is accessible via the ``mailer`` service. Overall, sending -an email is pretty straightforward:: - - public function indexAction($name) - { - $message = \Swift_Message::newInstance() - ->setSubject('Hello Email') - ->setFrom('send@example.com') - ->setTo('recipient@example.com') - ->setBody( - $this->renderView( - 'HelloBundle:Hello:email.txt.twig', - array('name' => $name) - ) - ) - ; - $this->get('mailer')->send($message); - - return $this->render(...); - } - -To keep things decoupled, the email body has been stored in a template and -rendered with the ``renderView()`` method. - -The ``$message`` object supports many more options, such as including attachments, -adding HTML content, and much more. Fortunately, Swiftmailer covers the topic -of `Creating Messages`_ in great detail in its documentation. - -.. tip:: - - Several other cookbook articles are available related to sending emails - in Symfony2: - - * :doc:`gmail` - * :doc:`dev_environment` - * :doc:`spool` - -.. _`Swiftmailer`: http://swiftmailer.org/ -.. _`Creating Messages`: http://swiftmailer.org/docs/messages.html diff --git a/cookbook/email/gmail.rst b/cookbook/email/gmail.rst deleted file mode 100644 index 63ff6a12eba..00000000000 --- a/cookbook/email/gmail.rst +++ /dev/null @@ -1,71 +0,0 @@ -.. index:: - single: Emails; Gmail - -How to use Gmail to send Emails -=============================== - -During development, instead of using a regular SMTP server to send emails, you -might find using Gmail easier and more practical. The Swiftmailer bundle makes -it really easy. - -.. tip:: - - Instead of using your regular Gmail account, it's of course recommended - that you create a special account. - -In the development configuration file, change the ``transport`` setting to -``gmail`` and set the ``username`` and ``password`` to the Google credentials: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_dev.yml - swiftmailer: - transport: gmail - username: your_gmail_username - password: your_gmail_password - - .. code-block:: xml - - - - - - - - .. code-block:: php - - // app/config/config_dev.php - $container->loadFromExtension('swiftmailer', array( - 'transport' => "gmail", - 'username' => "your_gmail_username", - 'password' => "your_gmail_password", - )); - -You're done! - -.. tip:: - - If you are using the Symfony Standard Edition, configure the parameters at ``parameters.yml``: - - .. code-block:: yaml - - # app/config/parameters.yml - parameters: - ... - mailer_transport: gmail - mailer_host: ~ - mailer_user: your_gmail_username - mailer_password: your_gmail_password - -.. note:: - - The ``gmail`` transport is simply a shortcut that uses the ``smtp`` transport - and sets ``encryption``, ``auth_mode`` and ``host`` to work with Gmail. diff --git a/cookbook/email/index.rst b/cookbook/email/index.rst deleted file mode 100644 index 7209fbcc652..00000000000 --- a/cookbook/email/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Email -===== - -.. toctree:: - :maxdepth: 2 - - email - gmail - dev_environment - spool - testing diff --git a/cookbook/email/spool.rst b/cookbook/email/spool.rst deleted file mode 100644 index edea9482730..00000000000 --- a/cookbook/email/spool.rst +++ /dev/null @@ -1,131 +0,0 @@ -.. index:: - single: Emails; Spooling - -How to Spool Emails -=================== - -When you are using the ``SwiftmailerBundle`` to send an email from a Symfony2 -application, it will default to sending the email immediately. You may, however, -want to avoid the performance hit of the communication between ``Swiftmailer`` -and the email transport, 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 means that ``Swiftmailer`` -does not attempt to send the email but instead saves the message to 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 -by ``Swiftmailer``. - -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 -swiftmailer with the memory option, use the following configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - swiftmailer: - # ... - spool: { type: memory } - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('swiftmailer', array( - ..., - 'spool' => array('type' => 'memory') - )); - -Spool using a file ------------------- - -In order to use the spool with a file, use the following configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - swiftmailer: - # ... - spool: - type: file - path: /path/to/spool - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('swiftmailer', array( - // ... - - 'spool' => array( - 'type' => 'file', - 'path' => '/path/to/spool', - ), - )); - -.. tip:: - - If you want to store the spool somewhere with your project directory, - remember that you can use the `%kernel.root_dir%` parameter to reference - the project's root: - - .. code-block:: yaml - - path: "%kernel.root_dir%/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:: bash - - $ php app/console swiftmailer:spool:send --env=prod - -It has an option to limit the number of messages to be sent: - -.. code-block:: bash - - $ php app/console swiftmailer:spool:send --message-limit=10 --env=prod - -You can also set the time limit in seconds: - -.. code-block:: bash - - $ php app/console swiftmailer:spool:send --time-limit=10 --env=prod - -Of course you will not want to run this manually in reality. Instead, the -console command should be triggered by a cron job or scheduled task and run -at a regular interval. diff --git a/cookbook/email/testing.rst b/cookbook/email/testing.rst deleted file mode 100644 index 7386618303d..00000000000 --- a/cookbook/email/testing.rst +++ /dev/null @@ -1,70 +0,0 @@ -.. index:: - single: Emails; Testing - -How to test that an Email is sent in a functional Test -====================================================== - -Sending e-mails with Symfony2 is pretty straightforward thanks to the -``SwiftmailerBundle``, which leverages the power of the `Swiftmailer`_ library. - -To functionally test that an email was sent, and even assert the email subject, -content or any other headers, you can use :ref:`the Symfony2 Profiler `. - -Start with an easy controller action that sends an e-mail:: - - public function sendEmailAction($name) - { - $message = \Swift_Message::newInstance() - ->setSubject('Hello Email') - ->setFrom('send@example.com') - ->setTo('recipient@example.com') - ->setBody('You should see me from the profiler!') - ; - - $this->get('mailer')->send($message); - - return $this->render(...); - } - -.. note:: - - Don't forget to enable the profiler as explained in :doc:`/cookbook/testing/profiling`. - -In your functional test, use the ``swiftmailer`` collector on the profiler -to get information about the messages send on the previous request:: - - // src/Acme/DemoBundle/Tests/Controller/MailControllerTest.php - use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - - class MailControllerTest extends WebTestCase - { - public function testMailIsSentAndContentIsOk() - { - $client = static::createClient(); - - // Enable 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'); - - // Check that an e-mail was sent - $this->assertEquals(1, $mailCollector->getMessageCount()); - - $collectedMessages = $mailCollector->getMessages(); - $message = $collectedMessages[0]; - - // Asserting e-mail data - $this->assertInstanceOf('Swift_Message', $message); - $this->assertEquals('Hello Email', $message->getSubject()); - $this->assertEquals('send@example.com', key($message->getFrom())); - $this->assertEquals('recipient@example.com', key($message->getTo())); - $this->assertEquals( - 'You should see me from the profiler!', - $message->getBody() - ); - } - } - -.. _Swiftmailer: http://swiftmailer.org/ diff --git a/cookbook/event_dispatcher/before_after_filters.rst b/cookbook/event_dispatcher/before_after_filters.rst deleted file mode 100755 index 3ea82f65a95..00000000000 --- a/cookbook/event_dispatcher/before_after_filters.rst +++ /dev/null @@ -1,289 +0,0 @@ -.. index:: - single: Event Dispatcher - -How to setup before and after Filters -===================================== - -It is quite common in web application development to need some logic to be -executed just before or just after your controller actions acting as filters -or hooks. - -In symfony1, this was achieved with the preExecute and postExecute methods. -Most major frameworks have similar methods but there is no such thing in Symfony2. -The good news is that there is a much better way to interfere with the -Request -> Response process using the :doc:`EventDispatcher component`. - -Token validation Example ------------------------- - -Imagine that you need to develop an API where some controllers are public -but some others are restricted to one or some clients. For these private features, -you might provide a token to your clients to identify themselves. - -So, before executing your controller action, you need to check if the action -is restricted or not. If it is restricted, you need to validate the provided -token. - -.. note:: - - Please note that for simplicity in this recipe, tokens will be defined - in config and neither database setup nor authentication via the Security - component will be used. - -Before filters with the ``kernel.controller`` Event ---------------------------------------------------- - -First, store some basic token configuration using ``config.yml`` and the -parameters key: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - parameters: - tokens: - client1: pass1 - client2: pass2 - - .. code-block:: xml - - - - - pass1 - pass2 - - - - .. code-block:: php - - // app/config/config.php - $container->setParameter('tokens', array( - 'client1' => 'pass1', - 'client2' => 'pass2', - )); - -Tag Controllers to be checked -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A ``kernel.controller`` listener gets notified on *every* request, right before -the controller is executed. So, first, you need some way to identify if the -controller that matches the request needs token validation. - -A clean and easy way is to create an empty interface and make the controllers -implement it:: - - namespace Acme\DemoBundle\Controller; - - interface TokenAuthenticatedController - { - // ... - } - -A controller that implements this interface simply looks like this:: - - namespace Acme\DemoBundle\Controller; - - use Acme\DemoBundle\Controller\TokenAuthenticatedController; - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - - class FooController extends Controller implements TokenAuthenticatedController - { - // An action that needs authentication - public function barAction() - { - // ... - } - } - -Creating an Event Listener -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Next, you'll need to create an event listener, which will hold the logic -that you want executed before your controllers. If you're not familiar with -event listeners, you can learn more about them at :doc:`/cookbook/service_container/event_listener`:: - - // src/Acme/DemoBundle/EventListener/TokenListener.php - namespace Acme\DemoBundle\EventListener; - - use Acme\DemoBundle\Controller\TokenAuthenticatedController; - use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - use Symfony\Component\HttpKernel\Event\FilterControllerEvent; - - class TokenListener - { - private $tokens; - - public function __construct($tokens) - { - $this->tokens = $tokens; - } - - public function onKernelController(FilterControllerEvent $event) - { - $controller = $event->getController(); - - /* - * $controller passed can be either a class or a Closure. This is not usual in Symfony2 but it may happen. - * If it is a class, it comes in array format - */ - if (!is_array($controller)) { - return; - } - - if ($controller[0] instanceof TokenAuthenticatedController) { - $token = $event->getRequest()->query->get('token'); - if (!in_array($token, $this->tokens)) { - throw new AccessDeniedHttpException('This action needs a valid token!'); - } - } - } - } - -Registering the Listener -~~~~~~~~~~~~~~~~~~~~~~~~ - -Finally, register your listener as a service and tag it as an event listener. -By listening on ``kernel.controller``, you're telling Symfony that you want -your listener to be called just before any controller is executed. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml (or inside your services.yml) - services: - demo.tokens.action_listener: - class: Acme\DemoBundle\EventListener\TokenListener - arguments: ["%tokens%"] - tags: - - { name: kernel.event_listener, event: kernel.controller, method: onKernelController } - - .. code-block:: xml - - - - %tokens% - - - - .. code-block:: php - - // app/config/config.php (or inside your services.php) - use Symfony\Component\DependencyInjection\Definition; - - $listener = new Definition('Acme\DemoBundle\EventListener\TokenListener', array('%tokens%')); - $listener->addTag('kernel.event_listener', array( - 'event' => 'kernel.controller', - 'method' => 'onKernelController' - )); - $container->setDefinition('demo.tokens.action_listener', $listener); - -With this configuration, your ``TokenListener`` ``onKernelController`` method -will be executed on each request. If the controller that is about to be executed -implements ``TokenAuthenticatedController``, token authentication is -applied. This lets you have a "before" filter on any controller that you -want. - -After filters with the ``kernel.response`` Event ------------------------------------------------- - -In addition to having a "hook" that's executed before your controller, you -can also add a hook that's executed *after* your controller. For this example, -imagine that you want to add a sha1 hash (with a salt using that token) to -all responses that have passed this token authentication. - -Another core Symfony event - called ``kernel.response`` - is notified on -every request, but after the controller returns a Response object. Creating -an "after" listener is as easy as creating a listener class and registering -it as a service on this event. - -For example, take the ``TokenListener`` 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) - { - // ... - - if ($controller[0] instanceof TokenAuthenticatedController) { - $token = $event->getRequest()->query->get('token'); - if (!in_array($token, $this->tokens)) { - throw new AccessDeniedHttpException('This action needs a valid token!'); - } - - // mark the request as having passed token authentication - $event->getRequest()->attributes->set('auth_token', $token); - } - } - -Now, add another method to this class - ``onKernelResponse`` - that looks -for this flag on the request object and sets a custom 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; - - public function onKernelResponse(FilterResponseEvent $event) - { - // check to see if onKernelController marked this as a token "auth'ed" request - if (!$token = $event->getRequest()->attributes->get('auth_token')) { - return; - } - - $response = $event->getResponse(); - - // create a hash and set it as a response header - $hash = sha1($response->getContent().$token); - $response->headers->set('X-CONTENT-HASH', $hash); - } - -Finally, a second "tag" is needed on the service definition to notify Symfony -that the ``onKernelResponse`` event should be notified for the ``kernel.response`` -event: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml (or inside your services.yml) - services: - demo.tokens.action_listener: - class: Acme\DemoBundle\EventListener\TokenListener - arguments: ["%tokens%"] - tags: - - { name: kernel.event_listener, event: kernel.controller, method: onKernelController } - - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse } - - .. code-block:: xml - - - - %tokens% - - - - - .. code-block:: php - - // app/config/config.php (or inside your services.php) - use Symfony\Component\DependencyInjection\Definition; - - $listener = new Definition('Acme\DemoBundle\EventListener\TokenListener', array('%tokens%')); - $listener->addTag('kernel.event_listener', array( - 'event' => 'kernel.controller', - 'method' => 'onKernelController' - )); - $listener->addTag('kernel.event_listener', array( - 'event' => 'kernel.response', - 'method' => 'onKernelResponse' - )); - $container->setDefinition('demo.tokens.action_listener', $listener); - -That's it! The ``TokenListener`` is now notified before every controller is -executed (``onKernelController``) and after every controller returns a response -(``onKernelResponse``). By making specific controllers implement the ``TokenAuthenticatedController`` -interface, your listener knows which controllers it should take action on. -And by storing a value in the request's "attributes" bag, the ``onKernelResponse`` -method knows to add the extra header. Have fun! diff --git a/cookbook/event_dispatcher/class_extension.rst b/cookbook/event_dispatcher/class_extension.rst deleted file mode 100644 index 43d1f01793c..00000000000 --- a/cookbook/event_dispatcher/class_extension.rst +++ /dev/null @@ -1,125 +0,0 @@ -.. index:: - single: Event Dispatcher - -How to extend a Class without using Inheritance -=============================================== - -To allow multiple classes to add methods to another one, you can define the -magic ``__call()`` method in the class you want to be extended like this: - -.. code-block:: php - - class Foo - { - // ... - - public function __call($method, $arguments) - { - // create an event named 'foo.method_is_not_found' - $event = new HandleUndefinedMethodEvent($this, $method, $arguments); - $this->dispatcher->dispatch('foo.method_is_not_found', $event); - - // no listener was able to process the event? The method does not exist - if (!$event->isProcessed()) { - throw new \Exception(sprintf('Call to undefined method %s::%s.', get_class($this), $method)); - } - - // return the listener returned value - return $event->getReturnValue(); - } - } - -This uses a special ``HandleUndefinedMethodEvent`` that should also be -created. This is a generic class that could be reused each time you need to -use this pattern of class extension: - -.. code-block:: php - - use Symfony\Component\EventDispatcher\Event; - - class HandleUndefinedMethodEvent extends Event - { - protected $subject; - protected $method; - protected $arguments; - protected $returnValue; - protected $isProcessed = false; - - public function __construct($subject, $method, $arguments) - { - $this->subject = $subject; - $this->method = $method; - $this->arguments = $arguments; - } - - public function getSubject() - { - return $this->subject; - } - - public function getMethod() - { - return $this->method; - } - - public function getArguments() - { - return $this->arguments; - } - - /** - * Sets the value to return and stops other listeners from being notified - */ - public function setReturnValue($val) - { - $this->returnValue = $val; - $this->isProcessed = true; - $this->stopPropagation(); - } - - public function getReturnValue($val) - { - return $this->returnValue; - } - - public function isProcessed() - { - return $this->isProcessed; - } - } - -Next, create a class that will listen to the ``foo.method_is_not_found`` event -and *add* the method ``bar()``: - -.. code-block:: php - - class Bar - { - public function onFooMethodIsNotFound(HandleUndefinedMethodEvent $event) - { - // only respond to the calls to the 'bar' method - if ('bar' != $event->getMethod()) { - // allow another listener to take care of this unknown method - return; - } - - // the subject object (the foo instance) - $foo = $event->getSubject(); - - // the bar method arguments - $arguments = $event->getArguments(); - - // ... do something - - // set the return value - $event->setReturnValue($someValue); - } - } - -Finally, add the new ``bar`` method to the ``Foo`` class by registering an -instance of ``Bar`` with the ``foo.method_is_not_found`` event: - -.. code-block:: php - - $bar = new Bar(); - $dispatcher->addListener('foo.method_is_not_found', array($bar, 'onFooMethodIsNotFound')); diff --git a/cookbook/event_dispatcher/index.rst b/cookbook/event_dispatcher/index.rst deleted file mode 100644 index 8a30d4d1584..00000000000 --- a/cookbook/event_dispatcher/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Event Dispatcher -================ - -.. toctree:: - :maxdepth: 2 - - before_after_filters - class_extension - method_behavior - \ No newline at end of file diff --git a/cookbook/event_dispatcher/method_behavior.rst b/cookbook/event_dispatcher/method_behavior.rst deleted file mode 100644 index 69b3d88cb69..00000000000 --- a/cookbook/event_dispatcher/method_behavior.rst +++ /dev/null @@ -1,57 +0,0 @@ -.. index:: - single: Event Dispatcher - -How to customize a Method Behavior without using Inheritance -============================================================ - -Doing something before or after a Method Call ---------------------------------------------- - -If you want to do something just before, or just after a method is called, you -can dispatch an event respectively at the beginning or at the end of the -method:: - - class Foo - { - // ... - - public function send($foo, $bar) - { - // do something before the method - $event = new FilterBeforeSendEvent($foo, $bar); - $this->dispatcher->dispatch('foo.pre_send', $event); - - // get $foo and $bar from the event, they may have been modified - $foo = $event->getFoo(); - $bar = $event->getBar(); - - // the real method implementation is here - $ret = ...; - - // do something after the method - $event = new FilterSendReturnValue($ret); - $this->dispatcher->dispatch('foo.post_send', $event); - - return $event->getReturnValue(); - } - } - -In this example, two events are thrown: ``foo.pre_send``, before the method is -executed, and ``foo.post_send`` after the method is executed. Each uses a -custom Event class to communicate information to the listeners of the two -events. These event classes would need to be created by you and should allow, -in this example, the variables ``$foo``, ``$bar`` and ``$ret`` to be retrieved -and set by the listeners. - -For example, assuming the ``FilterSendReturnValue`` has a ``setReturnValue`` -method, one listener might look like this: - -.. code-block:: php - - public function onFooPostSend(FilterSendReturnValue $event) - { - $ret = $event->getReturnValue(); - // modify the original ``$ret`` value - - $event->setReturnValue($ret); - } diff --git a/cookbook/form/create_custom_field_type.rst b/cookbook/form/create_custom_field_type.rst deleted file mode 100644 index a12fbc3c875..00000000000 --- a/cookbook/form/create_custom_field_type.rst +++ /dev/null @@ -1,348 +0,0 @@ -.. index:: - single: Form; Custom field type - -How to Create a Custom Form Field Type -====================================== - -Symfony comes with a bunch of core field types available for building forms. -However there are situations where you may want to create a custom form field -type for a specific purpose. This recipe assumes you need a field definition -that holds a person's gender, based on the existing choice field. This section -explains how the field is defined, how you can customize its layout and finally, -how you can register it for use in your application. - -Defining the Field Type ------------------------ - -In order to create the custom field type, first you have to create the class -representing the field. In this situation the class holding the field type -will be called `GenderType` and the file will be stored in the default location -for form fields, which is ``\Form\Type``. Make sure the field extends -:class:`Symfony\\Component\\Form\\AbstractType`:: - - // src/Acme/DemoBundle/Form/Type/GenderType.php - namespace Acme\DemoBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - class GenderType extends AbstractType - { - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'choices' => array( - 'm' => 'Male', - 'f' => 'Female', - ) - )); - } - - public function getParent() - { - return 'choice'; - } - - public function getName() - { - return 'gender'; - } - } - -.. tip:: - - The location of this file is not important - the ``Form\Type`` directory - is just a convention. - -Here, the return value of the ``getParent`` function indicates that you're -extending the ``choice`` field type. This means that, by default, you inherit -all of the logic and rendering of that field type. To see some of the logic, -check out the `ChoiceType`_ class. There are three methods that are particularly -important: - -* ``buildForm()`` - Each field type has a ``buildForm`` method, which is where - you configure and build any field(s). Notice that this is the same method - you use to setup *your* forms, and it works the same here. - -* ``buildView()`` - This method is used to set any extra variables you'll - need when rendering your field in a template. For example, in `ChoiceType`_, - a ``multiple`` variable is set and used in the template to set (or not - set) the ``multiple`` attribute on the ``select`` field. See `Creating a Template for the Field`_ - for more details. - -* ``setDefaultOptions()`` - This defines options for your form type that - can be used in ``buildForm()`` and ``buildView()``. There are a lot of - options common to all fields (see :doc:`/reference/forms/types/form`), - but you can create any others that you need here. - -.. tip:: - - If you're creating a field that consists of many fields, then be sure - to set your "parent" type as ``form`` or something that extends ``form``. - Also, if you need to modify the "view" of any of your child types from - your parent type, use the ``finishView()`` method. - -The ``getName()`` method returns an identifier which should be unique in -your application. This is used in various places, such as when customizing -how your form type will be rendered. - -The goal of this field was to extend the choice type to enable selection of -a gender. This is achieved by fixing the ``choices`` to a list of possible -genders. - -Creating a Template for the Field ---------------------------------- - -Each field type is rendered by a template fragment, which is determined in -part by the value of your ``getName()`` method. For more information, see -:ref:`cookbook-form-customization-form-themes`. - -In this case, since the parent field is ``choice``, you don't *need* to do -any work as the custom field type will automatically be rendered like a ``choice`` -type. But for the sake of this example, let's suppose that when your field -is "expanded" (i.e. radio buttons or checkboxes, instead of a select field), -you want to always render it in a ``ul`` element. In your form theme template -(see above link for details), create a ``gender_widget`` block to handle this: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #} - {% block gender_widget %} - {% spaceless %} - {% if expanded %} -
    - {% for child in form %} -
  • - {{ form_widget(child) }} - {{ form_label(child) }} -
  • - {% endfor %} -
- {% else %} - {# just let the choice widget render the select tag #} - {{ block('choice_widget') }} - {% endif %} - {% endspaceless %} - {% endblock %} - - .. code-block:: html+php - - - -
    block($form, 'widget_container_attributes') ?>> - -
  • - widget($child) ?> - label($child) ?> -
  • - -
- - - renderBlock('choice_widget') ?> - - -.. note:: - - Make sure the correct widget prefix is used. In this example the name should - be ``gender_widget``, according to the value returned by ``getName``. - Further, the main config file should point to the custom form template - so that it's used when rendering all forms. - - .. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - twig: - form: - resources: - - 'AcmeDemoBundle:Form:fields.html.twig' - - .. code-block:: xml - - - - - AcmeDemoBundle:Form:fields.html.twig - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('twig', array( - 'form' => array( - 'resources' => array( - 'AcmeDemoBundle:Form:fields.html.twig', - ), - ), - )); - -Using the Field Type --------------------- - -You can now use your custom field type immediately, simply by creating a -new instance of the type in one of your forms:: - - // src/Acme/DemoBundle/Form/Type/AuthorType.php - namespace Acme\DemoBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - - class AuthorType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('gender_code', new GenderType(), array( - 'empty_value' => 'Choose a gender', - )); - } - } - -But this only works because the ``GenderType()`` is very simple. What if -the gender codes were stored in configuration or in a database? The next -section explains how more complex field types solve this problem. - -.. _form-cookbook-form-field-service: - -Creating your Field Type as a Service -------------------------------------- - -So far, this entry has assumed that you have a very simple custom field type. -But if you need access to configuration, a database connection, or some other -service, then you'll want to register your custom type as a service. For -example, suppose that you're storing the gender parameters in configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - parameters: - genders: - m: Male - f: Female - - .. code-block:: xml - - - - - Male - Female - - - - .. code-block:: php - - // app/config/config.php - $container->setParameter('genders.m', 'Male'); - $container->setParameter('genders.f', 'Female'); - -To use the parameter, define your custom field type as a service, injecting -the ``genders`` parameter value as the first argument to its to-be-created -``__construct`` function: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/DemoBundle/Resources/config/services.yml - services: - acme_demo.form.type.gender: - class: Acme\DemoBundle\Form\Type\GenderType - arguments: - - "%genders%" - tags: - - { name: form.type, alias: gender } - - .. code-block:: xml - - - - %genders% - - - - .. code-block:: php - - // src/Acme/DemoBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; - - $container - ->setDefinition('acme_demo.form.type.gender', new Definition( - 'Acme\DemoBundle\Form\Type\GenderType', - array('%genders%') - )) - ->addTag('form.type', array( - 'alias' => 'gender', - )) - ; - -.. tip:: - - Make sure the services file is being imported. See :ref:`service-container-imports-directive` - for details. - -Be sure that the ``alias`` attribute of the tag corresponds with the value -returned by the ``getName`` method defined earlier. You'll see the importance -of this in a moment when you use the custom field type. But first, add a ``__construct`` -method to ``GenderType``, which receives the gender configuration:: - - // src/Acme/DemoBundle/Form/Type/GenderType.php - namespace Acme\DemoBundle\Form\Type; - - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - // ... - - // ... - class GenderType extends AbstractType - { - private $genderChoices; - - public function __construct(array $genderChoices) - { - $this->genderChoices = $genderChoices; - } - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'choices' => $this->genderChoices, - )); - } - - // ... - } - -Great! The ``GenderType`` is now fueled by the configuration parameters and -registered as a service. Additionally, because you used the ``form.type`` alias in its -configuration, using the field is now much easier:: - - // src/Acme/DemoBundle/Form/Type/AuthorType.php - namespace Acme\DemoBundle\Form\Type; - - use Symfony\Component\Form\FormBuilderInterface; - - // ... - - class AuthorType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('gender_code', 'gender', array( - 'empty_value' => 'Choose a gender', - )); - } - } - -Notice that instead of instantiating a new instance, you can just refer to -it by the alias used in your service configuration, ``gender``. Have fun! - -.. _`ChoiceType`: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php -.. _`FieldType`: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Extension/Core/Type/FieldType.php diff --git a/cookbook/form/create_form_type_extension.rst b/cookbook/form/create_form_type_extension.rst deleted file mode 100644 index e6cb3593508..00000000000 --- a/cookbook/form/create_form_type_extension.rst +++ /dev/null @@ -1,321 +0,0 @@ -.. index:: - single: Form; Form type extension - -How to Create a Form Type Extension -=================================== - -:doc:`Custom form field types` are great when -you need field types with a specific purpose, such as a gender selector, -or a VAT number input. - -But sometimes, you don't really need to add new field types - you want -to add features on top of existing types. This is where form type -extensions come in. - -Form type extensions have 2 main use-cases: - -#. You want to add a **generic feature to several types** (such as - adding a "help" text to every field type); -#. You want to add a **specific feature to a single type** (such - as adding a "download" feature to the "file" field type). - -In both those cases, it might be possible to achieve your goal with custom -form rendering, or custom form field types. But using form type extensions -can be cleaner (by limiting the amount of business logic in templates) -and more flexible (you can add several type extensions to a single form -type). - -Form type extensions can achieve most of what custom field types can do, -but instead of being field types of their own, **they plug into existing types**. - -Imagine that you manage a ``Media`` entity, and that each media is associated -to a file. Your ``Media`` form uses a file type, but when editing the entity, -you would like to see its image automatically rendered next to the file -input. - -You could of course do this by customizing how this field is rendered in a -template. But field type extensions allow you to do this in a nice DRY fashion. - -Defining the Form Type Extension --------------------------------- - -Your first task will be to create the form type extension class. Let's -call it ``ImageTypeExtension``. By standard, form extensions usually live -in the ``Form\Extension`` directory of one of your bundles. - -When creating a form type extension, you can either implement the -:class:`Symfony\\Component\\Form\\FormTypeExtensionInterface` interface -or extend the :class:`Symfony\\Component\\Form\\AbstractTypeExtension` -class. In most cases, it's easier to extend the abstract class:: - - // src/Acme/DemoBundle/Form/Extension/ImageTypeExtension.php - namespace Acme\DemoBundle\Form\Extension; - - use Symfony\Component\Form\AbstractTypeExtension; - - class ImageTypeExtension extends AbstractTypeExtension - { - /** - * Returns the name of the type being extended. - * - * @return string The name of the type being extended - */ - public function getExtendedType() - { - return 'file'; - } - } - -The only method you **must** implement is the ``getExtendedType`` function. -It is used to indicate the name of the form type that will be extended -by your extension. - -.. tip:: - - The value you return in the ``getExtendedType`` method corresponds - to the value returned by the ``getName`` method in the form type class - you wish to extend. - -In addition to the ``getExtendedType`` function, you will probably want -to override one of the following methods: - -* ``buildForm()`` - -* ``buildView()`` - -* ``setDefaultOptions()`` - -* ``finishView()`` - -For more information on what those methods do, you can refer to the -:doc:`Creating Custom Field Types` -cookbook article. - -Registering your Form Type Extension as a Service --------------------------------------------------- - -The next step is to make Symfony aware of your extension. All you -need to do is to declare it as a service by using the ``form.type_extension`` -tag: - -.. configuration-block:: - - .. code-block:: yaml - - services: - acme_demo_bundle.image_type_extension: - class: Acme\DemoBundle\Form\Extension\ImageTypeExtension - tags: - - { name: form.type_extension, alias: file } - - .. code-block:: xml - - - - - - .. code-block:: php - - $container - ->register( - 'acme_demo_bundle.image_type_extension', - 'Acme\DemoBundle\Form\Extension\ImageTypeExtension' - ) - ->addTag('form.type_extension', array('alias' => 'file')); - -The ``alias`` key of the tag is the type of field that this extension should -be applied to. In your case, as you want to extend the ``file`` field type, -you will use ``file`` as an alias. - -Adding the extension Business Logic ------------------------------------ - -The goal of your extension is to display nice images next to file inputs -(when the underlying model contains images). For that purpose, let's assume -that you use an approach similar to the one described in -:doc:`How to handle File Uploads with Doctrine`: -you have a Media model with a file property (corresponding to the file field -in the form) and a path property (corresponding to the image path in the -database):: - - // src/Acme/DemoBundle/Entity/Media.php - namespace Acme\DemoBundle\Entity; - - use Symfony\Component\Validator\Constraints as Assert; - - class Media - { - // ... - - /** - * @var string The path - typically stored in the database - */ - private $path; - - /** - * @var \Symfony\Component\HttpFoundation\File\UploadedFile - * @Assert\File(maxSize="2M") - */ - public $file; - - // ... - - /** - * Get the image url - * - * @return null|string - */ - public function getWebPath() - { - // ... $webPath being the full image url, to be used in templates - - return $webPath; - } - } - -Your form type extension class will need to do two things in order to extend -the ``file`` form type: - -#. Override the ``setDefaultOptions`` method in order to add an image_path - option; -#. Override the ``buildForm`` and ``buildView`` methods in order to pass the image - url to the view. - -The logic is the following: when adding a form field of type ``file``, -you will be able to specify a new option: ``image_path``. This option will -tell the file field how to get the actual image url in order to display -it in the view:: - - // src/Acme/DemoBundle/Form/Extension/ImageTypeExtension.php - namespace Acme\DemoBundle\Form\Extension; - - use Symfony\Component\Form\AbstractTypeExtension; - use Symfony\Component\Form\FormView; - use Symfony\Component\Form\FormInterface; - use Symfony\Component\PropertyAccess\PropertyAccess; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - class ImageTypeExtension extends AbstractTypeExtension - { - /** - * Returns the name of the type being extended. - * - * @return string The name of the type being extended - */ - public function getExtendedType() - { - return 'file'; - } - - /** - * Add the image_path option - * - * @param OptionsResolverInterface $resolver - */ - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setOptional(array('image_path')); - } - - /** - * Pass the image url to the view - * - * @param FormView $view - * @param FormInterface $form - * @param array $options - */ - public function buildView(FormView $view, FormInterface $form, array $options) - { - if (array_key_exists('image_path', $options)) { - $parentData = $form->getParent()->getData(); - - if (null !== $parentData) { - $accessor = PropertyAccess::getPropertyAccessor(); - $imageUrl = $accessor->getValue($parentData, $options['image_path']); - } else { - $imageUrl = null; - } - - // set an "image_url" variable that will be available when rendering this field - $view->set('image_url', $imageUrl); - } - } - - } - -Override the File Widget Template Fragment ------------------------------------------- - -Each field type is rendered by a template fragment. Those template fragments -can be overridden in order to customize form rendering. For more information, -you can refer to the :ref:`cookbook-form-customization-form-themes` article. - -In your extension class, you have added a new variable (``image_url``), but -you still need to take advantage of this new variable in your templates. -Specifically, you need to override the ``file_widget`` block: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #} - {% extends 'form_div_layout.html.twig' %} - - {% block file_widget %} - {% spaceless %} - - {{ block('form_widget') }} - {% if image_url is not null %} - - {% endif %} - - {% endspaceless %} - {% endblock %} - - .. code-block:: html+php - - - widget($form) ?> - - - - -.. note:: - - You will need to change your config file or explicitly specify how - you want your form to be themed in order for Symfony to use your overridden - block. See :ref:`cookbook-form-customization-form-themes` for more - information. - -Using the Form Type Extension ------------------------------- - -From now on, when adding a field of type ``file`` in your form, you can -specify an ``image_path`` option that will be used to display an image -next to the file field. For example:: - - // src/Acme/DemoBundle/Form/Type/MediaType.php - namespace Acme\DemoBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - - class MediaType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('name', 'text') - ->add('file', 'file', array('image_path' => 'webPath')); - } - - public function getName() - { - return 'media'; - } - } - -When displaying the form, if the underlying model has already been associated -with an image, you will see it displayed next to the file input. diff --git a/cookbook/form/data_transformers.rst b/cookbook/form/data_transformers.rst deleted file mode 100644 index c3a8d43dcc4..00000000000 --- a/cookbook/form/data_transformers.rst +++ /dev/null @@ -1,353 +0,0 @@ -.. index:: - single: Form; Data transformers - -How to use Data Transformers -============================ - -You'll often find the need to transform the data the user entered in a form into -something else for use in your program. You could easily do this manually in your -controller, but what if you want to use this specific form in different places? - -Say you have a one-to-one relation of Task to Issue, e.g. a Task optionally has an -issue linked to it. Adding a listbox with all possible issues can eventually lead to -a really long listbox in which it is impossible to find something. You might -want to add a textbox instead, where the user can simply enter the issue number. - -You could try to do this in your controller, but it's not the best solution. -It would be better if this issue were automatically converted to an Issue object. -This is where Data Transformers come into play. - -Creating the Transformer ------------------------- - -First, create an `IssueToNumberTransformer` class - this class will be responsible -for converting to and from the issue number and the Issue object:: - - // src/Acme/TaskBundle/Form/DataTransformer/IssueToNumberTransformer.php - namespace Acme\TaskBundle\Form\DataTransformer; - - use Symfony\Component\Form\DataTransformerInterface; - use Symfony\Component\Form\Exception\TransformationFailedException; - use Doctrine\Common\Persistence\ObjectManager; - use Acme\TaskBundle\Entity\Issue; - - class IssueToNumberTransformer implements DataTransformerInterface - { - /** - * @var ObjectManager - */ - private $om; - - /** - * @param ObjectManager $om - */ - public function __construct(ObjectManager $om) - { - $this->om = $om; - } - - /** - * Transforms an object (issue) to a string (number). - * - * @param Issue|null $issue - * @return string - */ - public function transform($issue) - { - if (null === $issue) { - return ""; - } - - return $issue->getNumber(); - } - - /** - * Transforms a string (number) to an object (issue). - * - * @param string $number - * - * @return Issue|null - * - * @throws TransformationFailedException if object (issue) is not found. - */ - public function reverseTransform($number) - { - if (!$number) { - return null; - } - - $issue = $this->om - ->getRepository('AcmeTaskBundle:Issue') - ->findOneBy(array('number' => $number)) - ; - - if (null === $issue) { - throw new TransformationFailedException(sprintf( - 'An issue with number "%s" does not exist!', - $number - )); - } - - return $issue; - } - } - -.. tip:: - - If you want a new issue to be created when an unknown number is entered, you - can instantiate it rather than throwing the ``TransformationFailedException``. - -Using the Transformer ---------------------- - -Now that you have the transformer built, you just need to add it to your -issue field in some form. - - You can also use transformers without creating a new custom form type - by calling ``addModelTransformer`` (or ``addViewTransformer`` - see - `Model and View Transformers`_) on any field builder:: - - use Symfony\Component\Form\FormBuilderInterface; - use Acme\TaskBundle\Form\DataTransformer\IssueToNumberTransformer; - - class TaskType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - // ... - - // this assumes that the entity manager was passed in as an option - $entityManager = $options['em']; - $transformer = new IssueToNumberTransformer($entityManager); - - // add a normal text field, but add your transformer to it - $builder->add( - $builder->create('issue', 'text') - ->addModelTransformer($transformer) - ); - } - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'data_class' => 'Acme\TaskBundle\Entity\Task', - )); - - $resolver->setRequired(array( - 'em', - )); - - $resolver->setAllowedTypes(array( - 'em' => 'Doctrine\Common\Persistence\ObjectManager', - )); - - // ... - } - - // ... - } - -This example requires that you pass in the entity manager as an option -when creating your form. Later, you'll learn how you could create a custom -``issue`` field type to avoid needing to do this in your controller:: - - $taskForm = $this->createForm(new TaskType(), $task, array( - 'em' => $this->getDoctrine()->getEntityManager(), - )); - -Cool, you're done! Your user will be able to enter an issue number into the -text field and it will be transformed back into an Issue object. This means -that, after a successful submission, the Form framework will pass a real Issue -object to ``Task::setIssue()`` instead of the issue number. - -If the issue isn't found, a form error will be created for that field and -its error message can be controlled with the ``invalid_message`` field option. - -.. caution:: - - Notice that adding a transformer requires using a slightly more complicated - syntax when adding the field. The following is **wrong**, as the transformer - would be applied to the entire form, instead of just this field:: - - // THIS IS WRONG - TRANSFORMER WILL BE APPLIED TO THE ENTIRE FORM - // see above example for correct code - $builder->add('issue', 'text') - ->addModelTransformer($transformer); - -Model and View Transformers -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.1 - The names and method of the transformers were changed in Symfony 2.1. - ``prependNormTransformer`` became ``addModelTransformer`` and ``appendClientTransformer`` - became ``addViewTransformer``. - -In the above example, the transformer was used as a "model" transformer. -In fact, there are two different type of transformers and three different -types of underlying data. - -.. image:: /images/cookbook/form/DataTransformersTypes.png - :align: center - -In any form, the 3 different types of data are: - -1) **Model data** - This is the data in the format used in your application -(e.g. an ``Issue`` object). If you call ``Form::getData`` or ``Form::setData``, -you're dealing with the "model" data. - -2) **Norm Data** - This is a normalized version of your data, and is commonly -the same as your "model" data (though not in our example). It's not commonly -used directly. - -3) **View Data** - This is the format that's used to fill in the form fields -themselves. It's also the format in which the user will submit the data. When -you call ``Form::submit($data)``, the ``$data`` is in the "view" data format. - -The 2 different types of transformers help convert to and from each of these -types of data: - -**Model transformers**: - - ``transform``: "model data" => "norm data" - - ``reverseTransform``: "norm data" => "model data" - -**View transformers**: - - ``transform``: "norm data" => "view data" - - ``reverseTransform``: "view data" => "norm data" - -Which transformer you need depends on your situation. - -To use the view transformer, call ``addViewTransformer``. - -So why use the model transformer? ---------------------------------- - -In this example, the field is a ``text`` field, and a text field is always -expected to be a simple, scalar format in the "norm" and "view" formats. For -this reason, the most appropriate transformer was the "model" transformer -(which converts to/from the *norm* format - string issue number - to the *model* -format - Issue object). - -The difference between the transformers is subtle and you should always think -about what the "norm" data for a field should really be. For example, the -"norm" data for a ``text`` field is a string, but is a ``DateTime`` object -for a ``date`` field. - -Using Transformers in a custom field type ------------------------------------------ - -In the above example, you applied the transformer to a normal ``text`` field. -This was easy, but has two downsides: - -1) You need to always remember to apply the transformer whenever you're adding -a field for issue numbers - -2) You need to worry about passing in the ``em`` option whenever you're creating -a form that uses the transformer. - -Because of these, you may choose to create a :doc:`create a custom field type`. -First, create the custom field type class:: - - // src/Acme/TaskBundle/Form/Type/IssueSelectorType.php - namespace Acme\TaskBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - use Acme\TaskBundle\Form\DataTransformer\IssueToNumberTransformer; - use Doctrine\Common\Persistence\ObjectManager; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - class IssueSelectorType extends AbstractType - { - /** - * @var ObjectManager - */ - private $om; - - /** - * @param ObjectManager $om - */ - public function __construct(ObjectManager $om) - { - $this->om = $om; - } - - public function buildForm(FormBuilderInterface $builder, array $options) - { - $transformer = new IssueToNumberTransformer($this->om); - $builder->addModelTransformer($transformer); - } - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'invalid_message' => 'The selected issue does not exist', - )); - } - - public function getParent() - { - return 'text'; - } - - public function getName() - { - return 'issue_selector'; - } - } - -Next, register your type as a service and tag it with ``form.type`` so that -it's recognized as a custom field type: - -.. configuration-block:: - - .. code-block:: yaml - - services: - acme_demo.type.issue_selector: - class: Acme\TaskBundle\Form\Type\IssueSelectorType - arguments: ["@doctrine.orm.entity_manager"] - tags: - - { name: form.type, alias: issue_selector } - - .. code-block:: xml - - - - - - - .. code-block:: php - - $container - ->setDefinition('acme_demo.type.issue_selector', array( - new Reference('doctrine.orm.entity_manager'), - )) - ->addTag('form.type', array( - 'alias' => 'issue_selector', - )) - ; - -Now, whenever you need to use your special ``issue_selector`` field type, -it's quite easy:: - - // src/Acme/TaskBundle/Form/Type/TaskType.php - namespace Acme\TaskBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - - class TaskType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('task') - ->add('dueDate', null, array('widget' => 'single_text')); - ->add('issue', 'issue_selector'); - } - - public function getName() - { - return 'task'; - } - } diff --git a/cookbook/form/direct_submit.rst b/cookbook/form/direct_submit.rst deleted file mode 100644 index 2d68aeae774..00000000000 --- a/cookbook/form/direct_submit.rst +++ /dev/null @@ -1,120 +0,0 @@ -.. index:: - single: Form; Form::submit() - -How to use the submit() Function to handle Form Submissions -=========================================================== - -.. versionadded:: - Before Symfony 2.3, the ``submit`` method was known as ``bind``. - -In Symfony 2.3, a new :method:`Symfony\Component\Form\FormInterface::handleRequest` -method was added, which makes handling form submissions easier than ever:: - - use Symfony\Component\HttpFoundation\Request; - // ... - - public function newAction(Request $request) - { - $form = $this->createFormBuilder() - // ... - ->getForm(); - - $form->handleRequest($request); - - if ($form->isValid()) { - // perform some action... - - return $this->redirect($this->generateUrl('task_success')); - } - - return $this->render('AcmeTaskBundle:Default:new.html.twig', array( - 'form' => $form->createView(), - )); - } - -.. tip:: - - To see more about this method, read :ref:`book-form-handling-form-submissions`. - -Calling Form::submit() manually -------------------------------- - -In some cases, you want better control over when exactly your form is submitted -and what data is passed to it. Instead of using the -:method:`Symfony\Component\Form\FormInterface::handleRequest` -method, pass the submitted data directly to -:method:`Symfony\Component\Form\FormInterface::submit`:: - - use Symfony\Component\HttpFoundation\Request; - // ... - - public function newAction(Request $request) - { - $form = $this->createFormBuilder() - // ... - ->getForm(); - - if ($request->isMethod('POST')) { - $form->submit($request->request->get($form->getName())); - - if ($form->isValid()) { - // perform some action... - - return $this->redirect($this->generateUrl('task_success')); - } - } - - return $this->render('AcmeTaskBundle:Default:new.html.twig', array( - 'form' => $form->createView(), - )); - } - -.. tip:: - - Forms consisting of nested fields expect an array in - :method:`Symfony\Component\Form\FormInterface::submit`. You can also submit - individual fields by calling :method:`Symfony\Component\Form\FormInterface::submit` - directly on the field:: - - $form->get('firstName')->submit('Fabien'); - -.. _cookbook-form-submit-request: - -Passing a Request to Form::submit() (deprecated) ------------------------------------------------- - -.. versionadded:: - Before Symfony 2.3, the ``submit`` method was known as ``bind``. - -Before Symfony 2.3, the :method:`Symfony\Component\Form\FormInterface::submit` -method accepted a :class:`Symfony\\Component\\HttpFoundation\\Request` object as -a convenient shortcut to the previous example:: - - use Symfony\Component\HttpFoundation\Request; - // ... - - public function newAction(Request $request) - { - $form = $this->createFormBuilder() - // ... - ->getForm(); - - if ($request->isMethod('POST')) { - $form->submit($request); - - if ($form->isValid()) { - // perform some action... - - return $this->redirect($this->generateUrl('task_success')); - } - } - - return $this->render('AcmeTaskBundle:Default:new.html.twig', array( - 'form' => $form->createView(), - )); - } - -Passing the :class:`Symfony\\Component\HttpFoundation\\Request` directly to -:method:`Symfony\\Component\\Form\\FormInterface::submit`` still works, but is -deprecated and will be removed in Symfony 3.0. You should use the method -:method:`Symfony\Component\Form\FormInterface::handleRequest` instead. diff --git a/cookbook/form/dynamic_form_modification.rst b/cookbook/form/dynamic_form_modification.rst deleted file mode 100644 index d74ef800590..00000000000 --- a/cookbook/form/dynamic_form_modification.rst +++ /dev/null @@ -1,625 +0,0 @@ -.. index:: - single: Form; Events - -How to Dynamically Modify Forms Using Form Events -================================================= - -Often times, a form can't be created statically. In this entry, you'll learn -how to customize your form based on three common use-cases: - -1) :ref:`cookbook-form-events-underlying-data` - -Example: you have a "Product" form and need to modify/add/remove a field -based on the data on the underlying Product being edited. - -2) :ref:`cookbook-form-events-user-data` - -Example: you create a "Friend Message" form and need to build a drop-down -that contains only users that are friends with the *current* authenticated -user. - -3) :ref:`cookbook-form-events-submitted-data` - -Example: on a registration form, you have a "country" field and a "state" -field which should populate dynamically based on the value in the "country" -field. - -.. _cookbook-form-events-underlying-data: - -Customizing your Form based on the underlying Data --------------------------------------------------- - -Before jumping right into dynamic form generation, let's have a quick review -of what a bare form class looks like:: - - // src/Acme/DemoBundle/Form/Type/ProductType.php - namespace Acme\DemoBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - class ProductType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('name'); - $builder->add('price'); - } - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'data_class' => 'Acme\DemoBundle\Entity\Product' - )); - } - - public function getName() - { - return 'product'; - } - } - -.. note:: - - If this particular section of code isn't already familiar to you, you - probably need to take a step back and first review the :doc:`Forms chapter ` - before proceeding. - -Assume for a moment that this form utilizes an imaginary "Product" class -that has only two properties ("name" and "price"). The form generated from -this class will look the exact same regardless if a new Product is being created -or if an existing product is being edited (e.g. a product fetched from the database). - -Suppose now, that you don't want the user to be able to change the ``name`` value -once the object has been created. To do this, you can rely on Symfony's -:doc:`Event Dispatcher ` -system to analyze the data on the object and modify the form based on the -Product object's data. In this entry, you'll learn how to add this level of -flexibility to your forms. - -.. _`cookbook-forms-event-subscriber`: - -Adding An Event Subscriber To A Form Class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -So, instead of directly adding that "name" widget via your ProductType form -class, let's delegate the responsibility of creating that particular field -to an Event Subscriber:: - - // src/Acme/DemoBundle/Form/Type/ProductType.php - namespace Acme\DemoBundle\Form\Type; - - // ... - use Acme\DemoBundle\Form\EventListener\AddNameFieldSubscriber; - - class ProductType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('price'); - - $builder->addEventSubscriber(new AddNameFieldSubscriber()); - } - - // ... - } - -.. _`cookbook-forms-inside-subscriber-class`: - -Inside the Event Subscriber Class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The goal is to create a "name" field *only* if the underlying Product object -is new (e.g. hasn't been persisted to the database). Based on that, the subscriber -might look like the following: - -.. versionadded:: 2.2 - The ability to pass a string into :method:`FormInterface::add` - was added in Symfony 2.2. - -.. code-block:: php - - // src/Acme/DemoBundle/Form/EventListener/AddNameFieldSubscriber.php - namespace Acme\DemoBundle\Form\EventListener; - - use Symfony\Component\Form\FormEvent; - use Symfony\Component\Form\FormEvents; - use Symfony\Component\EventDispatcher\EventSubscriberInterface; - - class AddNameFieldSubscriber implements EventSubscriberInterface - { - public static function getSubscribedEvents() - { - // Tells the dispatcher that you want to listen on the form.pre_set_data - // event and that the preSetData method should be called. - return array(FormEvents::PRE_SET_DATA => 'preSetData'); - } - - public function preSetData(FormEvent $event) - { - $data = $event->getData(); - $form = $event->getForm(); - - // check if the product object is "new" - // If you didn't pass any data to the form, the data is "null". - // This should be considered a new "Product" - if (!$data || !$data->getId()) { - $form->add('name', 'text'); - } - } - } - -.. tip:: - - The ``FormEvents::PRE_SET_DATA`` line actually resolves to the string - ``form.pre_set_data``. :class:`Symfony\\Component\\Form\\FormEvents` serves - an organizational purpose. It is a centralized location in which you can - find all of the various form events available. - -.. note:: - - You can view the full list of form events via the :class:`Symfony\\Component\\Form\\FormEvents` - class. - -.. _cookbook-form-events-user-data: - -How to Dynamically Generate Forms based on user Data ----------------------------------------------------- - -Sometimes you want a form to be generated dynamically based not only on data -from the form but also on something else - like some data from the current user. -Suppose you have a social website where a user can only message people who -are his friends on the website. In this case, a "choice list" of whom to message -should only contain users that are the current user's friends. - -Creating the Form Type -~~~~~~~~~~~~~~~~~~~~~~ - -Using an event listener, your form might look like this:: - - // src/Acme/DemoBundle/Form/Type/FriendMessageFormType.php - namespace Acme\DemoBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - use Symfony\Component\Form\FormEvents; - use Symfony\Component\Form\FormEvent; - use Symfony\Component\Security\Core\SecurityContext; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - class FriendMessageFormType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('subject', 'text') - ->add('body', 'textarea') - ; - $builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event){ - // ... add a choice list of friends of the current application user - }); - } - - public function getName() - { - return 'acme_friend_message'; - } - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - } - } - -The problem is now to get the current user and create a choice field that -contains only this user's friends. - -Luckily it is pretty easy to inject a service inside of the form. This can be -done in the constructor:: - - private $securityContext; - - public function __construct(SecurityContext $securityContext) - { - $this->securityContext = $securityContext; - } - -.. note:: - - You might wonder, now that you have access to the User (through the security - context), why not just use it directly in ``buildForm`` and omit the - event listener? This is because doing so in the ``buildForm`` method - would result in the whole form type being modified and not just this - one form instance. This may not usually be a problem, but technically - a single form type could be used on a single request to create many forms - or fields. - -Customizing the Form Type -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Now that you have all the basics in place you an take advantage of the ``securityContext`` -and fill in the listener logic:: - - // src/Acme/DemoBundle/FormType/FriendMessageFormType.php - - use Symfony\Component\Security\Core\SecurityContext; - use Doctrine\ORM\EntityRepository; - // ... - - class FriendMessageFormType extends AbstractType - { - private $securityContext; - - public function __construct(SecurityContext $securityContext) - { - $this->securityContext = $securityContext; - } - - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('subject', 'text') - ->add('body', 'textarea') - ; - - // grab the user, do a quick sanity check that one exists - $user = $this->securityContext->getToken()->getUser(); - if (!$user) { - throw new \LogicException( - 'The FriendMessageFormType cannot be used without an authenticated user!' - ); - } - - $factory = $builder->getFormFactory(); - - $builder->addEventListener( - FormEvents::PRE_SET_DATA, - function(FormEvent $event) use($user, $factory){ - $form = $event->getForm(); - - $formOptions = array( - 'class' => 'Acme\DemoBundle\Entity\User', - 'multiple' => false, - 'expanded' => false, - 'property' => 'fullName', - 'query_builder' => function(EntityRepository $er) use ($user) { - // build a custom query, or call a method on your repository (even better!) - }, - ); - - // create the field, this is similar the $builder->add() - // field name, field type, data, options - $form->add($factory->createNamed('friend', 'entity', null, $formOptions)); - } - ); - } - - // ... - } - -Using the Form -~~~~~~~~~~~~~~ - -Our form is now ready to use and there are two possible ways to use it inside -of a controller: - -a) create it manually and remember to pass the security context to it; - -or - -b) define it as a service. - -a) Creating the Form manually -............................. - -This is very simple, and is probably the better approach unless you're using -your new form type in many places or embedding it into other forms:: - - class FriendMessageController extends Controller - { - public function newAction(Request $request) - { - $securityContext = $this->container->get('security.context'); - $form = $this->createForm( - new FriendMessageFormType($securityContext) - ); - - // ... - } - } - -b) Defining the Form as a Service -................................. - -To define your form as a service, just create a normal service and then tag -it with :ref:`dic-tags-form-type`. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - services: - acme.form.friend_message: - class: Acme\DemoBundle\Form\Type\FriendMessageFormType - arguments: [@security.context] - tags: - - - name: form.type - alias: acme_friend_message - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $definition = new Definition('Acme\DemoBundle\Form\Type\FriendMessageFormType'); - $definition->addTag('form.type', array('alias' => 'acme_friend_message')); - $container->setDefinition( - 'acme.form.friend_message', - $definition, - array('security.context') - ); - -If you wish to create it from within a controller or any other service that has -access to the form factory, you then use:: - - class FriendMessageController extends Controller - { - public function newAction(Request $request) - { - $form = $this->createForm('acme_friend_message'); - - // ... - } - } - -You can also easily embed the form type into another form:: - - // inside some other "form type" class - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('message', 'acme_friend_message'); - } - -.. _cookbook-form-events-submitted-data: - -Dynamic generation for submitted Forms --------------------------------------- - -Another case that can appear is that you want to customize the form specific to -the data that was submitted by the user. For example, imagine you have a registration -form for sports gatherings. Some events will allow you to specify your preferred -position on the field. This would be a ``choice`` field for example. However the -possible choices will depend on each sport. Football will have attack, defense, -goalkeeper etc... Baseball will have a pitcher but will not have goalkeeper. You -will need the correct options to be set in order for validation to pass. - -The meetup is passed as an entity hidden field to the form. So we can access each -sport like this:: - - // src/Acme/DemoBundle/Form/Type/SportMeetupType.php - class SportMeetupType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('number_of_people', 'text') - ->add('discount_coupon', 'text') - ; - $factory = $builder->getFormFactory(); - - $builder->addEventListener( - FormEvents::PRE_SET_DATA, - function(FormEvent $event) use($user, $factory){ - $form = $event->getForm(); - - // this would be your entity, i.e. SportMeetup - $data = $event->getData(); - - $positions = $data->getSport()->getAvailablePositions(); - - // ... proceed with customizing the form based on available positions - } - ); - } - } - -When you're building this form to display to the user for the first time, -then this example works perfectly. - -However, things get more difficult when you handle the form submission. This -is be cause the ``PRE_SET_DATA`` event tells us the data that you're starting -with (e.g. an empty ``SportMeetup`` object), *not* the submitted data. - -On a form, we can usually listen to the following events: - -* ``PRE_SET_DATA`` -* ``POST_SET_DATA`` -* ``PRE_SUBMIT`` -* ``SUBMIT`` -* ``POST_SUBMIT`` - -.. versionadded:: 2.3 - The events ``PRE_SUBMIT``, ``SUBMIT`` and ``POST_SUBMIT`` were added in - Symfony 2.3. Before, they were named ``PRE_BIND``, ``BIND`` and ``POST_BIND``. - -When listening to ``SUBMIT`` and ``POST_SUBMIT``, it's already "too late" to make -changes to the form. Fortunately, ``PRE_SUBMIT`` is perfect for this. There -is, however, a big difference in what ``$event->getData()`` returns for each -of these events. Specifically, in ``PRE_SUBMIT``, ``$event->getData()`` returns -the raw data submitted by the user. - -This can be used to get the ``SportMeetup`` id and retrieve it from the database, -given you have a reference to the object manager (if using doctrine). In -the end, you have an event subscriber that listens to two different events, -requires some external services and customizes the form. In such a situation, -it's probably better to define this as a service rather than using an anonymouse -function as the event listener callback. - -The subscriber would now look like:: - - // src/Acme/DemoBundle/Form/EventListener/RegistrationSportListener.php - namespace Acme\DemoBundle\Form\EventListener; - - use Symfony\Component\Form\FormFactoryInterface; - use Doctrine\ORM\EntityManager; - use Symfony\Component\Form\FormEvent; - - class RegistrationSportListener implements EventSubscriberInterface - { - /** - * @var FormFactoryInterface - */ - private $factory; - - /** - * @var EntityManager - */ - private $om; - - /** - * @param factory FormFactoryInterface - */ - public function __construct(FormFactoryInterface $factory, EntityManager $om) - { - $this->factory = $factory; - $this->om = $om; - } - - public static function getSubscribedEvents() - { - return array( - FormEvents::PRE_SUBMIT => 'preSubmit', - FormEvents::PRE_SET_DATA => 'preSetData', - ); - } - - /** - * @param event FormEvent - */ - public function preSetData(FormEvent $event) - { - $meetup = $event->getData()->getMeetup(); - - // Before SUBMITing the form, the "meetup" will be null - if (null === $meetup) { - return; - } - - $form = $event->getForm(); - $positions = $meetup->getSport()->getPositions(); - - $this->customizeForm($form, $positions); - } - - public function preSubmit(FormEvent $event) - { - $data = $event->getData(); - $id = $data['event']; - $meetup = $this->om - ->getRepository('AcmeDemoBundle:SportMeetup') - ->find($id); - - if ($meetup === null) { - $msg = 'The event %s could not be found for you registration'; - throw new \Exception(sprintf($msg, $id)); - } - $form = $event->getForm(); - $positions = $meetup->getSport()->getPositions(); - - $this->customizeForm($form, $positions); - } - - protected function customizeForm($form, $positions) - { - // ... customize the form according to the positions - } - } - -You can see that you need to listen on these two events and have different callbacks -only because in two different scenarios, the data that you can use is given in a -different format. Other than that, this class always performs exactly the same -things on a given form. - -Now that you have that setup, register your form and the listener as services: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - acme.form.sport_meetup: - class: Acme\SportBundle\Form\Type\SportMeetupType - arguments: [@acme.form.meetup_registration_listener] - tags: - - { name: form.type, alias: acme_meetup_registration } - acme.form.meetup_registration_listener - class: Acme\SportBundle\Form\EventListener\RegistrationSportListener - arguments: [@form.factory, @doctrine] - - .. code-block:: xml - - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $definition = new Definition('Acme\SportBundle\Form\Type\SportMeetupType'); - $definition->addTag('form.type', array('alias' => 'acme_meetup_registration')); - $container->setDefinition( - 'acme.form.meetup_registration_listener', - $definition, - array('security.context') - ); - $definition = new Definition('Acme\SportBundle\Form\EventListener\RegistrationSportListener'); - $container->setDefinition( - 'acme.form.meetup_registration_listener', - $definition, - array('form.factory', 'doctrine') - ); - -In this setup, the ``RegistrationSportListener`` will be a constructor argument -to ``SportMeetupType``. You can then register it as an event subscriber on -your form:: - - private $registrationSportListener; - - public function __construct(RegistrationSportListener $registrationSportListener) - { - $this->registrationSportListener = $registrationSportListener; - } - - public function buildForm(FormBuilderInterface $builder, array $options) - { - // ... - $builder->addEventSubscriber($this->registrationSportListener); - } - -And this should tie everything together. You can now retrieve your form from the -controller, display it to a user, and validate it with the right choice options -set for every possible kind of sport that our users are registering for. - -One piece that may still be missing is the client-side updating of your form -after the sport is selected. This should be handled by making an AJAX call -back to your application. In that controller, you can submit your form, but -instead of processing it, simply use the submitted form to render the updated -fields. The response from the AJAX call can then be used to update the view. diff --git a/cookbook/form/form_collections.rst b/cookbook/form/form_collections.rst deleted file mode 100755 index e84136970ec..00000000000 --- a/cookbook/form/form_collections.rst +++ /dev/null @@ -1,687 +0,0 @@ -.. index:: - single: Form; Embed collection of forms - -How to Embed a Collection of Forms -================================== - -In this entry, you'll learn how to create a form that embeds a collection -of many other forms. This could be useful, for example, if you had a ``Task`` -class and you wanted to edit/create/remove many ``Tag`` objects related to -that Task, right inside the same form. - -.. note:: - - In this entry, it's loosely assumed that you're using Doctrine as your - database store. But if you're not using Doctrine (e.g. Propel or just - a database connection), it's all very similar. There are only a few parts - of this tutorial that really care about "persistence". - - If you *are* using Doctrine, you'll need to add the Doctrine metadata, - including the ``ManyToMany`` association mapping definition on the Task's - ``tags`` property. - -Let's start there: suppose that each ``Task`` belongs to multiple ``Tags`` -objects. Start by creating a simple ``Task`` class:: - - // src/Acme/TaskBundle/Entity/Task.php - namespace Acme\TaskBundle\Entity; - - use Doctrine\Common\Collections\ArrayCollection; - - class Task - { - protected $description; - - protected $tags; - - public function __construct() - { - $this->tags = new ArrayCollection(); - } - - public function getDescription() - { - return $this->description; - } - - public function setDescription($description) - { - $this->description = $description; - } - - public function getTags() - { - return $this->tags; - } - - public function setTags(ArrayCollection $tags) - { - $this->tags = $tags; - } - } - -.. note:: - - The ``ArrayCollection`` is specific to Doctrine and is basically the - same as using an ``array`` (but it must be an ``ArrayCollection`` if - you're using Doctrine). - -Now, create a ``Tag`` class. As you saw above, a ``Task`` can have many ``Tag`` -objects:: - - // src/Acme/TaskBundle/Entity/Tag.php - namespace Acme\TaskBundle\Entity; - - class Tag - { - public $name; - } - -.. tip:: - - The ``name`` property is public here, but it can just as easily be protected - or private (but then it would need ``getName`` and ``setName`` methods). - -Now let's get to the forms. Create a form class so that a ``Tag`` object -can be modified by the user:: - - // src/Acme/TaskBundle/Form/Type/TagType.php - namespace Acme\TaskBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - class TagType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('name'); - } - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'data_class' => 'Acme\TaskBundle\Entity\Tag', - )); - } - - public function getName() - { - return 'tag'; - } - } - -With this, you have enough to render a tag form by itself. But since the end -goal is to allow the tags of a ``Task`` to be modified right inside the task -form itself, create a form for the ``Task`` class. - -Notice that you embed a collection of ``TagType`` forms using the -:doc:`collection` field type:: - - // src/Acme/TaskBundle/Form/Type/TaskType.php - namespace Acme\TaskBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - class TaskType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('description'); - - $builder->add('tags', 'collection', array('type' => new TagType())); - } - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'data_class' => 'Acme\TaskBundle\Entity\Task', - )); - } - - public function getName() - { - return 'task'; - } - } - -In your controller, you'll now initialize a new instance of ``TaskType``:: - - // src/Acme/TaskBundle/Controller/TaskController.php - namespace Acme\TaskBundle\Controller; - - use Acme\TaskBundle\Entity\Task; - use Acme\TaskBundle\Entity\Tag; - use Acme\TaskBundle\Form\Type\TaskType; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - - class TaskController extends Controller - { - public function newAction(Request $request) - { - $task = new Task(); - - // dummy code - this is here just so that the Task has some tags - // otherwise, this isn't an interesting example - $tag1 = new Tag(); - $tag1->name = 'tag1'; - $task->getTags()->add($tag1); - $tag2 = new Tag(); - $tag2->name = 'tag2'; - $task->getTags()->add($tag2); - // end dummy code - - $form = $this->createForm(new TaskType(), $task); - - $form->handleRequest($request); - - if ($form->isValid()) { - // ... maybe do some form processing, like saving the Task and Tag objects - } - - return $this->render('AcmeTaskBundle:Task:new.html.twig', array( - 'form' => $form->createView(), - )); - } - } - -The corresponding template is now able to render both the ``description`` -field for the task form as well as all the ``TagType`` forms for any tags -that are already related to this ``Task``. In the above controller, I added -some dummy code so that you can see this in action (since a ``Task`` has -zero tags when first created). - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/TaskBundle/Resources/views/Task/new.html.twig #} - - {# ... #} - - {{ form_start(form) }} - {# render the task's only field: description #} - {{ form_row(form.description) }} - -

Tags

-
    - {# iterate over each existing tag and render its only field: name #} - {% for tag in form.tags %} -
  • {{ form_row(tag.name) }}
  • - {% endfor %} -
- {{ form_end(form) }} - - {# ... #} - - .. code-block:: html+php - - - - - - start($form) ?> - - row($form['description']) ?> - -

Tags

-
    - -
  • row($tag['name']) ?>
  • - -
- end($form) ?> - - - -When the user submits the form, the submitted data for the ``Tags`` fields -are used to construct an ArrayCollection of ``Tag`` objects, which is then -set on the ``tag`` field of the ``Task`` instance. - -The ``Tags`` collection is accessible naturally via ``$task->getTags()`` -and can be persisted to the database or used however you need. - -So far, this works great, but this doesn't allow you to dynamically add new -tags or delete existing tags. So, while editing existing tags will work -great, your user can't actually add any new tags yet. - -.. caution:: - - In this entry, you embed only one collection, but you are not limited - to this. You can also embed nested collection as many level down as you - like. But if you use Xdebug in your development setup, you may receive - a ``Maximum function nesting level of '100' reached, aborting!`` error. - This is due to the ``xdebug.max_nesting_level`` PHP setting, which defaults - to ``100``. - - This directive limits recursion to 100 calls which may not be enough for - rendering the form in the template if you render the whole form at - once (e.g ``form_widget(form)``). To fix this you can set this directive - to a higher value (either via a PHP ini file or via :phpfunction:`ini_set`, - for example in ``app/autoload.php``) or render each form field by hand - using ``form_row``. - -.. _cookbook-form-collections-new-prototype: - -Allowing "new" tags with the "prototype" ------------------------------------------ - -Allowing the user to dynamically add new tags means that you'll need to -use some JavaScript. Previously you added two tags to your form in the controller. -Now let the user add as many tag forms as he needs directly in the browser. -This will be done through a bit of JavaScript. - -The first thing you need to do is to let the form collection know that it will -receive an unknown number of tags. So far you've added two tags and the form -type expects to receive exactly two, otherwise an error will be thrown: -``This form should not contain extra fields``. To make this flexible, -add the ``allow_add`` option to your collection field:: - - // src/Acme/TaskBundle/Form/Type/TaskType.php - - // ... - - use Symfony\Component\Form\FormBuilderInterface; - - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('description'); - - $builder->add('tags', 'collection', array( - 'type' => new TagType(), - 'allow_add' => true, - 'by_reference' => false, - )); - } - -Note that ``'by_reference' => false`` was also added. Normally, the form -framework would modify the tags on a `Task` object *without* actually -ever calling `setTags`. By setting :ref:`by_reference` -to `false`, `setTags` will be called. This will be important later as you'll -see. - -In addition to telling the field to accept any number of submitted objects, the -``allow_add`` also makes a "prototype" variable available to you. This "prototype" -is a little "template" that contains all the HTML to be able to render any -new "tag" forms. To render it, make the following change to your template: - -.. configuration-block:: - - .. code-block:: html+jinja - -
    - ... -
- - .. code-block:: html+php - -
    - ... -
- -.. note:: - - If you render your whole "tags" sub-form at once (e.g. ``form_row(form.tags)``), - then the prototype is automatically available on the outer ``div`` as - the ``data-prototype`` attribute, similar to what you see above. - -.. tip:: - - The ``form.tags.vars.prototype`` is a form element that looks and feels just - like the individual ``form_widget(tag)`` elements inside your ``for`` loop. - This means that you can call ``form_widget``, ``form_row`` or ``form_label`` - on it. You could even choose to render only one of its fields (e.g. the - ``name`` field): - - .. code-block:: html+jinja - - {{ form_widget(form.tags.vars.prototype.name)|e }} - -On the rendered page, the result will look something like this: - -.. code-block:: html - -
    - -The goal of this section will be to use JavaScript to read this attribute -and dynamically add new tag forms when the user clicks a "Add a tag" link. -To make things simple, this example uses jQuery and assumes you have it included -somewhere on your page. - -Add a ``script`` tag somewhere on your page so you can start writing some JavaScript. - -First, add a link to the bottom of the "tags" list via JavaScript. Second, -bind to the "click" event of that link so you can add a new tag form (``addTagForm`` -will be show next): - -.. code-block:: javascript - - // Get the ul that holds the collection of tags - var collectionHolder = $('ul.tags'); - - // setup an "add a tag" link - var $addTagLink = $('Add a tag'); - var $newLinkLi = $('
  • ').append($addTagLink); - - jQuery(document).ready(function() { - // add the "add a tag" anchor and li to the tags ul - collectionHolder.append($newLinkLi); - - // count the current form inputs we have (e.g. 2), use that as the new - // index when inserting a new item (e.g. 2) - collectionHolder.data('index', collectionHolder.find(':input').length); - - $addTagLink.on('click', function(e) { - // prevent the link from creating a "#" on the URL - e.preventDefault(); - - // add a new tag form (see next code block) - addTagForm(collectionHolder, $newLinkLi); - }); - }); - -The ``addTagForm`` function's job will be to use the ``data-prototype`` attribute -to dynamically add a new form when this link is clicked. The ``data-prototype`` -HTML contains the tag ``text`` input element with a name of ``task[tags][__name__][name]`` -and id of ``task_tags___name___name``. The ``__name__`` is a little "placeholder", -which you'll replace with a unique, incrementing number (e.g. ``task[tags][3][name]``). - -.. versionadded:: 2.1 - The placeholder was changed from ``$$name$$`` to ``__name__`` in Symfony 2.1 - -The actual code needed to make this all work can vary quite a bit, but here's -one example: - -.. code-block:: javascript - - function addTagForm(collectionHolder, $newLinkLi) { - // Get the data-prototype explained earlier - var prototype = collectionHolder.data('prototype'); - - // get the new index - var index = collectionHolder.data('index'); - - // Replace '__name__' in the prototype's HTML to - // instead be a number based on how many items we have - var newForm = prototype.replace(/__name__/g, index); - - // increase the index with one for the next item - collectionHolder.data('index', index + 1); - - // Display the form in the page in an li, before the "Add a tag" link li - var $newFormLi = $('
  • ').append(newForm); - $newLinkLi.before($newFormLi); - } - -.. note:: - - It is better to separate your javascript in real JavaScript files than - to write it inside the HTML as is done here. - -Now, each time a user clicks the ``Add a tag`` link, a new sub form will -appear on the page. When the form is submitted, any new tag forms will be converted -into new ``Tag`` objects and added to the ``tags`` property of the ``Task`` object. - -.. sidebar:: Doctrine: Cascading Relations and saving the "Inverse" side - - To get the new tags to save in Doctrine, you need to consider a couple - more things. First, unless you iterate over all of the new ``Tag`` objects - and call ``$em->persist($tag)`` on each, you'll receive an error from - Doctrine: - - A new entity was found through the relationship `Acme\TaskBundle\Entity\Task#tags` - that was not configured to cascade persist operations for entity... - - To fix this, you may choose to "cascade" the persist operation automatically - from the ``Task`` object to any related tags. To do this, add the ``cascade`` - option to your ``ManyToMany`` metadata: - - .. configuration-block:: - - .. code-block:: php-annotations - - // src/Acme/TaskBundle/Entity/Task.php - - // ... - - /** - * @ORM\ManyToMany(targetEntity="Tag", cascade={"persist"}) - */ - protected $tags; - - .. code-block:: yaml - - # src/Acme/TaskBundle/Resources/config/doctrine/Task.orm.yml - Acme\TaskBundle\Entity\Task: - type: entity - # ... - oneToMany: - tags: - targetEntity: Tag - cascade: [persist] - - .. code-block:: xml - - - - - - - - - - - - - - - A second potential issue deals with the `Owning Side and Inverse Side`_ - of Doctrine relationships. In this example, if the "owning" side of the - relationship is "Task", then persistence will work fine as the tags are - properly added to the Task. However, if the owning side is on "Tag", then - you'll need to do a little bit more work to ensure that the correct side - of the relationship is modified. - - The trick is to make sure that the single "Task" is set on each "Tag". - One easy way to do this is to add some extra logic to ``setTags()``, - which is called by the form framework since :ref:`by_reference` - is set to ``false``:: - - // src/Acme/TaskBundle/Entity/Task.php - - // ... - - public function setTags(ArrayCollection $tags) - { - foreach ($tags as $tag) { - $tag->addTask($this); - } - - $this->tags = $tags; - } - - Inside ``Tag``, just make sure you have an ``addTask`` method:: - - // src/Acme/TaskBundle/Entity/Tag.php - - // ... - - public function addTask(Task $task) - { - if (!$this->tasks->contains($task)) { - $this->tasks->add($task); - } - } - - If you have a ``OneToMany`` relationship, then the workaround is similar, - except that you can simply call ``setTask`` from inside ``setTags``. - -.. _cookbook-form-collections-remove: - -Allowing tags to be removed ----------------------------- - -The next step is to allow the deletion of a particular item in the collection. -The solution is similar to allowing tags to be added. - -Start by adding the ``allow_delete`` option in the form Type:: - - // src/Acme/TaskBundle/Form/Type/TaskType.php - - // ... - use Symfony\Component\Form\FormBuilderInterface; - - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('description'); - - $builder->add('tags', 'collection', array( - 'type' => new TagType(), - 'allow_add' => true, - 'allow_delete' => true, - 'by_reference' => false, - )); - } - -Templates Modifications -~~~~~~~~~~~~~~~~~~~~~~~ - -The ``allow_delete`` option has one consequence: if an item of a collection -isn't sent on submission, the related data is removed from the collection -on the server. The solution is thus to remove the form element from the DOM. - -First, add a "delete this tag" link to each tag form: - -.. code-block:: javascript - - jQuery(document).ready(function() { - // add a delete link to all of the existing tag form li elements - collectionHolder.find('li').each(function() { - addTagFormDeleteLink($(this)); - }); - - // ... the rest of the block from above - }); - - function addTagForm() { - // ... - - // add a delete link to the new form - addTagFormDeleteLink($newFormLi); - } - -The ``addTagFormDeleteLink`` function will look something like this: - -.. code-block:: javascript - - function addTagFormDeleteLink($tagFormLi) { - var $removeFormA = $('delete this tag'); - $tagFormLi.append($removeFormA); - - $removeFormA.on('click', function(e) { - // prevent the link from creating a "#" on the URL - e.preventDefault(); - - // remove the li for the tag form - $tagFormLi.remove(); - }); - } - -When a tag form is removed from the DOM and submitted, the removed ``Tag`` object -will not be included in the collection passed to ``setTags``. Depending on -your persistence layer, this may or may not be enough to actually remove -the relationship between the removed ``Tag`` and ``Task`` object. - -.. sidebar:: Doctrine: Ensuring the database persistence - - When removing objects in this way, you may need to do a little bit more - work to ensure that the relationship between the Task and the removed Tag - is properly removed. - - In Doctrine, you have two side of the relationship: the owning side and the - inverse side. Normally in this case you'll have a ManyToMany relation - and the deleted tags will disappear and persist correctly (adding new - tags also works effortlessly). - - But if you have an ``OneToMany`` relation or a ``ManyToMany`` with a - ``mappedBy`` on the Task entity (meaning Task is the "inverse" side), - you'll need to do more work for the removed tags to persist correctly. - - In this case, you can modify the controller to remove the relationship - on the removed tag. This assumes that you have some ``editAction`` which - is handling the "update" of your Task:: - - // src/Acme/TaskBundle/Controller/TaskController.php - - // ... - - public function editAction($id, Request $request) - { - $em = $this->getDoctrine()->getManager(); - $task = $em->getRepository('AcmeTaskBundle:Task')->find($id); - - if (!$task) { - throw $this->createNotFoundException('No task found for is '.$id); - } - - $originalTags = array(); - - // Create an array of the current Tag objects in the database - foreach ($task->getTags() as $tag) { - $originalTags[] = $tag; - } - - $editForm = $this->createForm(new TaskType(), $task); - - $editForm->handleRequest($request); - - if ($editForm->isValid()) { - - // filter $originalTags to contain tags no longer present - foreach ($task->getTags() as $tag) { - foreach ($originalTags as $key => $toDel) { - if ($toDel->getId() === $tag->getId()) { - unset($originalTags[$key]); - } - } - } - - // remove the relationship between the tag and the Task - foreach ($originalTags as $tag) { - // remove the Task from the Tag - $tag->getTasks()->removeElement($task); - - // if it were a ManyToOne relationship, remove the relationship like this - // $tag->setTask(null); - - $em->persist($tag); - - // if you wanted to delete the Tag entirely, you can also do that - // $em->remove($tag); - } - - $em->persist($task); - $em->flush(); - - // redirect back to some edit page - return $this->redirect($this->generateUrl('task_edit', array('id' => $id))); - } - - // render some form template - } - - As you can see, adding and removing the elements correctly can be tricky. - Unless you have a ManyToMany relationship where Task is the "owning" side, - you'll need to do extra work to make sure that the relationship is properly - updated (whether you're adding new tags or removing existing tags) on - each Tag object itself. - - -.. _`Owning Side and Inverse Side`: http://docs.doctrine-project.org/en/latest/reference/unitofwork-associations.html diff --git a/cookbook/form/form_customization.rst b/cookbook/form/form_customization.rst deleted file mode 100644 index eb41d37ec1f..00000000000 --- a/cookbook/form/form_customization.rst +++ /dev/null @@ -1,953 +0,0 @@ -.. index:: - single: Form; Custom form rendering - -How to customize Form Rendering -=============================== - -Symfony gives you a wide variety of ways to customize how a form is rendered. -In this guide, you'll learn how to customize every possible part of your -form with as little effort as possible whether you use Twig or PHP as your -templating engine. - -Form Rendering Basics ---------------------- - -Recall that the label, error and HTML widget of a form field can easily -be rendered by using the ``form_row`` Twig function or the ``row`` PHP helper -method: - -.. configuration-block:: - - .. code-block:: jinja - - {{ form_row(form.age) }} - - .. code-block:: php - - row($form['age']) }} ?> - -You can also render each of the three parts of the field individually: - -.. configuration-block:: - - .. code-block:: html+jinja - -
    - {{ form_label(form.age) }} - {{ form_errors(form.age) }} - {{ form_widget(form.age) }} -
    - - .. code-block:: php - -
    - label($form['age']) }} ?> - errors($form['age']) }} ?> - widget($form['age']) }} ?> -
    - -In both cases, the form label, errors and HTML widget are rendered by using -a set of markup that ships standard with Symfony. For example, both of the -above templates would render: - -.. code-block:: html - -
    - -
      -
    • This field is required
    • -
    - -
    - -To quickly prototype and test a form, you can render the entire form with -just one line: - -.. configuration-block:: - - .. code-block:: jinja - - {{ form_widget(form) }} - - .. code-block:: php - - widget($form) }} ?> - -The remainder of this recipe will explain how every part of the form's markup -can be modified at several different levels. For more information about form -rendering in general, see :ref:`form-rendering-template`. - -.. _cookbook-form-customization-form-themes: - -What are Form Themes? ---------------------- - -Symfony uses form fragments - a small piece of a template that renders just -one part of a form - to render each part of a form - field labels, errors, -``input`` text fields, ``select`` tags, etc. - -The fragments are defined as blocks in Twig and as template files in PHP. - -A *theme* is nothing more than a set of fragments that you want to use when -rendering a form. In other words, if you want to customize one portion of -how a form is rendered, you'll import a *theme* which contains a customization -of the appropriate form fragments. - -Symfony comes with a default theme (`form_div_layout.html.twig`_ in Twig and -``FrameworkBundle:Form`` in PHP) that defines each and every fragment needed -to render every part of a form. - -In the next section you will learn how to customize a theme by overriding -some or all of its fragments. - -For example, when the widget of an ``integer`` type field is rendered, an ``input`` -``number`` field is generated - -.. configuration-block:: - - .. code-block:: html+jinja - - {{ form_widget(form.age) }} - - .. code-block:: php - - widget($form['age']) ?> - -renders: - -.. code-block:: html - - - -Internally, Symfony uses the ``integer_widget`` fragment to render the field. -This is because the field type is ``integer`` and you're rendering its ``widget`` -(as opposed to its ``label`` or ``errors``). - -In Twig that would default to the block ``integer_widget`` from the `form_div_layout.html.twig`_ -template. - -In PHP it would rather be the ``integer_widget.html.php`` file located in -the ``FrameworkBundle/Resources/views/Form`` folder. - -The default implementation of the ``integer_widget`` fragment looks like this: - -.. configuration-block:: - - .. code-block:: jinja - - {# form_div_layout.html.twig #} - {% block integer_widget %} - {% set type = type|default('number') %} - {{ block('form_widget_simple') }} - {% endblock integer_widget %} - - .. code-block:: html+php - - - block($form, 'form_widget_simple', array('type' => isset($type) ? $type : "number")) ?> - -As you can see, this fragment itself renders another fragment - ``form_widget_simple``: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# form_div_layout.html.twig #} - {% block form_widget_simple %} - {% set type = type|default('text') %} - - {% endblock form_widget_simple %} - - .. code-block:: html+php - - - value="escape($value) ?>" - block($form, 'widget_attributes') ?> - /> - -The point is, the fragments dictate the HTML output of each part of a form. To -customize the form output, you just need to identify and override the correct -fragment. A set of these form fragment customizations is known as a form "theme". -When rendering a form, you can choose which form theme(s) you want to apply. - -In Twig a theme is a single template file and the fragments are the blocks defined -in this file. - -In PHP a theme is a folder and the fragments are individual template files in -this folder. - -.. _cookbook-form-customization-sidebar: - -.. sidebar:: Knowing which block to customize - - In this example, the customized fragment name is ``integer_widget`` because - you want to override the HTML ``widget`` for all ``integer`` field types. If - you need to customize textarea fields, you would customize ``textarea_widget``. - - As you can see, the fragment name is a combination of the field type and - which part of the field is being rendered (e.g. ``widget``, ``label``, - ``errors``, ``row``). As such, to customize how errors are rendered for - just input ``text`` fields, you should customize the ``text_errors`` fragment. - - More commonly, however, you'll want to customize how errors are displayed - across *all* fields. You can do this by customizing the ``form_errors`` - fragment. This takes advantage of field type inheritance. Specifically, - since the ``text`` type extends from the ``form`` type, the form component - will first look for the type-specific fragment (e.g. ``text_errors``) before - falling back to its parent fragment name if it doesn't exist (e.g. ``form_errors``). - - For more information on this topic, see :ref:`form-template-blocks`. - -.. _cookbook-form-theming-methods: - -Form Theming ------------- - -To see the power of form theming, suppose you want to wrap every input ``number`` -field with a ``div`` tag. The key to doing this is to customize the -``integer_widget`` fragment. - -Form Theming in Twig --------------------- - -When customizing the form field block in Twig, you have two options on *where* -the customized form block can live: - -+--------------------------------------+-----------------------------------+-------------------------------------------+ -| Method | Pros | Cons | -+======================================+===================================+===========================================+ -| Inside the same template as the form | Quick and easy | Can't be reused in other templates | -+--------------------------------------+-----------------------------------+-------------------------------------------+ -| Inside a separate template | Can be reused by many templates | Requires an extra template to be created | -+--------------------------------------+-----------------------------------+-------------------------------------------+ - -Both methods have the same effect but are better in different situations. - -.. _cookbook-form-twig-theming-self: - -Method 1: Inside the same Template as the Form -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The easiest way to customize the ``integer_widget`` block is to customize it -directly in the template that's actually rendering the form. - -.. code-block:: html+jinja - - {% extends '::base.html.twig' %} - - {% form_theme form _self %} - - {% block integer_widget %} -
    - {% set type = type|default('number') %} - {{ block('form_widget_simple') }} -
    - {% endblock %} - - {% block content %} - {# ... render the form #} - - {{ form_row(form.age) }} - {% endblock %} - -By using the special ``{% form_theme form _self %}`` tag, Twig looks inside -the same template for any overridden form blocks. Assuming the ``form.age`` -field is an ``integer`` type field, when its widget is rendered, the customized -``integer_widget`` block will be used. - -The disadvantage of this method is that the customized form block can't be -reused when rendering other forms in other templates. In other words, this method -is most useful when making form customizations that are specific to a single -form in your application. If you want to reuse a form customization across -several (or all) forms in your application, read on to the next section. - -.. _cookbook-form-twig-separate-template: - -Method 2: Inside a Separate Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can also choose to put the customized ``integer_widget`` form block in a -separate template entirely. The code and end-result are the same, but you -can now re-use the form customization across many templates: - -.. code-block:: html+jinja - - {# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #} - {% block integer_widget %} -
    - {% set type = type|default('number') %} - {{ block('form_widget_simple') }} -
    - {% endblock %} - -Now that you've created the customized form block, you need to tell Symfony -to use it. Inside the template where you're actually rendering your form, -tell Symfony to use the template via the ``form_theme`` tag: - -.. _cookbook-form-twig-theme-import-template: - -.. code-block:: html+jinja - - {% form_theme form 'AcmeDemoBundle:Form:fields.html.twig' %} - - {{ form_widget(form.age) }} - -When the ``form.age`` widget is rendered, Symfony will use the ``integer_widget`` -block from the new template and the ``input`` tag will be wrapped in the -``div`` element specified in the customized block. - -.. _cookbook-form-php-theming: - -Form Theming in PHP -------------------- - -When using PHP as a templating engine, the only method to customize a fragment -is to create a new template file - this is similar to the second method used by -Twig. - -The template file must be named after the fragment. You must create a ``integer_widget.html.php`` -file in order to customize the ``integer_widget`` fragment. - -.. code-block:: html+php - - -
    - block($form, 'form_widget_simple', array('type' => isset($type) ? $type : "number")) ?> -
    - -Now that you've created the customized form template, you need to tell Symfony -to use it. Inside the template where you're actually rendering your form, -tell Symfony to use the theme via the ``setTheme`` helper method: - -.. _cookbook-form-php-theme-import-template: - -.. code-block:: php - - setTheme($form, array('AcmeDemoBundle:Form')) ;?> - - widget($form['age']) ?> - -When the ``form.age`` widget is rendered, Symfony will use the customized -``integer_widget.html.php`` template and the ``input`` tag will be wrapped in -the ``div`` element. - -.. _cookbook-form-twig-import-base-blocks: - -Referencing Base Form Blocks (Twig specific) --------------------------------------------- - -So far, to override a particular form block, the best method is to copy -the default block from `form_div_layout.html.twig`_, paste it into a different template, -and then customize it. In many cases, you can avoid doing this by referencing -the base block when customizing it. - -This is easy to do, but varies slightly depending on if your form block customizations -are in the same template as the form or a separate template. - -Referencing Blocks from inside the same Template as the Form -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Import the blocks by adding a ``use`` tag in the template where you're rendering -the form: - -.. code-block:: jinja - - {% use 'form_div_layout.html.twig' with integer_widget as base_integer_widget %} - -Now, when the blocks from `form_div_layout.html.twig`_ are imported, the -``integer_widget`` block is called ``base_integer_widget``. This means that when -you redefine the ``integer_widget`` block, you can reference the default markup -via ``base_integer_widget``: - -.. code-block:: html+jinja - - {% block integer_widget %} -
    - {{ block('base_integer_widget') }} -
    - {% endblock %} - -Referencing Base Blocks from an External Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If your form customizations live inside an external template, you can reference -the base block by using the ``parent()`` Twig function: - -.. code-block:: html+jinja - - {# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #} - {% extends 'form_div_layout.html.twig' %} - - {% block integer_widget %} -
    - {{ parent() }} -
    - {% endblock %} - -.. note:: - - It is not possible to reference the base block when using PHP as the - templating engine. You have to manually copy the content from the base block - to your new template file. - -.. _cookbook-form-global-theming: - -Making Application-wide Customizations --------------------------------------- - -If you'd like a certain form customization to be global to your application, -you can accomplish this by making the form customizations in an external -template and then importing it inside your application configuration: - -Twig -~~~~ - -By using the following configuration, any customized form blocks inside the -``AcmeDemoBundle:Form:fields.html.twig`` template will be used globally when a -form is rendered. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - twig: - form: - resources: - - 'AcmeDemoBundle:Form:fields.html.twig' - # ... - - .. code-block:: xml - - - - - AcmeDemoBundle:Form:fields.html.twig - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('twig', array( - 'form' => array( - 'resources' => array( - 'AcmeDemoBundle:Form:fields.html.twig', - ), - ), - - // ... - )); - -By default, Twig uses a *div* layout when rendering forms. Some people, however, -may prefer to render forms in a *table* layout. Use the ``form_table_layout.html.twig`` -resource to use such a layout: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - twig: - form: - resources: ['form_table_layout.html.twig'] - # ... - - .. code-block:: xml - - - - - form_table_layout.html.twig - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('twig', array( - 'form' => array( - 'resources' => array( - 'form_table_layout.html.twig', - ), - ), - - // ... - )); - -If you only want to make the change in one template, add the following line to -your template file rather than adding the template as a resource: - -.. code-block:: html+jinja - - {% form_theme form 'form_table_layout.html.twig' %} - -Note that the ``form`` variable in the above code is the form view variable -that you passed to your template. - -PHP -~~~ - -By using the following configuration, any customized form fragments inside the -``src/Acme/DemoBundle/Resources/views/Form`` folder will be used globally when a -form is rendered. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - templating: - form: - resources: - - 'AcmeDemoBundle:Form' - # ... - - - .. code-block:: xml - - - - - - AcmeDemoBundle:Form - - - - - - - .. code-block:: php - - // app/config/config.php - // PHP - $container->loadFromExtension('framework', array( - 'templating' => array( - 'form' => array( - 'resources' => array( - 'AcmeDemoBundle:Form', - ), - ), - ), - - // ... - )); - -By default, the PHP engine uses a *div* layout when rendering forms. Some people, -however, may prefer to render forms in a *table* layout. Use the ``FrameworkBundle:FormTable`` -resource to use such a layout: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - templating: - form: - resources: - - 'FrameworkBundle:FormTable' - - .. code-block:: xml - - - - - - FrameworkBundle:FormTable - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - 'templating' => array( - 'form' => array( - 'resources' => array( - 'FrameworkBundle:FormTable', - ), - ), - ), - - // ... - )); - -If you only want to make the change in one template, add the following line to -your template file rather than adding the template as a resource: - -.. code-block:: html+php - - setTheme($form, array('FrameworkBundle:FormTable')); ?> - -Note that the ``$form`` variable in the above code is the form view variable -that you passed to your template. - -How to customize an Individual field ------------------------------------- - -So far, you've seen the different ways you can customize the widget output -of all text field types. You can also customize individual fields. For example, -suppose you have two ``text`` fields - ``first_name`` and ``last_name`` - but -you only want to customize one of the fields. This can be accomplished by -customizing a fragment whose name is a combination of the field id attribute and -which part of the field is being customized. For example: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% form_theme form _self %} - - {% block _product_name_widget %} -
    - {{ block('form_widget_simple') }} -
    - {% endblock %} - - {{ form_widget(form.name) }} - - .. code-block:: html+php - - - setTheme($form, array('AcmeDemoBundle:Form')); ?> - - widget($form['name']); ?> - - - -
    - echo $view['form']->block('form_widget_simple') ?> -
    - -Here, the ``_product_name_widget`` fragment defines the template to use for the -field whose *id* is ``product_name`` (and name is ``product[name]``). - -.. tip:: - - The ``product`` portion of the field is the form name, which may be set - manually or generated automatically based on your form type name (e.g. - ``ProductType`` equates to ``product``). If you're not sure what your - form name is, just view the source of your generated form. - -You can also override the markup for an entire field row using the same method: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# _product_name_row.html.twig #} - {% form_theme form _self %} - - {% block _product_name_row %} -
    - {{ form_label(form) }} - {{ form_errors(form) }} - {{ form_widget(form) }} -
    - {% endblock %} - - .. code-block:: html+php - - - -
    - label($form) ?> - errors($form) ?> - widget($form) ?> -
    - -Other Common Customizations ---------------------------- - -So far, this recipe has shown you several different ways to customize a single -piece of how a form is rendered. The key is to customize a specific fragment that -corresponds to the portion of the form you want to control (see -:ref:`naming form blocks`). - -In the next sections, you'll see how you can make several common form customizations. -To apply these customizations, use one of the methods described in the -:ref:`cookbook-form-theming-methods` section. - -Customizing Error Output -~~~~~~~~~~~~~~~~~~~~~~~~ - -.. note:: - The form component only handles *how* the validation errors are rendered, - and not the actual validation error messages. The error messages themselves - are determined by the validation constraints you apply to your objects. - For more information, see the chapter on :doc:`validation`. - -There are many different ways to customize how errors are rendered when a -form is submitted with errors. The error messages for a field are rendered -when you use the ``form_errors`` helper: - -.. configuration-block:: - - .. code-block:: jinja - - {{ form_errors(form.age) }} - - .. code-block:: php - - errors($form['age']); ?> - -By default, the errors are rendered inside an unordered list: - -.. code-block:: html - -
      -
    • This field is required
    • -
    - -To override how errors are rendered for *all* fields, simply copy, paste -and customize the ``form_errors`` fragment. - -.. configuration-block:: - - .. code-block:: html+jinja - - {# form_errors.html.twig #} - {% block form_errors %} - {% spaceless %} - {% if errors|length > 0 %} -
      - {% for error in errors %} -
    • {{ error.message }}
    • - {% endfor %} -
    - {% endif %} - {% endspaceless %} - {% endblock form_errors %} - - .. code-block:: html+php - - - -
      - -
    • getMessage() ?>
    • - -
    - - -.. tip:: - - See :ref:`cookbook-form-theming-methods` for how to apply this customization. - -You can also customize the error output for just one specific field type. -For example, certain errors that are more global to your form (i.e. not specific -to just one field) are rendered separately, usually at the top of your form: - -.. configuration-block:: - - .. code-block:: jinja - - {{ form_errors(form) }} - - .. code-block:: php - - render($form); ?> - -To customize *only* the markup used for these errors, follow the same directions -as above, but now call the block ``form_errors`` (Twig) / the file ``form_errors.html.php`` -(PHP). Now, when errors for the ``form`` type are rendered, your customized -fragment will be used instead of the default ``form_errors``. - -Customizing the "Form Row" -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When you can manage it, the easiest way to render a form field is via the -``form_row`` function, which renders the label, errors and HTML widget of -a field. To customize the markup used for rendering *all* form field rows, -override the ``form_row`` fragment. For example, suppose you want to add a -class to the ``div`` element around each row: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# form_row.html.twig #} - {% block form_row %} -
    - {{ form_label(form) }} - {{ form_errors(form) }} - {{ form_widget(form) }} -
    - {% endblock form_row %} - - .. code-block:: html+php - - -
    - label($form) ?> - errors($form) ?> - widget($form) ?> -
    - -.. tip:: - - See :ref:`cookbook-form-theming-methods` for how to apply this customization. - -Adding a "Required" Asterisk to Field Labels -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you want to denote all of your required fields with a required asterisk (``*``), -you can do this by customizing the ``form_label`` fragment. - -In Twig, if you're making the form customization inside the same template as your -form, modify the ``use`` tag and add the following: - -.. code-block:: html+jinja - - {% use 'form_div_layout.html.twig' with form_label as base_form_label %} - - {% block form_label %} - {{ block('base_form_label') }} - - {% if required %} - * - {% endif %} - {% endblock %} - -In Twig, if you're making the form customization inside a separate template, use -the following: - -.. code-block:: html+jinja - - {% extends 'form_div_layout.html.twig' %} - - {% block form_label %} - {{ parent() }} - - {% if required %} - * - {% endif %} - {% endblock %} - -When using PHP as a templating engine you have to copy the content from the -original template: - -.. code-block:: html+php - - - - - - - humanize($name); } ?> - - - - - * - - -.. tip:: - - See :ref:`cookbook-form-theming-methods` for how to apply this customization. - -Adding "help" messages -~~~~~~~~~~~~~~~~~~~~~~ - -You can also customize your form widgets to have an optional "help" message. - -In Twig, If you're making the form customization inside the same template as your -form, modify the ``use`` tag and add the following: - -.. code-block:: html+jinja - - {% use 'form_div_layout.html.twig' with form_widget_simple as base_form_widget_simple %} - - {% block form_widget_simple %} - {{ block('base_form_widget_simple') }} - - {% if help is defined %} - {{ help }} - {% endif %} - {% endblock %} - -In twig, If you're making the form customization inside a separate template, use -the following: - -.. code-block:: html+jinja - - {% extends 'form_div_layout.html.twig' %} - - {% block form_widget_simple %} - {{ parent() }} - - {% if help is defined %} - {{ help }} - {% endif %} - {% endblock %} - -When using PHP as a templating engine you have to copy the content from the -original template: - -.. code-block:: html+php - - - - - value="escape($value) ?>" - block($form, 'widget_attributes') ?> - /> - - - - escape($help) ?> - - -To render a help message below a field, pass in a ``help`` variable: - -.. configuration-block:: - - .. code-block:: jinja - - {{ form_widget(form.title, {'help': 'foobar'}) }} - - .. code-block:: php - - widget($form['title'], array('help' => 'foobar')) ?> - -.. tip:: - - See :ref:`cookbook-form-theming-methods` for how to apply this customization. - -Using Form Variables --------------------- - -Most of the functions available for rendering different parts of a form (e.g. -the form widget, form label, form widget, etc) also allow you to make certain -customizations directly. Look at the following example: - -.. configuration-block:: - - .. code-block:: jinja - - {# render a widget, but add a "foo" class to it #} - {{ form_widget(form.name, { 'attr': {'class': 'foo'} }) }} - - .. code-block:: php - - - widget($form['name'], array( - 'attr' => array( - 'class' => 'foo', - ), - )) ?> - -The array passed as the second argument contains form "variables". For -more details about this concept in Twig, see :ref:`twig-reference-form-variables`. - -.. _`form_div_layout.html.twig`: https://github.com/symfony/symfony/blob/2.2/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig diff --git a/cookbook/form/index.rst b/cookbook/form/index.rst deleted file mode 100644 index d148a76ac55..00000000000 --- a/cookbook/form/index.rst +++ /dev/null @@ -1,16 +0,0 @@ -Form -==== - -.. toctree:: - :maxdepth: 2 - - form_customization - data_transformers - dynamic_form_modification - form_collections - create_custom_field_type - create_form_type_extension - inherit_data_option - unit_testing - use_empty_data - direct_submit diff --git a/cookbook/form/inherit_data_option.rst b/cookbook/form/inherit_data_option.rst deleted file mode 100644 index 20c421a6cf2..00000000000 --- a/cookbook/form/inherit_data_option.rst +++ /dev/null @@ -1,158 +0,0 @@ -.. index:: - single: Form; The "inherit_data" option - -How to Reduce Code Duplication with "inherit_data" -================================================== - -.. versionadded:: 2.3 - This ``inherit_data`` option was known as ``virtual`` before Symfony 2.3. - -The ``inherit_data`` form field option can be very useful when you have some -duplicated fields in different entities. For example, imagine you have two -entities, a ``Company`` and a ``Customer``:: - - // src/Acme/HelloBundle/Entity/Company.php - namespace Acme\HelloBundle\Entity; - - class Company - { - private $name; - private $website; - - private $address; - private $zipcode; - private $city; - private $country; - } - -.. code-block:: php - - // src/Acme/HelloBundle/Entity/Customer.php - namespace Acme\HelloBundle\Entity; - - class Customer - { - private $firstName; - private $lastName; - - private $address; - private $zipcode; - private $city; - private $country; - } - -As you can see, each entity shares a few of the same fields: ``address``, -``zipcode``, ``city``, ``country``. - -Let's build two forms for these entities, ``CompanyType`` and ``CustomerType``:: - - // src/Acme/HelloBundle/Form/Type/CompanyType.php - namespace Acme\HelloBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilder; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - - class CompanyType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('name', 'text') - ->add('website', 'text'); - } - } - -.. code-block:: php - - // src/Acme/HelloBundle/Form/Type/CustomerType.php - namespace Acme\HelloBundle\Form\Type; - - use Symfony\Component\Form\FormBuilderInterface; - use Symfony\Component\Form\AbstractType; - - class CustomerType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('firstName', 'text') - ->add('lastName', 'text'); - } - } - -Instead of including the duplicated fields ``address``, ``zipcode``, ``city`` -and ``country``in both of these forms, we will create a third form for that. -We will call this form simply ``LocationType``:: - - // src/Acme/HelloBundle/Form/Type/LocationType.php - namespace Acme\HelloBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - class LocationType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('address', 'textarea') - ->add('zipcode', 'text') - ->add('city', 'text') - ->add('country', 'text'); - } - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'inherit_data' => true - )); - } - - public function getName() - { - return 'location'; - } - } - -The location form has an interesting option set, namely ``inherit_data``. This -option lets the form inherit its data from its parent form. If embedded in -the company form, the fields of the location form will access the properties of -the ``Company`` instance. If embedded in the customer form, the fields will -access the properties of the ``Customer`` instance instead. Easy, eh? - -.. note:: - - Instead of setting the ``inherit_data`` option inside ``LocationType``, you - can also (just like with any option) pass it in the third argument of - ``$builder->add()``. - -Let's make this work by adding the location form to our two original forms:: - - // src/Acme/HelloBundle/Form/Type/CompanyType.php - public function buildForm(FormBuilderInterface $builder, array $options) - { - // ... - - $builder->add('foo', new LocationType(), array( - 'data_class' => 'Acme\HelloBundle\Entity\Company' - )); - } - -.. code-block:: php - - // src/Acme/HelloBundle/Form/Type/CustomerType.php - public function buildForm(FormBuilderInterface $builder, array $options) - { - // ... - - $builder->add('bar', new LocationType(), array( - 'data_class' => 'Acme\HelloBundle\Entity\Customer' - )); - } - -That's it! You have extracted duplicated field definitions to a separate -location form that you can reuse wherever you need it. diff --git a/cookbook/form/unit_testing.rst b/cookbook/form/unit_testing.rst deleted file mode 100644 index 499eccd81d1..00000000000 --- a/cookbook/form/unit_testing.rst +++ /dev/null @@ -1,247 +0,0 @@ -.. index:: - single: Form; Form testing - -How to Unit Test your Forms -=========================== - -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 -:class:`Symfony\\Component\\Form\\FormView`. - -The only class that is usually manipulated by programmers is the form type class -which serves as a form blueprint. It is used to generate the ``Form`` and the -``FormView``. You could test it directly by mocking its interactions with the -factory but it would be complex. It is better to pass it to FormFactory like it -is done in a real application. It is simple to bootstrap and you can trust -the Symfony components enough to use them as a testing base. - -There is already a class that you can benefit from for simple FormTypes -testing: :class:`Symfony\\Component\\Form\\Tests\\Extension\\Core\\Type\\TypeTestCase`. -It is used to test the core types and you can use it to test your types too. - -.. note:: - - Depending on the way you installed your Symfony or Symfony Form Component - the tests may not be downloaded. Use the --prefer-source option with - composer if this is the case. - -The Basics ----------- - -The simplest ``TypeTestCase`` implementation looks like the following:: - - // src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php - namespace Acme\TestBundle\Tests\Form\Type; - - use Acme\TestBundle\Form\Type\TestedType; - use Acme\TestBundle\Model\TestObject; - use Symfony\Component\Form\Tests\Extension\Core\Type\TypeTestCase; - - class TestedTypeTest extends TypeTestCase - { - public function testSubmitValidData() - { - $formData = array( - 'test' => 'test', - 'test2' => 'test2', - ); - - $type = new TestedType(); - $form = $this->factory->create($type); - - $object = new TestObject(); - $object->fromArray($formData); - - // submit the data to the form directly - $form->submit($formData); - - $this->assertTrue($form->isSynchronized()); - $this->assertEquals($object, $form->getData()); - - $view = $form->createView(); - $children = $view->children; - - foreach (array_keys($formData) as $key) { - $this->assertArrayHasKey($key, $children); - } - } - } - -So, what does it test? Let's explain it line by line. - -First you verify if the ``FormType`` compiles. This includes basic class -inheritance, the ``buildForm`` function and options resolution. This should -be the first test you write:: - - $type = new TestedType(); - $form = $this->factory->create($type); - -This test checks that none of your data transformers used by the form -failed. The :method:`Symfony\\Component\\Form\\FormInterface::isSynchronized`` -method is only set to ``false`` if a data transformer throws an exception:: - - $form->submit($formData); - $this->assertTrue($form->isSynchronized()); - -.. note:: - - Don't test the validation: it is applied by a listener that is not - active in the test case and it relies on validation configuration. - Instead, unit test your custom constraints directly. - -Next, verify the submission and mapping of the form. The test below -checks if all the fields are correctly specified:: - - $this->assertEquals($object, $form->getData()); - -Finally, check the creation of the ``FormView``. You should check if all -widgets you want to display are available in the children property:: - - $view = $form->createView(); - $children = $view->children; - - foreach (array_keys($formData) as $key) { - $this->assertArrayHasKey($key, $children); - } - -Adding a Type your Form depends on ----------------------------------- - -Your form may depend on other types that are defined as services. It -might look like this:: - - // src/Acme/TestBundle/Form/Type/TestedType.php - - // ... the buildForm method - $builder->add('acme_test_child_type'); - -To create your form correctly, you need to make the type available to the -form factory in your test. The easiest way is to register it manually -before creating the parent form:: - - // src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php - namespace Acme\TestBundle\Tests\Form\Type; - - use Acme\TestBundle\Form\Type\TestedType; - use Acme\TestBundle\Model\TestObject; - use Symfony\Component\Form\Tests\Extension\Core\Type\TypeTestCase; - - class TestedTypeTest extends TypeTestCase - { - public function testSubmitValidData() - { - $this->factory->addType(new TestChildType()); - - $type = new TestedType(); - $form = $this->factory->create($type); - - // ... your test - } - } - -.. caution:: - - Make sure the child type you add is well tested. Otherwise you may - be getting errors that are not related to the form you are currently - testing but to its children. - -Adding custom Extensions ------------------------- - -It often happens that you use some options that are added by -:doc:`form extensions`. One of the -cases may be the ``ValidatorExtension`` with its ``invalid_message`` option. -The ``TypeTestCase`` loads only the core form extension so an "Invalid option" -exception will be raised if you try to use it for testing a class that depends -on other extensions. You need add those extensions to the factory object:: - - // src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php - namespace Acme\TestBundle\Tests\Form\Type; - - use Acme\TestBundle\Form\Type\TestedType; - use Acme\TestBundle\Model\TestObject; - use Symfony\Component\Form\Tests\Extension\Core\Type\TypeTestCase; - - class TestedTypeTest extends TypeTestCase - { - protected function setUp() - { - parent::setUp(); - - $this->factory = Forms::createFormFactoryBuilder() - ->addTypeExtension( - new FormTypeValidatorExtension( - $this->getMock('Symfony\Component\Validator\ValidatorInterface') - ) - ) - ->addTypeGuesser( - $this->getMockBuilder( - 'Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser' - ) - ->disableOriginalConstructor() - ->getMock() - ) - ->getFormFactory(); - - $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); - $this->builder = new FormBuilder(null, null, $this->dispatcher, $this->factory); - } - - // ... your tests - } - -Testing against different Sets of Data --------------------------------------- - -If you are not familiar yet with PHPUnit's `data providers`_, this might be -a good opportunity to use them:: - - // src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php - namespace Acme\TestBundle\Tests\Form\Type; - - use Acme\TestBundle\Form\Type\TestedType; - use Acme\TestBundle\Model\TestObject; - use Symfony\Component\Form\Tests\Extension\Core\Type\TypeTestCase; - - class TestedTypeTest extends TypeTestCase - { - - /** - * @dataProvider getValidTestData - */ - public function testForm($data) - { - // ... your test - } - - public function getValidTestData() - { - return array( - array( - 'data' => array( - 'test' => 'test', - 'test2' => 'test2', - ), - ), - array( - 'data' => array(), - ), - array( - 'data' => array( - 'test' => null, - 'test2' => null, - ), - ), - ); - } - } - -The code above will run your test three times with 3 different sets of -data. This allows for decoupling the test fixtures from the tests and -easily testing against multiple sets of data. - -You can also pass another argument, such as a boolean if the form has to -be synchronized with the given set of data or not etc. - -.. _`data providers`: http://www.phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.data-providers diff --git a/cookbook/form/use_empty_data.rst b/cookbook/form/use_empty_data.rst deleted file mode 100644 index 0b09ac3cf62..00000000000 --- a/cookbook/form/use_empty_data.rst +++ /dev/null @@ -1,86 +0,0 @@ -.. index:: - single: Form; Empty data - -How to configure Empty Data for a Form Class -============================================ - -The ``empty_data`` option allows you to specify an empty data set for your -form class. This empty data set would be used if you submit your form, but -haven't called ``setData()`` on your form or passed in data when you created -you form. For example:: - - public function indexAction() - { - $blog = ...; - - // $blog is passed in as the data, so the empty_data option is not needed - $form = $this->createForm(new BlogType(), $blog); - - // no data is passed in, so empty_data is used to get the "starting data" - $form = $this->createForm(new BlogType()); - } - -By default, ``empty_data`` is set to ``null``. Or, if you have specified -a ``data_class`` option for your form class, it will default to a new instance -of that class. That instance will be created by calling the constructor -with no arguments. - -If you want to override this default behavior, there are two ways to do this. - -Option 1: Instantiate a new Class ---------------------------------- - -One reason you might use this option is if you want to use a constructor -that takes arguments. Remember, the default ``data_class`` option calls -that constructor with no arguments:: - - // src/Acme/DemoBundle/Form/Type/BlogType.php - - // ... - use Symfony\Component\Form\AbstractType; - use Acme\DemoBundle\Entity\Blog; - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - class BlogType extends AbstractType - { - private $someDependency; - - public function __construct($someDependency) - { - $this->someDependency = $someDependency; - } - // ... - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'empty_data' => new Blog($this->someDependency), - )); - } - } - -You can instantiate your class however you want. In this example, we pass -some dependency into the ``BlogType`` when we instantiate it, then use that -to instantiate the ``Blog`` object. The point is, you can set ``empty_data`` -to the exact "new" object that you want to use. - -Option 2: Provide a Closure ---------------------------- - -Using a closure is the preferred method, since it will only create the object -if it is needed. - -The closure must accept a ``FormInterface`` instance as the first argument:: - - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - use Symfony\Component\Form\FormInterface; - // ... - - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'empty_data' => function (FormInterface $form) { - return new Blog($form->get('title')->getData()); - }, - )); - } diff --git a/cookbook/index.rst b/cookbook/index.rst deleted file mode 100644 index d50b4b025fa..00000000000 --- a/cookbook/index.rst +++ /dev/null @@ -1,34 +0,0 @@ -The Cookbook -============ - -.. toctree:: - :hidden: - - workflow/index - controller/index - routing/index - assetic/index - doctrine/index - form/index - validation/index - configuration/index - serializer - service_container/index - bundles/index - email/index - testing/index - security/index - cache/index - templating/index - logging/index - console/index - debugging - event_dispatcher/index - request/index - session/index - profiler/index - web_services/index - symfony1 - deployment-tools - -.. include:: /cookbook/map.rst.inc diff --git a/cookbook/logging/channels_handlers.rst b/cookbook/logging/channels_handlers.rst deleted file mode 100644 index 08161091004..00000000000 --- a/cookbook/logging/channels_handlers.rst +++ /dev/null @@ -1,98 +0,0 @@ -.. index:: - single: Logging - -How to log Messages to different Files -====================================== - -.. versionadded:: 2.1 - The ability to specify channels for a specific handler was added to - the MonologBundle for Symfony 2.1. - -The Symfony Standard Edition contains a bunch of channels for logging: ``doctrine``, -``event``, ``security`` and ``request``. Each channel corresponds to a logger -service (``monolog.logger.XXX``) in the container and is injected to the -concerned service. The purpose of channels is to be able to organize different -types of log messages. - -By default, Symfony2 logs every messages into a single file (regardless of -the channel). - -Switching a Channel to a different Handler ------------------------------------------- - -Now, suppose you want to log the ``doctrine`` channel to a different file. - -To do so, just create a new handler and configure it like this: - -.. configuration-block:: - - .. code-block:: yaml - - monolog: - handlers: - main: - type: stream - path: /var/log/symfony.log - channels: !doctrine - doctrine: - type: stream - path: /var/log/doctrine.log - channels: doctrine - - .. code-block:: xml - - - - - - exclusive - doctrine - - - - - - inclusive - doctrine - - - - - -Yaml specification ------------------- - -You can specify the configuration by many forms: - -.. code-block:: yaml - - channels: ~ # Include all the channels - - channels: foo # Include only channel "foo" - channels: !foo # Include all channels, except "foo" - - channels: [foo, bar] # Include only channels "foo" and "bar" - channels: [!foo, !bar] # Include all channels, except "foo" and "bar" - - channels: - type: inclusive # Include only those listed below - elements: [ foo, bar ] - channels: - type: exclusive # Include all, except those listed below - elements: [ foo, bar ] - -Creating your own Channel -------------------------- - -You can change the channel monolog logs to one service at a time. This is done -by tagging your service with ``monolog.logger`` and specifying which channel -the service should log to. By doing this, the logger that is injected into -that service is preconfigured to use the channel you've specified. - -For more information - including a full example - read ":ref:`dic_tags-monolog`" -in the Dependency Injection Tags reference section. - -Learn more from the Cookbook ----------------------------- - -* :doc:`/cookbook/logging/monolog` diff --git a/cookbook/logging/index.rst b/cookbook/logging/index.rst deleted file mode 100644 index c10cfa54716..00000000000 --- a/cookbook/logging/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Logging -======= - -.. toctree:: - :maxdepth: 2 - - monolog - monolog_email - channels_handlers \ No newline at end of file diff --git a/cookbook/logging/monolog.rst b/cookbook/logging/monolog.rst deleted file mode 100644 index 151d2020fea..00000000000 --- a/cookbook/logging/monolog.rst +++ /dev/null @@ -1,354 +0,0 @@ -.. index:: - single: Logging - -How to use Monolog to write Logs -================================ - -Monolog_ is a logging library for PHP 5.3 used by Symfony2. It is -inspired by the Python LogBook library. - -Usage ------ - -To log a message simply get the logger service from the container in -your controller:: - - public function indexAction() - { - $logger = $this->get('logger'); - $logger->info('I just got the logger'); - $logger->err('An error occurred'); - - // ... - } - -The ``logger`` service has different methods for different the logging levels. -See :class:`Symfony\\Component\\HttpKernel\\Log\\LoggerInterface` for details -on which methods are available. - -Handlers and Channels: Writing logs to different Locations ----------------------------------------------------------- - -In Monolog each logger defines a logging channel, which organizes your log -messages into different "categories". Then, each channel has a stack of handlers -to write the logs (the handlers can be shared). - -.. tip:: - - When injecting the logger in a service you can - :ref:`use a custom channel` control which "channel" - the logger will log to. - -The basic handler is the ``StreamHandler`` which writes logs in a stream -(by default in the ``app/logs/prod.log`` in the prod environment and -``app/logs/dev.log`` in the dev environment). - -Monolog comes also with a powerful built-in handler for the logging in -prod environment: ``FingersCrossedHandler``. It allows you to store the -messages in a buffer and to log them only if a message reaches the -action level (ERROR in the configuration provided in the standard -edition) by forwarding the messages to another handler. - -Using several handlers -~~~~~~~~~~~~~~~~~~~~~~ - -The logger uses a stack of handlers which are called successively. This -allows you to log the messages in several ways easily. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - monolog: - handlers: - applog: - type: stream - path: /var/log/symfony.log - level: error - main: - type: fingers_crossed - action_level: warning - handler: file - file: - type: stream - level: debug - syslog: - type: syslog - level: error - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('monolog', array( - 'handlers' => array( - 'applog' => array( - 'type' => 'stream', - 'path' => '/var/log/symfony.log', - 'level' => 'error', - ), - 'main' => array( - 'type' => 'fingers_crossed', - 'action_level' => 'warning', - 'handler' => 'file', - ), - 'file' => array( - 'type' => 'stream', - 'level' => 'debug', - ), - 'syslog' => array( - 'type' => 'syslog', - 'level' => 'error', - ), - ), - )); - -The above configuration defines a stack of handlers which will be called -in the order where they are defined. - -.. tip:: - - The handler named "file" will not be included in the stack itself as - it is used as a nested handler of the ``fingers_crossed`` handler. - -.. note:: - - If you want to change the config of MonologBundle in another config - file you need to redefine the whole stack. It cannot be merged - because the order matters and a merge does not allow to control the - order. - -Changing the formatter -~~~~~~~~~~~~~~~~~~~~~~ - -The handler uses a ``Formatter`` to format the record before logging -it. All Monolog handlers use an instance of -``Monolog\Formatter\LineFormatter`` by default but you can replace it -easily. Your formatter must implement -``Monolog\Formatter\FormatterInterface``. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - services: - my_formatter: - class: Monolog\Formatter\JsonFormatter - monolog: - handlers: - file: - type: stream - level: debug - formatter: my_formatter - - .. code-block:: xml - - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container - ->register('my_formatter', 'Monolog\Formatter\JsonFormatter'); - - $container->loadFromExtension('monolog', array( - 'handlers' => array( - 'file' => array( - 'type' => 'stream', - 'level' => 'debug', - 'formatter' => 'my_formatter', - ), - ), - )); - -Adding some extra data in the log messages ------------------------------------------- - -Monolog allows to process the record before logging it to add some -extra data. A processor can be applied for the whole handler stack or -only for a specific handler. - -A processor is simply a callable receiving the record as its first argument. - -Processors are configured using the ``monolog.processor`` DIC tag. See the -:ref:`reference about it`. - -Adding a Session/Request Token -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Sometimes it is hard to tell which entries in the log belong to which session -and/or request. The following example will add a unique token for each request -using a processor. - -.. code-block:: php - - namespace Acme\MyBundle; - - use Symfony\Component\HttpFoundation\Session\Session; - - class SessionRequestProcessor - { - private $session; - private $token; - - public function __construct(Session $session) - { - $this->session = $session; - } - - public function processRecord(array $record) - { - if (null === $this->token) { - try { - $this->token = substr($this->session->getId(), 0, 8); - } catch (\RuntimeException $e) { - $this->token = '????????'; - } - $this->token .= '-' . substr(uniqid(), -8); - } - $record['extra']['token'] = $this->token; - - return $record; - } - } - - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - services: - monolog.formatter.session_request: - class: Monolog\Formatter\LineFormatter - arguments: - - "[%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%%\n" - - monolog.processor.session_request: - class: Acme\MyBundle\SessionRequestProcessor - arguments: ["@session"] - tags: - - { name: monolog.processor, method: processRecord } - - monolog: - handlers: - main: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: debug - formatter: monolog.formatter.session_request - - .. code-block:: xml - - - - - - [%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%%\n - - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container - ->register('monolog.formatter.session_request', 'Monolog\Formatter\LineFormatter') - ->addArgument('[%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%%\n'); - - $container - ->register('monolog.processor.session_request', 'Acme\MyBundle\SessionRequestProcessor') - ->addArgument(new Reference('session')) - ->addTag('monolog.processor', array('method' => 'processRecord')); - - $container->loadFromExtension('monolog', array( - 'handlers' => array( - 'main' => array( - 'type' => 'stream', - 'path' => '%kernel.logs_dir%/%kernel.environment%.log', - 'level' => 'debug', - 'formatter' => 'monolog.formatter.session_request', - ), - ), - )); - -.. note:: - - If you use several handlers, you can also register the processor at the - handler level instead of globally. - -.. _Monolog: https://github.com/Seldaek/monolog diff --git a/cookbook/logging/monolog_email.rst b/cookbook/logging/monolog_email.rst deleted file mode 100644 index c3ef1e147b1..00000000000 --- a/cookbook/logging/monolog_email.rst +++ /dev/null @@ -1,220 +0,0 @@ -.. index:: - single: Logging; Emailing errors - -How to Configure Monolog to Email Errors -======================================== - -Monolog_ can be configured to send an email when an error occurs with an -application. The configuration for this requires a few nested handlers -in order to avoid receiving too many emails. This configuration looks -complicated at first but each handler is fairly straight forward when -it is broken down. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_prod.yml - monolog: - handlers: - mail: - type: fingers_crossed - action_level: critical - handler: buffered - buffered: - type: buffer - handler: swift - swift: - type: swift_mailer - from_email: error@example.com - to_email: error@example.com - subject: An Error Occurred! - level: debug - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - // app/config/config_prod.php - $container->loadFromExtension('monolog', array( - 'handlers' => array( - 'mail' => array( - 'type' => 'fingers_crossed', - 'action_level' => 'critical', - 'handler' => 'buffered', - ), - 'buffered' => array( - 'type' => 'buffer', - 'handler' => 'swift', - ), - 'swift' => array( - 'type' => 'swift_mailer', - 'from_email' => 'error@example.com', - 'to_email' => 'error@example.com', - 'subject' => 'An Error Occurred!', - 'level' => 'debug', - ), - ), - )); - - -The ``mail`` handler is a ``fingers_crossed`` handler which means that -it is only triggered when the action level, in this case ``critical`` is reached. -It then logs everything including messages below the action level. The -``critical`` level is only triggered for 5xx HTTP code errors. The ``handler`` -setting means that the output is then passed onto the ``buffered`` handler. - -.. tip:: - - If you want both 400 level and 500 level errors to trigger an email, - set the ``action_level`` to ``error`` instead of ``critical``. - -The ``buffered`` handler simply keeps all the messages for a request and -then passes them onto the nested handler in one go. If you do not use this -handler then each message will be emailed separately. This is then passed -to the ``swift`` handler. This is the handler that actually deals with -emailing you the error. The settings for this are straightforward, the -to and from addresses and the subject. - -You can combine these handlers with other handlers so that the errors still -get logged on the server as well as the emails being sent: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_prod.yml - monolog: - handlers: - main: - type: fingers_crossed - action_level: critical - handler: grouped - grouped: - type: group - members: [streamed, buffered] - streamed: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: debug - buffered: - type: buffer - handler: swift - swift: - type: swift_mailer - from_email: error@example.com - to_email: error@example.com - subject: An Error Occurred! - level: debug - - .. code-block:: xml - - - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/config_prod.php - $container->loadFromExtension('monolog', array( - 'handlers' => array( - 'main' => array( - 'type' => 'fingers_crossed', - 'action_level' => 'critical', - 'handler' => 'grouped', - ), - 'grouped' => array( - 'type' => 'group', - 'members' => array('streamed', 'buffered'), - ), - 'streamed' => array( - 'type' => 'stream', - 'path' => '%kernel.logs_dir%/%kernel.environment%.log', - 'level' => 'debug', - ), - 'buffered' => array( - 'type' => 'buffer', - 'handler' => 'swift', - ), - 'swift' => array( - 'type' => 'swift_mailer', - 'from_email' => 'error@example.com', - 'to_email' => 'error@example.com', - 'subject' => 'An Error Occurred!', - 'level' => 'debug', - ), - ), - )); - - -This uses the ``group`` handler to send the messages to the two -group members, the ``buffered`` and the ``stream`` handlers. The messages will -now be both written to the log file and emailed. - -.. _Monolog: https://github.com/Seldaek/monolog diff --git a/cookbook/map.rst.inc b/cookbook/map.rst.inc deleted file mode 100644 index aca53d3d095..00000000000 --- a/cookbook/map.rst.inc +++ /dev/null @@ -1,180 +0,0 @@ -* :doc:`/cookbook/assetic/index` - - * :doc:`/cookbook/assetic/asset_management` - * :doc:`/cookbook/assetic/yuicompressor` - * :doc:`/cookbook/assetic/jpeg_optimize` - * :doc:`/cookbook/assetic/apply_to_option` - -* :doc:`/cookbook/bundles/index` - - * :doc:`/cookbook/bundles/installation` - * :doc:`/cookbook/bundles/best_practices` - * :doc:`/cookbook/bundles/inheritance` - * :doc:`/cookbook/bundles/override` - * :doc:`/cookbook/bundles/remove` - * :doc:`/cookbook/bundles/extension` - * :doc:`/cookbook/bundles/prepend_extension` - -* :doc:`/cookbook/cache/index` - - * :doc:`/cookbook/cache/varnish` - -* :doc:`/cookbook/configuration/index` - - * :doc:`/cookbook/configuration/environments` - * :doc:`/cookbook/configuration/override_dir_structure` - * :doc:`/cookbook/configuration/front_controllers_and_kernel` - * :doc:`/cookbook/configuration/external_parameters` - * :doc:`/cookbook/configuration/pdo_session_storage` - * :doc:`/cookbook/configuration/apache_router` - * :doc:`/cookbook/configuration/web_server_configuration` - -* :doc:`/cookbook/console/index` - - * :doc:`/cookbook/console/console_command` - * :doc:`/cookbook/console/usage` - * :doc:`/cookbook/console/sending_emails` - * :doc:`/cookbook/console/logging` - -* :doc:`/cookbook/controller/index` - - * :doc:`/cookbook/controller/error_pages` - * :doc:`/cookbook/controller/service` - -* **Debugging** - - * :doc:`/cookbook/debugging` - -* **Deployment** - - * :doc:`/cookbook/deployment-tools` - -* :doc:`/cookbook/doctrine/index` - - * :doc:`/cookbook/doctrine/file_uploads` - * :doc:`/cookbook/doctrine/common_extensions` - * :doc:`/cookbook/doctrine/event_listeners_subscribers` - * :doc:`/cookbook/doctrine/dbal` - * :doc:`/cookbook/doctrine/reverse_engineering` - * :doc:`/cookbook/doctrine/multiple_entity_managers` - * :doc:`/cookbook/doctrine/custom_dql_functions` - * :doc:`/cookbook/doctrine/resolve_target_entity` - * :doc:`/cookbook/doctrine/registration_form` - -* :doc:`/cookbook/email/index` - - * :doc:`/cookbook/email/email` - * :doc:`/cookbook/email/gmail` - * :doc:`/cookbook/email/dev_environment` - * :doc:`/cookbook/email/spool` - * :doc:`/cookbook/email/testing` - -* :doc:`/cookbook/event_dispatcher/index` - - * :doc:`/cookbook/event_dispatcher/before_after_filters` - * :doc:`/cookbook/event_dispatcher/class_extension` - * :doc:`/cookbook/event_dispatcher/method_behavior` - * (service container) :doc:`/cookbook/service_container/event_listener` - -* :doc:`/cookbook/form/index` - - * :doc:`/cookbook/form/form_customization` - * :doc:`/cookbook/form/data_transformers` - * :doc:`/cookbook/form/dynamic_form_modification` - * :doc:`/cookbook/form/form_collections` - * :doc:`/cookbook/form/create_custom_field_type` - * :doc:`/cookbook/form/create_form_type_extension` - * :doc:`/cookbook/form/inherit_data_option` - * :doc:`/cookbook/form/unit_testing` - * :doc:`/cookbook/form/use_empty_data` - * :doc:`/cookbook/form/direct_submit` - * (validation) :doc:`/cookbook/validation/custom_constraint` - * (doctrine) :doc:`/cookbook/doctrine/file_uploads` - -* :doc:`/cookbook/logging/index` - - * :doc:`/cookbook/logging/monolog` - * :doc:`/cookbook/logging/monolog_email` - * :doc:`/cookbook/logging/channels_handlers` - -* :doc:`/cookbook/profiler/index` - - * :doc:`/cookbook/profiler/data_collector` - -* :doc:`/cookbook/request/index` - - * :doc:`/cookbook/request/mime_type` - -* :doc:`/cookbook/session/index` - - * :doc:`/cookbook/session/php_bridge` - -* :doc:`/cookbook/routing/index` - - * :doc:`/cookbook/routing/scheme` - * :doc:`/cookbook/routing/slash_in_parameter` - * :doc:`/cookbook/routing/redirect_in_config` - * :doc:`/cookbook/routing/method_parameters` - * :doc:`/cookbook/routing/service_container_parameters` - * :doc:`/cookbook/routing/custom_route_loader` - -* :doc:`/cookbook/security/index` - - * :doc:`/cookbook/security/entity_provider` - * :doc:`/cookbook/security/remember_me` - * :doc:`/cookbook/security/voters` - * :doc:`/cookbook/security/acl` - * :doc:`/cookbook/security/acl_advanced` - * :doc:`/cookbook/security/force_https` - * :doc:`/cookbook/security/form_login` - * :doc:`/cookbook/security/securing_services` - * :doc:`/cookbook/security/custom_provider` - * :doc:`/cookbook/security/custom_authentication_provider` - * :doc:`/cookbook/security/target_path` - -* **Serializer** - - * :doc:`/cookbook/serializer` - -* :doc:`/cookbook/service_container/index` - - * :doc:`/cookbook/service_container/event_listener` - * :doc:`/cookbook/service_container/scopes` - * :doc:`/cookbook/service_container/compiler_passes` - -* **symfony1** - - * :doc:`/cookbook/symfony1` - -* :doc:`/cookbook/templating/index` - - * :doc:`/cookbook/templating/global_variables` - * :doc:`/cookbook/templating/namespaced_paths` - * :doc:`/cookbook/templating/PHP` - * :doc:`/cookbook/templating/twig_extension` - * :doc:`/cookbook/templating/render_without_controller` - -* :doc:`/cookbook/testing/index` - - * :doc:`/cookbook/testing/http_authentication` - * :doc:`/cookbook/testing/simulating_authentication` - * :doc:`/cookbook/testing/insulating_clients` - * :doc:`/cookbook/testing/profiling` - * :doc:`/cookbook/testing/database` - * :doc:`/cookbook/testing/doctrine` - * :doc:`/cookbook/testing/bootstrap` - * (email) :doc:`/cookbook/email/testing` - -* :doc:`/cookbook/validation/index` - - * :doc:`/cookbook/validation/custom_constraint` - -* :doc:`/cookbook/web_services/index` - - * :doc:`/cookbook/web_services/php_soap_extension` - -* :doc:`/cookbook/workflow/index` - - * :doc:`/cookbook/workflow/new_project_git` - * :doc:`/cookbook/workflow/new_project_svn` - diff --git a/cookbook/profiler/data_collector.rst b/cookbook/profiler/data_collector.rst deleted file mode 100644 index 57f1be1b3db..00000000000 --- a/cookbook/profiler/data_collector.rst +++ /dev/null @@ -1,173 +0,0 @@ -.. index:: - single: Profiling; Data collector - -How to create a custom Data Collector -===================================== - -The Symfony2 :ref:`Profiler ` delegates data collecting to -data collectors. Symfony2 comes bundled with a few of them, but you can easily -create your own. - -Creating a Custom Data Collector --------------------------------- - -Creating a custom data collector is as simple as implementing the -:class:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface`:: - - interface DataCollectorInterface - { - /** - * Collects data for the given Request and Response. - * - * @param Request $request A Request instance - * @param Response $response A Response instance - * @param \Exception $exception An Exception instance - */ - function collect(Request $request, Response $response, \Exception $exception = null); - - /** - * Returns the name of the collector. - * - * @return string The collector name - */ - function getName(); - } - -The ``getName()`` method must return a unique name. This is used to access the -information later on (see :doc:`/cookbook/testing/profiling` for -instance). - -The ``collect()`` method is responsible for storing the data it wants to give -access to in local properties. - -.. caution:: - - As the profiler serializes data collector instances, you should not - store objects that cannot be serialized (like PDO objects), or you need - to provide your own ``serialize()`` method. - -Most of the time, it is convenient to extend -:class:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollector` and -populate the ``$this->data`` property (it takes care of serializing the -``$this->data`` property):: - - class MemoryDataCollector extends DataCollector - { - public function collect(Request $request, Response $response, \Exception $exception = null) - { - $this->data = array( - 'memory' => memory_get_peak_usage(true), - ); - } - - public function getMemory() - { - return $this->data['memory']; - } - - public function getName() - { - return 'memory'; - } - } - -.. _data_collector_tag: - -Enabling Custom Data Collectors -------------------------------- - -To enable a data collector, add it as a regular service in one of your -configuration, and tag it with ``data_collector``: - -.. configuration-block:: - - .. code-block:: yaml - - services: - data_collector.your_collector_name: - class: Fully\Qualified\Collector\Class\Name - tags: - - { name: data_collector } - - .. code-block:: xml - - - - - - .. code-block:: php - - $container - ->register('data_collector.your_collector_name', 'Fully\Qualified\Collector\Class\Name') - ->addTag('data_collector') - ; - -Adding Web Profiler Templates ------------------------------ - -When you want to display the data collected by your Data Collector in the web -debug toolbar or the web profiler, create a Twig template following this -skeleton: - -.. code-block:: jinja - - {% extends 'WebProfilerBundle:Profiler:layout.html.twig' %} - - {% block toolbar %} - {# the web debug toolbar content #} - {% endblock %} - - {% block head %} - {# if the web profiler panel needs some specific JS or CSS files #} - {% endblock %} - - {% block menu %} - {# the menu content #} - {% endblock %} - - {% block panel %} - {# the panel content #} - {% endblock %} - -Each block is optional. The ``toolbar`` block is used for the web debug -toolbar and ``menu`` and ``panel`` are used to add a panel to the web -profiler. - -All blocks have access to the ``collector`` object. - -.. tip:: - - Built-in templates use a base64 encoded image for the toolbar (`` - - - - .. code-block:: php - - $container - ->register('data_collector.your_collector_name', 'Acme\DebugBundle\Collector\Class\Name') - ->addTag('data_collector', array( - 'template' => 'AcmeDebugBundle:Collector:templatename', - 'id' => 'your_collector_name', - )) - ; diff --git a/cookbook/profiler/index.rst b/cookbook/profiler/index.rst deleted file mode 100644 index 1e71a47d187..00000000000 --- a/cookbook/profiler/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Profiler -======== - -.. toctree:: - :maxdepth: 2 - - data_collector diff --git a/cookbook/request/index.rst b/cookbook/request/index.rst deleted file mode 100644 index 4df7f7eb2d8..00000000000 --- a/cookbook/request/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Request -======= - -.. toctree:: - :maxdepth: 2 - - mime_type diff --git a/cookbook/request/mime_type.rst b/cookbook/request/mime_type.rst deleted file mode 100644 index 29ac6f484d9..00000000000 --- a/cookbook/request/mime_type.rst +++ /dev/null @@ -1,92 +0,0 @@ -.. index:: - single: Request; Add a request format and mime type - -How to register a new Request Format and Mime Type -================================================== - -Every ``Request`` has a "format" (e.g. ``html``, ``json``), which is used -to determine what type of content to return in the ``Response``. In fact, -the request format, accessible via -:method:`Symfony\\Component\\HttpFoundation\\Request::getRequestFormat`, -is used to set the MIME type of the ``Content-Type`` header on the ``Response`` -object. Internally, Symfony contains a map of the most common formats (e.g. -``html``, ``json``) and their associated MIME types (e.g. ``text/html``, -``application/json``). Of course, additional format-MIME type entries can -easily be added. This document will show how you can add the ``jsonp`` format -and corresponding MIME type. - -Create a ``kernel.request`` Listener -------------------------------------- - -The key to defining a new MIME type is to create a class that will "listen" to -the ``kernel.request`` event dispatched by the Symfony kernel. The -``kernel.request`` event is dispatched early in Symfony's request handling -process and allows you to modify the request object. - -Create the following class, replacing the path with a path to a bundle in your -project:: - - // src/Acme/DemoBundle/RequestListener.php - namespace Acme\DemoBundle; - - use Symfony\Component\HttpKernel\HttpKernelInterface; - use Symfony\Component\HttpKernel\Event\GetResponseEvent; - - class RequestListener - { - public function onKernelRequest(GetResponseEvent $event) - { - $event->getRequest()->setFormat('jsonp', 'application/javascript'); - } - } - -Registering your Listener -------------------------- - -As with any other listener, you need to add it in one of your configuration -files and register it as a listener by adding the ``kernel.event_listener`` tag: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - services: - acme.demobundle.listener.request: - class: Acme\DemoBundle\RequestListener - tags: - - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest } - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - # app/config/config.php - $definition = new Definition('Acme\DemoBundle\RequestListener'); - $definition->addTag('kernel.event_listener', array( - 'event' => 'kernel.request', - 'method' => 'onKernelRequest', - )); - $container->setDefinition('acme.demobundle.listener.request', $definition); - -At this point, the ``acme.demobundle.listener.request`` service has been -configured and will be notified when the Symfony kernel dispatches the -``kernel.request`` event. - -.. tip:: - - You can also register the listener in a configuration extension class (see - :ref:`service-container-extension-configuration` for more information). diff --git a/cookbook/routing/custom_route_loader.rst b/cookbook/routing/custom_route_loader.rst deleted file mode 100644 index a3317411f82..00000000000 --- a/cookbook/routing/custom_route_loader.rst +++ /dev/null @@ -1,264 +0,0 @@ -.. index:: - single: Routing; Custom route loader - -How to create a custom Route Loader -=================================== - -A custom route loader allows you to add routes to an application without -including them, for example, in a Yaml file. This comes in handy when -you have a bundle but don't want to manually add the routes for the bundle -to ``app/config/routing.yml``. This may be especially important when you want -to make the bundle reusable, or when you have open-sourced it as this would -slow down the installation process and make it error-prone. - -Alternatively, you could also use a custom route loader when you want your -routes to be automatically generated or located based on some convention or -pattern. One example is the `FOSRestBundle`_ where routing is generated based -off the names of the action methods in a controller. - -.. note:: - - There are many bundles out there that use their own route loaders to - accomplish cases like those described above, for instance - `FOSRestBundle`_, `KnpRadBundle`_ and `SonataAdminBundle`_. - -Loading Routes --------------- - -The routes in a Symfony application are loaded by the -:class:`Symfony\\Bundle\\FrameworkBundle\\Routing\\DelegatingLoader`. -This loader uses several other loaders (delegates) to load resources of -different types, for instance Yaml files or ``@Route`` and ``@Method`` annotations -in controller files. The specialized loaders implement -:class:`Symfony\\Component\\Config\\Loader\\LoaderInterface` -and therefore have two important methods: -:method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::supports` -and :method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::load`. - -Take these lines from the ``routing.yml`` in the AcmeDemoBundle of the Standard -Edition: - -.. code-block:: yaml - - # src/Acme/DemoBundle/Resources/config/routing.yml - _demo: - resource: "@AcmeDemoBundle/Controller/DemoController.php" - type: annotation - prefix: /demo - -When the main loader parses this, it tries all the delegate loaders and calls -their :method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::supports` -method with the given resource (``@AcmeDemoBundle/Controller/DemoController.php``) -and type (``annotation``) as arguments. When one of the loader returns ``true``, -its :method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::load` method -will be called, which should return a :class:`Symfony\\Component\\Routing\\RouteCollection` -containing :class:`Symfony\\Component\\Routing\\Route` objects. - -Creating a Custom Loader ------------------------- - -To load routes from some custom source (i.e. from something other than annotations, -Yaml or XML files), you need to create a custom route loader. This loader -should implement :class:`Symfony\\Component\\Config\\Loader\\LoaderInterface`. - -The sample loader below supports loading routing resources with a type of -``extra``. The type ``extra`` isn't important - you can just invent any resource -type you want. The resource name itself is not actually used in the example:: - - namespace Acme\DemoBundle\Routing; - - use Symfony\Component\Config\Loader\LoaderInterface; - use Symfony\Component\Config\Loader\LoaderResolver; - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - class ExtraLoader implements LoaderInterface - { - private $loaded = false; - - public function load($resource, $type = null) - { - if (true === $this->loaded) { - throw new \RuntimeException('Do not add the "extra" loader twice'); - } - - $routes = new RouteCollection(); - - // prepare a new route - $pattern = '/extra/{parameter}'; - $defaults = array( - '_controller' => 'AcmeDemoBundle:Demo:extra', - ); - $requirements = array( - 'parameter' => '\d+', - ); - $route = new Route($pattern, $defaults, $requirements); - - // add the new route to the route collection: - $routeName = 'extraRoute'; - $routes->add($routeName, $route); - - return $routes; - } - - public function supports($resource, $type = null) - { - return 'extra' === $type; - } - - public function getResolver() - { - // needed, but can be blank, unless you want to load other resources - // and if you do, using the Loader base class is easier (see below) - } - - public function setResolver(LoaderResolver $resolver) - { - // same as above - } - } - -.. note:: - - Make sure the controller you specify really exists. - -Now define a service for the ``ExtraLoader``: - -.. configuration-block:: - - .. code-block:: yaml - - services: - acme_demo.routing_loader: - class: Acme\DemoBundle\Routing\ExtraLoader - tags: - - { name: routing.loader } - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Definition; - - $container - ->setDefinition( - 'acme_demo.routing_loader', - new Definition('Acme\DemoBundle\Routing\ExtraLoader') - ) - ->addTag('routing.loader') - ; - -Notice the tag ``routing.loader``. All services with this tag will be marked -as potential route loaders and added as specialized routers to the -:class:`Symfony\\Bundle\\FrameworkBundle\\Routing\\DelegatingLoader`. - -Using the Custom Loader -~~~~~~~~~~~~~~~~~~~~~~~ - -If you did nothing else, your custom routing loader would *not* be called. -Instead, you only need to add a few extra lines to the routing configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - AcmeDemoBundle_Extra: - resource: . - type: extra - - .. code-block:: xml - - - - - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\RouteCollection; - - $collection = new RouteCollection(); - $collection->addCollection($loader->import('.', 'extra')); - - return $collection; - -The important part here is the ``type`` key. Its value should be "extra". -This is the type which our ``ExtraLoader`` supports and this will make sure -its ``load()`` method gets called. The ``resource`` key is insignificant -for the ``ExtraLoader``, so we set it to ".". - -.. note:: - - The routes defined using custom route loaders will be automatically - cached by the framework. So whenever you change something in the loader - class itself, don't forget to clear the cache. - -More Advanced Loaders ---------------------- - -In most cases it's better not to implement -:class:`Symfony\\Component\\Config\\Loader\\LoaderInterface` -yourself, but extend from :class:`Symfony\\Component\\Config\\Loader\\Loader`. -This class knows how to use a :class:`Symfony\\Component\\Config\\Loader\\LoaderResolver` -to load secondary routing resources. - -Of course you still need to implement -:method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::supports` -and :method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::load`. -Whenever you want to load another resource - for instance a Yaml routing -configuration file - you can call the -:method:`Symfony\\Component\\Config\\Loader\\Loader::import` method:: - - namespace Acme\DemoBundle\Routing; - - use Symfony\Component\Config\Loader\Loader; - use Symfony\Component\Routing\RouteCollection; - - class AdvancedLoader extends Loader - { - public function load($resource, $type = null) - { - $collection = new RouteCollection(); - - $resource = '@AcmeDemoBundle/Resources/config/import_routing.yml'; - $type = 'yaml'; - - $importedRoutes = $this->import($resource, $type); - - $collection->addCollection($importedRoutes); - - return $collection; - } - - public function supports($resource, $type = null) - { - return $type === 'advanced_extra'; - } - } - -.. note:: - - The resource name and type of the imported routing configuration can - be anything that would normally be supported by the routing configuration - loader (Yaml, XML, PHP, annotation, etc.). - -.. _`FOSRestBundle`: https://github.com/FriendsOfSymfony/FOSRestBundle -.. _`KnpRadBundle`: https://github.com/KnpLabs/KnpRadBundle -.. _`SonataAdminBundle`: https://github.com/sonata-project/SonataAdminBundle diff --git a/cookbook/routing/index.rst b/cookbook/routing/index.rst deleted file mode 100644 index 64bb70e54b0..00000000000 --- a/cookbook/routing/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -Routing -======= - -.. toctree:: - :maxdepth: 2 - - scheme - slash_in_parameter - redirect_in_config - method_parameters - service_container_parameters - custom_route_loader diff --git a/cookbook/routing/method_parameters.rst b/cookbook/routing/method_parameters.rst deleted file mode 100644 index ecf152ecc47..00000000000 --- a/cookbook/routing/method_parameters.rst +++ /dev/null @@ -1,92 +0,0 @@ -.. index:: - single: Routing; methods - -How to use HTTP Methods beyond GET and POST in Routes -===================================================== - -The HTTP method of a request is one of the requirements that can be checked -when seeing if it matches a route. This is introduced in the routing chapter -of the book ":doc:`/book/routing`" with examples using GET and POST. You can -also use other HTTP verbs in this way. For example, if you have a blog post -entry then you could use the same URL path to show it, make changes to it and -delete it by matching on GET, PUT and DELETE. - -.. configuration-block:: - - .. code-block:: yaml - - blog_show: - path: /blog/{slug} - defaults: { _controller: AcmeDemoBundle:Blog:show } - methods: [GET] - - blog_update: - path: /blog/{slug} - defaults: { _controller: AcmeDemoBundle:Blog:update } - methods: [PUT] - - blog_delete: - path: /blog/{slug} - defaults: { _controller: AcmeDemoBundle:Blog:delete } - methods: [DELETE] - - .. code-block:: xml - - - - - - - AcmeDemoBundle:Blog:show - - - - AcmeDemoBundle:Blog:update - - - - AcmeDemoBundle:Blog:delete - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('blog_show', new Route('/blog/{slug}', array( - '_controller' => 'AcmeDemoBundle:Blog:show', - ), array(), array(), '', array(), array('GET'))); - - $collection->add('blog_update', new Route('/blog/{slug}', array( - '_controller' => 'AcmeDemoBundle:Blog:update', - ), array(), array(), '', array(), array('PUT'))); - - $collection->add('blog_delete', new Route('/blog/{slug}', array( - '_controller' => 'AcmeDemoBundle:Blog:delete', - ), array(), array(), '', array('DELETE'))); - - return $collection; - -Faking the Method with _method ------------------------------- - -.. note:: - - The ``_method`` functionality shown here is disabled by default in Symfony 2.2 - and enabled by default in Symfony 2.3. To control it in Symfony 2.2, you - must call :method:`Request::enableHttpMethodParameterOverride ` - before you handle the request (e.g. in your front controller). In Symfony - 2.3, use the :ref:`configuration-framework-http_method_override` option. - -Unfortunately, life isn't quite this simple, since most browsers do not -support sending PUT and DELETE requests. Fortunately Symfony2 provides you -with a simple way of working around this limitation. By including a ``_method`` -parameter in the query string or parameters of an HTTP request, Symfony2 will -use this as the method when matching routes. Forms automatically include a -hidden field for this parameter if their submission method is not GET or POST. -See :ref:`the related chapter in the forms documentation` -for more information. diff --git a/cookbook/routing/redirect_in_config.rst b/cookbook/routing/redirect_in_config.rst deleted file mode 100644 index 4d6b7004e6e..00000000000 --- a/cookbook/routing/redirect_in_config.rst +++ /dev/null @@ -1,40 +0,0 @@ -.. index:: - single: Routing; Configure redirect to another route without a custom controller - -How to configure a redirect to another route without a custom controller -======================================================================== - -This guide explains how to configure a redirect from one route to another -without using a custom controller. - -Assume that there is no useful default controller for the ``/`` path of -your application and you want to redirect these requests to ``/app``. - -Your configuration will look like this: - -.. code-block:: yaml - - AppBundle: - resource: "@App/Controller/" - type: annotation - prefix: /app - - root: - path: / - defaults: - _controller: FrameworkBundle:Redirect:urlRedirect - path: /app - permanent: true - -In this example, you configure a route for the ``/`` path and let :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\RedirectController` -handle it. This controller comes standard with Symfony and offers two actions -for redirecting request: - -* ``urlRedirect`` redirects to another *path*. You must provide the ``path`` - parameter containing the path of the resource you want to redirect to. - -* ``redirect`` (not shown here) redirects to another *route*. You must provide the ``route`` - parameter with the *name* of the route you want to redirect to. - -The ``permanent`` switch tells both methods to issue a 301 HTTP status code -instead of the default ``302`` status code. diff --git a/cookbook/routing/scheme.rst b/cookbook/routing/scheme.rst deleted file mode 100644 index 9786f7a0a27..00000000000 --- a/cookbook/routing/scheme.rst +++ /dev/null @@ -1,72 +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 via schemes: - -.. configuration-block:: - - .. code-block:: yaml - - secure: - path: /secure - defaults: { _controller: AcmeDemoBundle:Main:secure } - schemes: [https] - - .. code-block:: xml - - - - - - - AcmeDemoBundle:Main:secure - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('secure', new Route('/secure', array( - '_controller' => 'AcmeDemoBundle:Main:secure', - ), array(), array(), '', array('https'))); - - return $collection; - -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: - -.. code-block:: jinja - - {# If the current scheme is HTTPS #} - {{ path('secure') }} - # generates /secure - - {# If the current scheme is HTTP #} - {{ path('secure') }} - {# generates 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. diff --git a/cookbook/routing/service_container_parameters.rst b/cookbook/routing/service_container_parameters.rst deleted file mode 100644 index 8b6d100e264..00000000000 --- a/cookbook/routing/service_container_parameters.rst +++ /dev/null @@ -1,121 +0,0 @@ -.. index:: - single: Routing; Service Container Parameters - -How to use Service Container Parameters in your Routes -====================================================== - -.. versionadded:: 2.1 - The ability to use parameters in your routes was added in Symfony 2.1. - -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 your 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:: yaml - - contact: - path: /{_locale}/contact - defaults: { _controller: AcmeDemoBundle:Main:contact } - requirements: - _locale: %acme_demo.locales% - - .. code-block:: xml - - - - - - - AcmeDemoBundle:Main:contact - %acme_demo.locales% - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('contact', new Route('/{_locale}/contact', array( - '_controller' => 'AcmeDemoBundle:Main:contact', - ), array( - '_locale' => '%acme_demo.locales%', - ))); - - return $collection; - -You can now control and set the ``acme_demo.locales`` parameter somewhere -in your container: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - parameters: - acme_demo.locales: en|es - - .. code-block:: xml - - - - en|es - - - .. code-block:: php - - # app/config/config.php - $container->setParameter('acme_demo.locales', 'en|es'); - -You can also use a parameter to define your route path (or part of your -path): - -.. configuration-block:: - - .. code-block:: yaml - - some_route: - path: /%acme_demo.route_prefix%/contact - defaults: { _controller: AcmeDemoBundle:Main:contact } - - .. code-block:: xml - - - - - - - AcmeDemoBundle:Main:contact - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('some_route', new Route('/%acme_demo.route_prefix%/contact', array( - '_controller' => 'AcmeDemoBundle:Main:contact', - ))); - - return $collection; - -.. 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%``. diff --git a/cookbook/routing/slash_in_parameter.rst b/cookbook/routing/slash_in_parameter.rst deleted file mode 100644 index cc1532c16ee..00000000000 --- a/cookbook/routing/slash_in_parameter.rst +++ /dev/null @@ -1,78 +0,0 @@ -.. index:: - single: Routing; Allow / in route 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, take the classic ``/hello/{name}`` route. By default, -``/hello/Fabien`` will match this route but not ``/hello/Fabien/Kris``. This -is because Symfony uses this character as separator between route parts. - -This guide covers how you can modify a route so that ``/hello/Fabien/Kris`` -matches the ``/hello/{name}`` route, where ``{name}`` equals ``Fabien/Kris``. - -Configure the Route -------------------- - -By default, the Symfony routing components requires that the parameters -match the following regex path: ``[^/]+``. This means that all characters -are allowed except ``/``. - -You must explicitly allow ``/`` to be part of your parameter by specifying -a more permissive regex path. - -.. configuration-block:: - - .. code-block:: yaml - - _hello: - path: /hello/{name} - defaults: { _controller: AcmeDemoBundle:Demo:hello } - requirements: - name: ".+" - - .. code-block:: xml - - - - - - - AcmeDemoBundle:Demo:hello - .+ - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('_hello', new Route('/hello/{name}', array( - '_controller' => 'AcmeDemoBundle:Demo:hello', - ), array( - 'name' => '.+', - ))); - - return $collection; - - .. code-block:: php-annotations - - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; - - class DemoController - { - /** - * @Route("/hello/{name}", name="_hello", requirements={"name" = ".+"}) - */ - public function helloAction($name) - { - // ... - } - } - -That's it! Now, the ``{name}`` parameter can contain the ``/`` character. diff --git a/cookbook/security/acl.rst b/cookbook/security/acl.rst deleted file mode 100644 index ba389ed7e2f..00000000000 --- a/cookbook/security/acl.rst +++ /dev/null @@ -1,220 +0,0 @@ -.. index:: - single: Security; Access Control Lists (ACLs) - -How to use Access Control Lists (ACLs) -====================================== - -In complex applications, you will often face the problem that access decisions -cannot only be based on the person (``Token``) who is requesting access, but -also involve a domain object that access is being requested for. This is where -the ACL system comes in. - -Imagine you are designing a blog system where your users can comment on your -posts. Now, you want a user to be able to edit his own comments, but not those -of other users; besides, you yourself want to be able to edit all comments. In -this scenario, ``Comment`` would be the domain object that you want to -restrict access to. You could take several approaches to accomplish this using -Symfony2, two basic approaches are (non-exhaustive): - -- *Enforce security in your business methods*: Basically, that means keeping a - reference inside each ``Comment`` to all users who have access, and then - compare these users to the provided ``Token``. -- *Enforce security with roles*: In this approach, you would add a role for - each ``Comment`` object, i.e. ``ROLE_COMMENT_1``, ``ROLE_COMMENT_2``, etc. - -Both approaches are perfectly valid. However, they couple your authorization -logic to your business code which makes it less reusable elsewhere, and also -increases the difficulty of unit testing. Besides, you could run into -performance issues if many users would have access to a single domain object. - -Fortunately, there is a better way, which you will find out about now. - -Bootstrapping -------------- - -Now, before you can finally get into action, you need to do some bootstrapping. -First, you need to configure the connection the ACL system is supposed to use: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - acl: - connection: default - - .. code-block:: xml - - - - default - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', 'acl', array( - 'connection' => 'default', - )); - - -.. note:: - - The ACL system requires a connection from either Doctrine DBAL (usable by - default) or Doctrine MongoDB (usable with `MongoDBAclBundle`_). However, - that does not mean that you have to use Doctrine ORM or ODM for mapping your - domain objects. You can use whatever mapper you like for your objects, be it - Doctrine ORM, MongoDB ODM, Propel, raw SQL, etc. The choice is yours. - -After the connection is configured, you have to import the database structure. -Fortunately, there is a task for this. Simply run the following command: - -.. code-block:: bash - - $ php app/console init:acl - -Getting Started ---------------- - -Coming back to the small example from the beginning, let's implement ACL for -it. - -Creating an ACL, and adding an ACE -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: php - - // src/Acme/DemoBundle/Controller/BlogController.php - namespace Acme\DemoBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Security\Core\Exception\AccessDeniedException; - use Symfony\Component\Security\Acl\Domain\ObjectIdentity; - use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity; - use Symfony\Component\Security\Acl\Permission\MaskBuilder; - - class BlogController - { - // ... - - public function addCommentAction(Post $post) - { - $comment = new Comment(); - - // ... setup $form, and submit data - - if ($form->isValid()) { - $entityManager = $this->getDoctrine()->getManager(); - $entityManager->persist($comment); - $entityManager->flush(); - - // creating the ACL - $aclProvider = $this->get('security.acl.provider'); - $objectIdentity = ObjectIdentity::fromDomainObject($comment); - $acl = $aclProvider->createAcl($objectIdentity); - - // retrieving the security identity of the currently logged-in user - $securityContext = $this->get('security.context'); - $user = $securityContext->getToken()->getUser(); - $securityIdentity = UserSecurityIdentity::fromAccount($user); - - // grant owner access - $acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER); - $aclProvider->updateAcl($acl); - } - } - } - -There are a couple of important implementation decisions in this code snippet. -For now, I only want to highlight two: - -First, you may have noticed that ``->createAcl()`` does not accept domain -objects directly, but only implementations of the ``ObjectIdentityInterface``. -This additional step of indirection allows you to work with ACLs even when you -have no actual domain object instance at hand. This will be extremely helpful -if you want to check permissions for a large number of objects without -actually hydrating these objects. - -The other interesting part is the ``->insertObjectAce()`` call. In the -example, you are granting the user who is currently logged in owner access to -the Comment. The ``MaskBuilder::MASK_OWNER`` is a pre-defined integer bitmask; -don't worry the mask builder will abstract away most of the technical details, -but using this technique you can store many different permissions in one -database row which gives a considerable boost in performance. - -.. tip:: - - The order in which ACEs are checked is significant. As a general rule, you - should place more specific entries at the beginning. - -Checking Access -~~~~~~~~~~~~~~~ - -.. code-block:: php - - // src/Acme/DemoBundle/Controller/BlogController.php - - // ... - - class BlogController - { - // ... - - public function editCommentAction(Comment $comment) - { - $securityContext = $this->get('security.context'); - - // check for edit access - if (false === $securityContext->isGranted('EDIT', $comment)) - { - throw new AccessDeniedException(); - } - - // ... retrieve actual comment object, and do your editing here - } - } - -In this example, you check whether the user has the ``EDIT`` permission. -Internally, Symfony2 maps the permission to several integer bitmasks, and -checks whether the user has any of them. - -.. note:: - - You can define up to 32 base permissions (depending on your OS PHP might - vary between 30 to 32). In addition, you can also define cumulative - permissions. - -Cumulative Permissions ----------------------- - -In the first example above, you only granted the user the ``OWNER`` base -permission. While this effectively also allows the user to perform any -operation such as view, edit, etc. on the domain object, there are cases where -you may want to grant these permissions explicitly. - -The ``MaskBuilder`` can be used for creating bit masks easily by combining -several base permissions: - -.. code-block:: php - - $builder = new MaskBuilder(); - $builder - ->add('view') - ->add('edit') - ->add('delete') - ->add('undelete') - ; - $mask = $builder->get(); // int(29) - -This integer bitmask can then be used to grant a user the base permissions you -added above: - -.. code-block:: php - - $identity = new UserSecurityIdentity('johannes', 'Acme\UserBundle\Entity\User'); - $acl->insertObjectAce($identity, $mask); - -The user is now allowed to view, edit, delete, and un-delete objects. - -.. _`MongoDBAclBundle`: https://github.com/IamPersistent/MongoDBAclBundle diff --git a/cookbook/security/acl_advanced.rst b/cookbook/security/acl_advanced.rst deleted file mode 100644 index 52c7687db00..00000000000 --- a/cookbook/security/acl_advanced.rst +++ /dev/null @@ -1,188 +0,0 @@ -.. index:: - single: Security; Advanced ACL concepts - -How to use Advanced ACL Concepts -================================ - -The aim of this chapter is to give a more in-depth view of the ACL system, and -also explain some of the design decisions behind it. - -Design Concepts ---------------- - -Symfony2's object instance security capabilities are based on the concept of -an Access Control List. Every domain object **instance** has its own ACL. The -ACL instance holds a detailed list of Access Control Entries (ACEs) which are -used to make access decisions. Symfony2's ACL system focuses on two main -objectives: - -- providing a way to efficiently retrieve a large amount of ACLs/ACEs for your - domain objects, and to modify them; -- providing a way to easily make decisions of whether a person is allowed to - perform an action on a domain object or not. - -As indicated by the first point, one of the main capabilities of Symfony2's -ACL system is a high-performance way of retrieving ACLs/ACEs. This is -extremely important since each ACL might have several ACEs, and inherit from -another ACL in a tree-like fashion. Therefore, no ORM is leveraged, instead -the default implementation interacts with your connection directly using Doctrine's -DBAL. - -Object Identities -~~~~~~~~~~~~~~~~~ - -The ACL system is completely decoupled from your domain objects. They don't -even have to be stored in the same database, or on the same server. In order -to achieve this decoupling, in the ACL system your objects are represented -through object identity objects. Every time you want to retrieve the ACL for a -domain object, the ACL system will first create an object identity from your -domain object, and then pass this object identity to the ACL provider for -further processing. - - -Security Identities -~~~~~~~~~~~~~~~~~~~ - -This is analog to the object identity, but represents a user, or a role in -your application. Each role, or user has its own security identity. - - -Database Table Structure ------------------------- - -The default implementation uses five database tables as listed below. The -tables are ordered from least rows to most rows in a typical application: - -- *acl_security_identities*: This table records all security identities (SID) - which hold ACEs. The default implementation ships with two security - identities: ``RoleSecurityIdentity``, and ``UserSecurityIdentity`` -- *acl_classes*: This table maps class names to a unique id which can be - referenced from other tables. -- *acl_object_identities*: Each row in this table represents a single domain - object instance. -- *acl_object_identity_ancestors*: This table allows all the ancestors of - an ACL to be determined in a very efficient way. -- *acl_entries*: This table contains all ACEs. This is typically the table - with the most rows. It can contain tens of millions without significantly - impacting performance. - - -Scope of Access Control Entries -------------------------------- - -Access control entries can have different scopes in which they apply. In -Symfony2, there are basically two different scopes: - -- Class-Scope: These entries apply to all objects with the same class. -- Object-Scope: This was the scope solely used in the previous chapter, and - it only applies to one specific object. - -Sometimes, you will find the need to apply an ACE only to a specific field of -the object. Let's say you want the ID only to be viewable by an administrator, -but not by your customer service. To solve this common problem, two more sub-scopes -have been added: - -- Class-Field-Scope: These entries apply to all objects with the same class, - but only to a specific field of the objects. -- Object-Field-Scope: These entries apply to a specific object, and only to a - specific field of that object. - -Pre-Authorization Decisions ---------------------------- - -For pre-authorization decisions, that is decisions made before any secure method (or -secure action) is invoked, the proven AccessDecisionManager service is used. -The AccessDecisionManager is also used for reaching authorization decisions based -on roles. Just like roles, the ACL system adds several new attributes which may be -used to check for different permissions. - -Built-in Permission Map -~~~~~~~~~~~~~~~~~~~~~~~ - -+------------------+----------------------------+-----------------------------+ -| Attribute | Intended Meaning | Integer Bitmasks | -+==================+============================+=============================+ -| VIEW | Whether someone is allowed | VIEW, EDIT, OPERATOR, | -| | to view the domain object. | MASTER, or OWNER | -+------------------+----------------------------+-----------------------------+ -| EDIT | Whether someone is allowed | EDIT, OPERATOR, MASTER, | -| | to make changes to the | or OWNER | -| | domain object. | | -+------------------+----------------------------+-----------------------------+ -| CREATE | Whether someone is allowed | CREATE, OPERATOR, MASTER, | -| | to create the domain | or OWNER | -| | object. | | -+------------------+----------------------------+-----------------------------+ -| DELETE | Whether someone is allowed | DELETE, OPERATOR, MASTER, | -| | to delete the domain | or OWNER | -| | object. | | -+------------------+----------------------------+-----------------------------+ -| UNDELETE | Whether someone is allowed | UNDELETE, OPERATOR, MASTER, | -| | to restore a previously | or OWNER | -| | deleted domain object. | | -+------------------+----------------------------+-----------------------------+ -| OPERATOR | Whether someone is allowed | OPERATOR, MASTER, or OWNER | -| | to perform all of the above| | -| | actions. | | -+------------------+----------------------------+-----------------------------+ -| MASTER | Whether someone is allowed | MASTER, or OWNER | -| | to perform all of the above| | -| | actions, and in addition is| | -| | allowed to grant | | -| | any of the above | | -| | permissions to others. | | -+------------------+----------------------------+-----------------------------+ -| OWNER | Whether someone owns the | OWNER | -| | domain object. An owner can| | -| | perform any of the above | | -| | actions *and* grant master | | -| | and owner permissions. | | -+------------------+----------------------------+-----------------------------+ - -Permission Attributes vs. Permission Bitmasks -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Attributes are used by the AccessDecisionManager, just like roles. Often, these -attributes represent in fact an aggregate of integer bitmasks. Integer bitmasks on -the other hand, are used by the ACL system internally to efficiently store your -users' permissions in the database, and perform access checks using extremely -fast bitmask operations. - -Extensibility -~~~~~~~~~~~~~ - -The above permission map is by no means static, and theoretically could be -completely replaced at will. However, it should cover most problems you -encounter, and for interoperability with other bundles, you are encouraged to -stick to the meaning envisaged for them. - -Post Authorization Decisions ----------------------------- - -Post authorization decisions are made after a secure method has been invoked, -and typically involve the domain object which is returned by such a method. -After invocation providers also allow to modify, or filter the domain object -before it is returned. - -Due to current limitations of the PHP language, there are no -post-authorization capabilities build into the core Security component. -However, there is an experimental JMSSecurityExtraBundle_ which adds these -capabilities. See its documentation for further information on how this is -accomplished. - -Process for Reaching Authorization Decisions --------------------------------------------- - -The ACL class provides two methods for determining whether a security identity -has the required bitmasks, ``isGranted`` and ``isFieldGranted``. When the ACL -receives an authorization request through one of these methods, it delegates -this request to an implementation of PermissionGrantingStrategy. This allows -you to replace the way access decisions are reached without actually modifying -the ACL class itself. - -The PermissionGrantingStrategy first checks all your object-scope ACEs if none -is applicable, the class-scope ACEs will be checked, if none is applicable, -then the process will be repeated with the ACEs of the parent ACL. If no -parent ACL exists, an exception will be thrown. - -.. _JMSSecurityExtraBundle: https://github.com/schmittjoh/JMSSecurityExtraBundle diff --git a/cookbook/security/custom_authentication_provider.rst b/cookbook/security/custom_authentication_provider.rst deleted file mode 100644 index c528b69ae32..00000000000 --- a/cookbook/security/custom_authentication_provider.rst +++ /dev/null @@ -1,583 +0,0 @@ -.. index:: - single: Security; Custom authentication provider - -How to create a custom Authentication Provider -============================================== - -If you have read the chapter on :doc:`/book/security`, you understand the -distinction Symfony2 makes between authentication and authorization in the -implementation of security. This chapter discusses the core classes involved -in the authentication process, and how to implement a custom authentication -provider. Because authentication and authorization are separate concepts, -this extension will be user-provider agnostic, and will function with your -application's user providers, may they be based in memory, a database, or -wherever else you choose to store them. - -Meet WSSE ---------- - -The following chapter demonstrates how to create a custom authentication -provider for WSSE authentication. The security protocol for WSSE provides -several security benefits: - -1. Username / Password encryption -2. Safe guarding against replay attacks -3. No web server configuration required - -WSSE is very useful for the securing of web services, may they be SOAP or -REST. - -There is plenty of great documentation on `WSSE`_, but this article will -focus not on the security protocol, but rather the manner in which a custom -protocol can be added to your Symfony2 application. The basis of WSSE is -that a request header is checked for encrypted credentials, verified using -a timestamp and `nonce`_, and authenticated for the requested user using a -password digest. - -.. note:: - - WSSE also supports application key validation, which is useful for web - services, but is outside the scope of this chapter. - -The Token ---------- - -The role of the token in the Symfony2 security context is an important one. -A token represents the user authentication data present in the request. Once -a request is authenticated, the token retains the user's data, and delivers -this data across the security context. First, you'll create your token class. -This will allow the passing of all relevant information to your authentication -provider. - -.. code-block:: php - - // src/Acme/DemoBundle/Security/Authentication/Token/WsseUserToken.php - namespace Acme\DemoBundle\Security\Authentication\Token; - - use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; - - class WsseUserToken extends AbstractToken - { - public $created; - public $digest; - public $nonce; - - public function __construct(array $roles = array()) - { - parent::__construct($roles); - - // If the user has roles, consider it authenticated - $this->setAuthenticated(count($roles) > 0); - } - - public function getCredentials() - { - return ''; - } - } - -.. note:: - - The ``WsseUserToken`` class extends the security component's - :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\AbstractToken` - class, which provides basic token functionality. Implement the - :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface` - on any class to use as a token. - -The Listener ------------- - -Next, you need a listener to listen on the security context. The listener -is responsible for fielding requests to the firewall and calling the authentication -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 -set an authenticated token in the security context if successful. - -.. code-block:: php - - // src/Acme/DemoBundle/Security/Firewall/WsseListener.php - namespace Acme\DemoBundle\Security\Firewall; - - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\HttpKernel\Event\GetResponseEvent; - use Symfony\Component\Security\Http\Firewall\ListenerInterface; - use Symfony\Component\Security\Core\Exception\AuthenticationException; - use Symfony\Component\Security\Core\SecurityContextInterface; - use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; - use Acme\DemoBundle\Security\Authentication\Token\WsseUserToken; - - class WsseListener implements ListenerInterface - { - protected $securityContext; - protected $authenticationManager; - - public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager) - { - $this->securityContext = $securityContext; - $this->authenticationManager = $authenticationManager; - } - - public function handle(GetResponseEvent $event) - { - $request = $event->getRequest(); - - $wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/'; - if (!$request->headers->has('x-wsse') || 1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches)) { - return; - } - - $token = new WsseUserToken(); - $token->setUser($matches[1]); - - $token->digest = $matches[2]; - $token->nonce = $matches[3]; - $token->created = $matches[4]; - - try { - $authToken = $this->authenticationManager->authenticate($token); - - $this->securityContext->setToken($authToken); - } catch (AuthenticationException $failed) { - // ... you might log something here - - // To deny the authentication clear the token. This will redirect to the login page. - // $this->securityContext->setToken(null); - // return; - - // Deny authentication with a '403 Forbidden' HTTP response - $response = new Response(); - $response->setStatusCode(403); - $event->setResponse($response); - - } - } - } - -This listener checks the request for the expected `X-WSSE` header, matches -the value returned for the expected WSSE information, creates a token using -that information, and passes the token on to the authentication manager. If -the proper information is not provided, or the authentication manager throws -an :class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException`, -a 403 Response is returned. - -.. note:: - - A class not used above, the - :class:`Symfony\\Component\\Security\\Http\\Firewall\\AbstractAuthenticationListener` - class, is a very useful base class which provides commonly needed functionality - for security extensions. This includes maintaining the token in the session, - providing success / failure handlers, login form urls, and more. As WSSE - does not require maintaining authentication sessions or login forms, it - won't be used for this example. - -The Authentication Provider ---------------------------- - -The authentication provider will do the verification of the ``WsseUserToken``. -Namely, the provider will verify the ``Created`` header value is valid within -five minutes, the ``Nonce`` header value is unique within five minutes, and -the ``PasswordDigest`` header value matches with the user's password. - -.. code-block:: php - - // src/Acme/DemoBundle/Security/Authentication/Provider/WsseProvider.php - namespace Acme\DemoBundle\Security\Authentication\Provider; - - use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; - use Symfony\Component\Security\Core\User\UserProviderInterface; - use Symfony\Component\Security\Core\Exception\AuthenticationException; - use Symfony\Component\Security\Core\Exception\NonceExpiredException; - use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - use Acme\DemoBundle\Security\Authentication\Token\WsseUserToken; - - class WsseProvider implements AuthenticationProviderInterface - { - private $userProvider; - private $cacheDir; - - public function __construct(UserProviderInterface $userProvider, $cacheDir) - { - $this->userProvider = $userProvider; - $this->cacheDir = $cacheDir; - } - - public function authenticate(TokenInterface $token) - { - $user = $this->userProvider->loadUserByUsername($token->getUsername()); - - if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword())) { - $authenticatedToken = new WsseUserToken($user->getRoles()); - $authenticatedToken->setUser($user); - - return $authenticatedToken; - } - - throw new AuthenticationException('The WSSE authentication failed.'); - } - - protected function validateDigest($digest, $nonce, $created, $secret) - { - // Check created time is not in the future - if (strtotime($created) > time()) { - return false; - } - - // Expire timestamp after 5 minutes - if (time() - strtotime($created) > 300) { - return false; - } - - // Validate nonce is unique within 5 minutes - if (file_exists($this->cacheDir.'/'.$nonce) && file_get_contents($this->cacheDir.'/'.$nonce) + 300 > time()) { - throw new NonceExpiredException('Previously used nonce detected'); - } - file_put_contents($this->cacheDir.'/'.$nonce, time()); - - // Validate Secret - $expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true)); - - return $digest === $expected; - } - - public function supports(TokenInterface $token) - { - return $token instanceof WsseUserToken; - } - } - -.. note:: - - The :class:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface` - requires an ``authenticate`` method on the user token, and a ``supports`` - method, which tells the authentication manager whether or not to use this - provider for the given token. In the case of multiple providers, the - authentication manager will then move to the next provider in the list. - -The Factory ------------ - -You have created a custom token, custom listener, and custom provider. Now -you need to tie them all together. How do you make your provider available -to your security configuration? The answer is by using a ``factory``. A factory -is where you hook into the security component, telling it the name of your -provider and any configuration options available for it. First, you must -create a class which implements -:class:`Symfony\\Bundle\\SecurityBundle\\DependencyInjection\\Security\\Factory\\SecurityFactoryInterface`. - -.. code-block:: php - - // src/Acme/DemoBundle/DependencyInjection/Security/Factory/WsseFactory.php - namespace Acme\DemoBundle\DependencyInjection\Security\Factory; - - use Symfony\Component\DependencyInjection\ContainerBuilder; - use Symfony\Component\DependencyInjection\Reference; - use Symfony\Component\DependencyInjection\DefinitionDecorator; - use Symfony\Component\Config\Definition\Builder\NodeDefinition; - use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; - - class WsseFactory implements SecurityFactoryInterface - { - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) - { - $providerId = 'security.authentication.provider.wsse.'.$id; - $container - ->setDefinition($providerId, new DefinitionDecorator('wsse.security.authentication.provider')) - ->replaceArgument(0, new Reference($userProvider)) - ; - - $listenerId = 'security.authentication.listener.wsse.'.$id; - $listener = $container->setDefinition($listenerId, new DefinitionDecorator('wsse.security.authentication.listener')); - - return array($providerId, $listenerId, $defaultEntryPoint); - } - - public function getPosition() - { - return 'pre_auth'; - } - - public function getKey() - { - return 'wsse'; - } - - public function addConfiguration(NodeDefinition $node) - { - } - } - -The :class:`Symfony\\Bundle\\SecurityBundle\\DependencyInjection\\Security\\Factory\\SecurityFactoryInterface` -requires the following methods: - -* ``create`` method, which adds the listener and authentication provider - to the DI container for the appropriate security context; - -* ``getPosition`` method, which must be of type ``pre_auth``, ``form``, ``http``, - and ``remember_me`` and defines the position at which the provider is called; - -* ``getKey`` method which defines the configuration key used to reference - the provider; - -* ``addConfiguration`` method, which is used to define the configuration - options underneath the configuration key in your security configuration. - Setting configuration options are explained later in this chapter. - -.. note:: - - A class not used in this example, - :class:`Symfony\\Bundle\\SecurityBundle\\DependencyInjection\\Security\\Factory\\AbstractFactory`, - is a very useful base class which provides commonly needed functionality - for security factories. It may be useful when defining an authentication - provider of a different type. - -Now that you have created a factory class, the ``wsse`` key can be used as -a firewall in your security configuration. - -.. note:: - - You may be wondering "why do you need a special factory class to add listeners - and providers to the dependency injection container?". This is a very - good question. The reason is you can use your firewall multiple times, - to secure multiple parts of your application. Because of this, each - time your firewall is used, a new service is created in the DI container. - The factory is what creates these new services. - -Configuration -------------- - -It's time to see your authentication provider in action. You will need to -do a few things in order to make this work. The first thing is to add the -services above to the DI container. Your factory class above makes reference -to service ids that do not exist yet: ``wsse.security.authentication.provider`` and -``wsse.security.authentication.listener``. It's time to define those services. - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/DemoBundle/Resources/config/services.yml - services: - wsse.security.authentication.provider: - class: Acme\DemoBundle\Security\Authentication\Provider\WsseProvider - arguments: ["", "%kernel.cache_dir%/security/nonces"] - - wsse.security.authentication.listener: - class: Acme\DemoBundle\Security\Firewall\WsseListener - arguments: ["@security.context", "@security.authentication.manager"] - - - .. code-block:: xml - - - - - - - - %kernel.cache_dir%/security/nonces - - - - - - - - - - .. code-block:: php - - // src/Acme/DemoBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; - - $container->setDefinition('wsse.security.authentication.provider', - new Definition( - 'Acme\DemoBundle\Security\Authentication\Provider\WsseProvider', array( - '', - '%kernel.cache_dir%/security/nonces', - ) - ) - ); - - $container->setDefinition('wsse.security.authentication.listener', - new Definition( - 'Acme\DemoBundle\Security\Firewall\WsseListener', array( - new Reference('security.context'), - new Reference('security.authentication.manager'), - ) - ) - ); - -Now that your services are defined, tell your security context about your -factory in your bundle class: - -.. versionadded:: 2.1 - Before 2.1, the factory below was added via ``security.yml`` instead. - -.. code-block:: php - - // src/Acme/DemoBundle/AcmeDemoBundle.php - namespace Acme\DemoBundle; - - use Acme\DemoBundle\DependencyInjection\Security\Factory\WsseFactory; - use Symfony\Component\HttpKernel\Bundle\Bundle; - use Symfony\Component\DependencyInjection\ContainerBuilder; - - class AcmeDemoBundle extends Bundle - { - public function build(ContainerBuilder $container) - { - parent::build($container); - - $extension = $container->getExtension('security'); - $extension->addSecurityListenerFactory(new WsseFactory()); - } - } - -You are finished! You can now define parts of your app as under WSSE protection. - -.. configuration-block:: - - .. code-block:: yaml - - security: - firewalls: - wsse_secured: - pattern: /api/.* - wsse: true - - .. code-block:: xml - - - - - - - - .. code-block:: php - - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'wsse_secured' => array( - 'pattern' => '/api/.*', - 'wsse' => true, - ), - ), - )); - - -Congratulations! You have written your very own custom security authentication -provider! - -A Little Extra --------------- - -How about making your WSSE authentication provider a bit more exciting? The -possibilities are endless. Why don't you start by adding some sparkle -to that shine? - -Configuration -~~~~~~~~~~~~~ - -You can add custom options under the ``wsse`` key in your security configuration. -For instance, the time allowed before expiring the ``Created`` header item, -by default, is 5 minutes. Make this configurable, so different firewalls -can have different timeout lengths. - -You will first need to edit ``WsseFactory`` and define the new option in -the ``addConfiguration`` method. - -.. code-block:: php - - class WsseFactory implements SecurityFactoryInterface - { - // ... - - public function addConfiguration(NodeDefinition $node) - { - $node - ->children() - ->scalarNode('lifetime')->defaultValue(300) - ->end(); - } - } - -Now, in the ``create`` method of the factory, the ``$config`` argument will -contain a 'lifetime' key, set to 5 minutes (300 seconds) unless otherwise -set in the configuration. Pass this argument to your authentication provider -in order to put it to use. - -.. code-block:: php - - class WsseFactory implements SecurityFactoryInterface - { - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) - { - $providerId = 'security.authentication.provider.wsse.'.$id; - $container - ->setDefinition($providerId, - new DefinitionDecorator('wsse.security.authentication.provider')) - ->replaceArgument(0, new Reference($userProvider)) - ->replaceArgument(2, $config['lifetime']); - // ... - } - - // ... - } - -.. note:: - - You'll also need to add a third argument to the ``wsse.security.authentication.provider`` - service configuration, which can be blank, but will be filled in with - the lifetime in the factory. The ``WsseProvider`` class will also now - need to accept a third constructor argument - the lifetime - which it - should use instead of the hard-coded 300 seconds. These two steps are - not shown here. - -The lifetime of each wsse request is now configurable, and can be -set to any desirable value per firewall. - -.. configuration-block:: - - .. code-block:: yaml - - security: - firewalls: - wsse_secured: - pattern: /api/.* - wsse: { lifetime: 30 } - - .. code-block:: xml - - - - - - - - .. code-block:: php - - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'wsse_secured' => array( - 'pattern' => '/api/.*', - 'wsse' => array( - 'lifetime' => 30, - ), - ), - ), - )); - -The rest is up to you! Any relevant configuration items can be defined -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`: http://en.wikipedia.org/wiki/Cryptographic_nonce diff --git a/cookbook/security/custom_provider.rst b/cookbook/security/custom_provider.rst deleted file mode 100644 index 6d1dbfc51b2..00000000000 --- a/cookbook/security/custom_provider.rst +++ /dev/null @@ -1,344 +0,0 @@ -.. index:: - single: Security; User Provider - -How to create a custom User Provider -==================================== - -Part of Symfony's standard authentication process depends on "user providers". -When a user submits a username and password, the authentication layer asks -the configured user provider to return a user object for a given username. -Symfony then checks whether the password of this user is correct and generates -a security token so the user stays authenticated during the current session. -Out of the box, Symfony has an "in_memory" and an "entity" user provider. -In this entry you'll see how you can create your own user provider, which -could be useful if your users are accessed via a custom database, a file, -or - as shown in this example - a web service. - -Create a User Class -------------------- - -First, regardless of *where* your user data is coming from, you'll need to -create a ``User`` class that represents that data. The ``User`` can look -however you want and contain any data. The only requirement is that the -class implements :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. -The methods in this interface should therefore be defined in the custom user -class: :method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getRoles`, -:method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getPassword`, -:method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getSalt`, -:method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getUsername`, -:method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::eraseCredentials`. -It may also be useful to implement the -:class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface` interface, -which defines a method to check if the user is equal to the current user. This -interface requires an :method:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface::isEqualTo` -method. - -Let's see this in action:: - - // src/Acme/WebserviceUserBundle/Security/User/WebserviceUser.php - namespace Acme\WebserviceUserBundle\Security\User; - - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Security\Core\User\EquatableInterface; - - class WebserviceUser implements UserInterface, EquatableInterface - { - private $username; - private $password; - private $salt; - private $roles; - - public function __construct($username, $password, $salt, array $roles) - { - $this->username = $username; - $this->password = $password; - $this->salt = $salt; - $this->roles = $roles; - } - - public function getRoles() - { - return $this->roles; - } - - public function getPassword() - { - return $this->password; - } - - public function getSalt() - { - return $this->salt; - } - - public function getUsername() - { - return $this->username; - } - - public function eraseCredentials() - { - } - - public function isEqualTo(UserInterface $user) - { - if (!$user instanceof WebserviceUser) { - return false; - } - - if ($this->password !== $user->getPassword()) { - return false; - } - - if ($this->getSalt() !== $user->getSalt()) { - return false; - } - - if ($this->username !== $user->getUsername()) { - return false; - } - - return true; - } - } - -.. versionadded:: 2.1 - The ``EquatableInterface`` was added in Symfony 2.1. Use the ``equals()`` - method of the ``UserInterface`` in Symfony 2.0. - -If you have more information about your users - like a "first name" - then -you can add a ``firstName`` field to hold that data. - -Create a User Provider ----------------------- - -Now that you have a ``User`` class, you'll create a user provider, which will -grab user information from some web service, create a ``WebserviceUser`` object, -and populate it with data. - -The user provider is just a plain PHP class that has to implement the -:class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`, -which requires three methods to be defined: ``loadUserByUsername($username)``, -``refreshUser(UserInterface $user)``, and ``supportsClass($class)``. For -more details, see :class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`. - -Here's an example of how this might look:: - - // src/Acme/WebserviceUserBundle/Security/User/WebserviceUserProvider.php - namespace Acme\WebserviceUserBundle\Security\User; - - use Symfony\Component\Security\Core\User\UserProviderInterface; - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; - use Symfony\Component\Security\Core\Exception\UnsupportedUserException; - - class WebserviceUserProvider implements UserProviderInterface - { - public function loadUserByUsername($username) - { - // make a call to your webservice here - $userData = ... - // pretend it returns an array on success, false if there is no user - - if ($userData) { - $password = '...'; - - // ... - - return new WebserviceUser($username, $password, $salt, $roles); - } - - throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username)); - } - - public function refreshUser(UserInterface $user) - { - if (!$user instanceof WebserviceUser) { - throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user))); - } - - return $this->loadUserByUsername($user->getUsername()); - } - - public function supportsClass($class) - { - return $class === 'Acme\WebserviceUserBundle\Security\User\WebserviceUser'; - } - } - -Create a Service for the User Provider --------------------------------------- - -Now you make the user provider available as a service: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/WebserviceUserBundle/Resources/config/services.yml - parameters: - webservice_user_provider.class: Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider - - services: - webservice_user_provider: - class: "%webservice_user_provider.class%" - - .. code-block:: xml - - - - Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider - - - - - - - .. code-block:: php - - // src/Acme/WebserviceUserBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; - - $container->setParameter('webservice_user_provider.class', 'Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider'); - - $container->setDefinition('webservice_user_provider', new Definition('%webservice_user_provider.class%'); - -.. tip:: - - The real implementation of the user provider will probably have some - dependencies or configuration options or other services. Add these as - arguments in the service definition. - -.. note:: - - Make sure the services file is being imported. See :ref:`service-container-imports-directive` - for details. - -Modify ``security.yml`` ------------------------ - -Everything comes together in your security configuration. Add the user provider -to the list of providers in the "security" section. Choose a name for the user provider -(e.g. "webservice") and mention the id of the service you just defined. - -.. configuration-block:: - - .. code-block:: yaml - - // app/config/security.yml - security: - providers: - webservice: - id: webservice_user_provider - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'providers' => array( - 'webservice' => array( - 'id' => 'webservice_user_provider', - ), - ), - )); - -Symfony also needs to know how to encode passwords that are supplied by website -users, e.g. by filling in a login form. You can do this by adding a line to the -"encoders" section in your security configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - encoders: - Acme\WebserviceUserBundle\Security\User\WebserviceUser: sha512 - - .. code-block:: xml - - - - sha512 - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'encoders' => array( - 'Acme\WebserviceUserBundle\Security\User\WebserviceUser' => 'sha512', - ), - )); - -The value here should correspond with however the passwords were originally -encoded when creating your users (however those users were created). When -a user submits her password, the password is appended to the salt value and -then encoded using this algorithm before being compared to the hashed password -returned by your ``getPassword()`` method. Additionally, depending on your -options, the password may be encoded multiple times and encoded to base64. - -.. sidebar:: Specifics on how passwords are encoded - - Symfony uses a specific method to combine the salt and encode the password - before comparing it to your encoded password. If ``getSalt()`` returns - nothing, then the submitted password is simply encoded using the algorithm - you specify in ``security.yml``. If a salt *is* specified, then the following - value is created and *then* hashed via the algorithm: - - ``$password.'{'.$salt.'}';`` - - If your external users have their passwords salted via a different method, - then you'll need to do a bit more work so that Symfony properly encodes - the password. That is beyond the scope of this entry, but would include - sub-classing ``MessageDigestPasswordEncoder`` and overriding the ``mergePasswordAndSalt`` - method. - - Additionally, the hash, by default, is encoded multiple times and encoded - to base64. For specific details, see `MessageDigestPasswordEncoder`_. - To prevent this, configure it in your configuration file: - - .. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - encoders: - Acme\WebserviceUserBundle\Security\User\WebserviceUser: - algorithm: sha512 - encode_as_base64: false - iterations: 1 - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'encoders' => array( - 'Acme\WebserviceUserBundle\Security\User\WebserviceUser' => array( - 'algorithm' => 'sha512', - 'encode_as_base64' => false, - 'iterations' => 1, - ), - ), - )); - -.. _MessageDigestPasswordEncoder: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Encoder/MessageDigestPasswordEncoder.php diff --git a/cookbook/security/entity_provider.rst b/cookbook/security/entity_provider.rst deleted file mode 100644 index 54708d9b544..00000000000 --- a/cookbook/security/entity_provider.rst +++ /dev/null @@ -1,687 +0,0 @@ -.. index:: - single: Security; User provider - single: Security; Entity provider - -How to load Security Users from the Database (the Entity Provider) -================================================================== - -The security layer is one of the smartest tools of Symfony. It handles two -things: the authentication and the authorization processes. Although it may -seem difficult to understand how it works internally, the security system -is very flexible and allows you to integrate your application with any authentication -backend, like Active Directory, an OAuth server or a database. - -Introduction ------------- - -This article focuses on how to authenticate users against a database table -managed by a Doctrine entity class. The content of this cookbook entry is split -in three parts. The first part is about designing a Doctrine ``User`` entity -class and making it usable in the security layer of Symfony. The second part -describes how to easily authenticate a user with the Doctrine -:class:`Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider` object -bundled with the framework and some configuration. -Finally, the tutorial will demonstrate how to create a custom -:class:`Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider` object to -retrieve users from a database with custom conditions. - -This tutorial assumes there is a bootstrapped and loaded -``Acme\UserBundle`` bundle in the application kernel. - -The Data Model --------------- - -For the purpose of this cookbook, the ``AcmeUserBundle`` bundle contains a -``User`` entity class with the following fields: ``id``, ``username``, ``salt``, -``password``, ``email`` and ``isActive``. The ``isActive`` field tells whether -or not the user account is active. - -To make it shorter, the getter and setter methods for each have been removed to -focus on the most important methods that come from the -:class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. - -.. code-block:: php - - // src/Acme/UserBundle/Entity/User.php - namespace Acme\UserBundle\Entity; - - use Doctrine\ORM\Mapping as ORM; - use Symfony\Component\Security\Core\User\UserInterface; - - /** - * Acme\UserBundle\Entity\User - * - * @ORM\Table(name="acme_users") - * @ORM\Entity(repositoryClass="Acme\UserBundle\Entity\UserRepository") - */ - class User implements UserInterface, \Serializable - { - /** - * @ORM\Column(type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - */ - private $id; - - /** - * @ORM\Column(type="string", length=25, unique=true) - */ - private $username; - - /** - * @ORM\Column(type="string", length=32) - */ - private $salt; - - /** - * @ORM\Column(type="string", length=40) - */ - private $password; - - /** - * @ORM\Column(type="string", length=60, unique=true) - */ - private $email; - - /** - * @ORM\Column(name="is_active", type="boolean") - */ - private $isActive; - - public function __construct() - { - $this->isActive = true; - $this->salt = md5(uniqid(null, true)); - } - - /** - * @inheritDoc - */ - public function getUsername() - { - return $this->username; - } - - /** - * @inheritDoc - */ - public function getSalt() - { - return $this->salt; - } - - /** - * @inheritDoc - */ - public function getPassword() - { - return $this->password; - } - - /** - * @inheritDoc - */ - public function getRoles() - { - return array('ROLE_USER'); - } - - /** - * @inheritDoc - */ - public function eraseCredentials() - { - } - - /** - * @see \Serializable::serialize() - */ - public function serialize() - { - return serialize(array( - $this->id, - )); - } - - /** - * @see \Serializable::unserialize() - */ - public function unserialize($serialized) - { - list ( - $this->id, - ) = unserialize($serialized); - } - } - -In order to use an instance of the ``AcmeUserBundle:User`` class in the Symfony -security layer, the entity class must implement the -:class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. This -interface forces the class to implement the five following methods: - -* ``getRoles()``, -* ``getPassword()``, -* ``getSalt()``, -* ``getUsername()``, -* ``eraseCredentials()`` - -For more details on each of these, see :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. - -.. versionadded:: 2.1 - In Symfony 2.1, the ``equals`` method was removed from ``UserInterface``. - If you need to override the default implementation of comparison logic, - implement the new :class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface` - interface and implement the ``isEqualTo`` method. - -.. code-block:: php - - // src/Acme/UserBundle/Entity/User.php - - namespace Acme\UserBundle\Entity; - - use Symfony\Component\Security\Core\User\EquatableInterface; - - // ... - - public function isEqualTo(UserInterface $user) - { - return $this->id === $user->getId(); - } - -.. note:: - - The :phpclass:`Serializable` interface and its ``serialize`` and ``unserialize`` - methods have been added to allow the ``User`` class to be serialized - to the session. This may or may not be needed depending on your setup, - but it's probably a good idea. Only the ``id`` needs to be serialized, - because the :method:`Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider::refreshUser` - method reloads the user on each request by using the ``id``. - -Below is an export of my ``User`` table from MySQL. For details on how to -create user records and encode their password, see :ref:`book-security-encoding-user-password`. - -.. code-block:: bash - - $ mysql> select * from user; - +----+----------+----------------------------------+------------------------------------------+--------------------+-----------+ - | id | username | salt | password | email | is_active | - +----+----------+----------------------------------+------------------------------------------+--------------------+-----------+ - | 1 | hhamon | 7308e59b97f6957fb42d66f894793079 | 09610f61637408828a35d7debee5b38a8350eebe | hhamon@example.com | 1 | - | 2 | jsmith | ce617a6cca9126bf4036ca0c02e82dee | 8390105917f3a3d533815250ed7c64b4594d7ebf | jsmith@example.com | 1 | - | 3 | maxime | cd01749bb995dc658fa56ed45458d807 | 9764731e5f7fb944de5fd8efad4949b995b72a3c | maxime@example.com | 0 | - | 4 | donald | 6683c2bfd90c0426088402930cadd0f8 | 5c3bcec385f59edcc04490d1db95fdb8673bf612 | donald@example.com | 1 | - +----+----------+----------------------------------+------------------------------------------+--------------------+-----------+ - 4 rows in set (0.00 sec) - -The database now contains four users with different usernames, emails and -statuses. The next part will focus on how to authenticate one of these users -thanks to the Doctrine entity user provider and a couple of lines of -configuration. - -Authenticating Someone against a Database ------------------------------------------ - -Authenticating a Doctrine user against the database with the Symfony security -layer is a piece of cake. Everything resides in the configuration of the -:doc:`SecurityBundle` stored in the -``app/config/security.yml`` file. - -Below is an example of configuration where the user will enter his/her -username and password via HTTP basic authentication. That information will -then be checked against your User entity records in the database: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - encoders: - Acme\UserBundle\Entity\User: - algorithm: sha1 - encode_as_base64: false - iterations: 1 - - role_hierarchy: - ROLE_ADMIN: ROLE_USER - ROLE_SUPER_ADMIN: [ ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ] - - providers: - administrators: - entity: { class: AcmeUserBundle:User, property: username } - - firewalls: - admin_area: - pattern: ^/admin - http_basic: ~ - - access_control: - - { path: ^/admin, roles: ROLE_ADMIN } - - .. code-block:: xml - - - - - - ROLE_USER - ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'encoders' => array( - 'Acme\UserBundle\Entity\User' => array( - 'algorithm' => 'sha1', - 'encode_as_base64' => false, - 'iterations' => 1, - ), - ), - 'role_hierarchy' => array( - 'ROLE_ADMIN' => 'ROLE_USER', - 'ROLE_SUPER_ADMIN' => array('ROLE_USER', 'ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH'), - ), - 'providers' => array( - 'administrator' => array( - 'entity' => array( - 'class' => 'AcmeUserBundle:User', - 'property' => 'username', - ), - ), - ), - 'firewalls' => array( - 'admin_area' => array( - 'pattern' => '^/admin', - 'http_basic' => null, - ), - ), - 'access_control' => array( - array('path' => '^/admin', 'role' => 'ROLE_ADMIN'), - ), - )); - -The ``encoders`` section associates the ``sha1`` password encoder to the entity -class. This means that Symfony will expect the password that's stored in -the database to be encoded using this algorithm. For details on how to create -a new User object with a properly encoded password, see the -:ref:`book-security-encoding-user-password` section of the security chapter. - -The ``providers`` section defines an ``administrators`` user provider. A -user provider is a "source" of where users are loaded during authentication. -In this case, the ``entity`` keyword means that Symfony will use the Doctrine -entity user provider to load User entity objects from the database by using -the ``username`` unique field. In other words, this tells Symfony how to -fetch the user from the database before checking the password validity. - -This code and configuration works but it's not enough to secure the application -for **active** users. As of now, you can still authenticate with ``maxime``. The -next section explains how to forbid non active users. - -Forbid non Active Users ------------------------ - -The easiest way to exclude non active users is to implement the -:class:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface` -interface that takes care of checking the user's account status. -The :class:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface` -extends the :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface` -interface, so you just need to switch to the new interface in the ``AcmeUserBundle:User`` -entity class to benefit from simple and advanced authentication behaviors. - -The :class:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface` -interface adds four extra methods to validate the account status: - -* ``isAccountNonExpired()`` checks whether the user's account has expired, -* ``isAccountNonLocked()`` checks whether the user is locked, -* ``isCredentialsNonExpired()`` checks whether the user's credentials (password) - has expired, -* ``isEnabled()`` checks whether the user is enabled. - -For this example, the first three methods will return ``true`` whereas the -``isEnabled()`` method will return the boolean value in the ``isActive`` field. - -.. code-block:: php - - // src/Acme/UserBundle/Entity/User.php - namespace Acme\UserBundle\Entity; - - // ... - use Symfony\Component\Security\Core\User\AdvancedUserInterface; - - class User implements AdvancedUserInterface - { - // ... - - public function isAccountNonExpired() - { - return true; - } - - public function isAccountNonLocked() - { - return true; - } - - public function isCredentialsNonExpired() - { - return true; - } - - public function isEnabled() - { - return $this->isActive; - } - } - -If you try to authenticate as ``maxime``, the access is now forbidden as this -user does not have an enabled account. The next session will focus on how -to write a custom entity provider to authenticate a user with his username -or his email address. - -Authenticating Someone with a Custom Entity Provider ----------------------------------------------------- - -The next step is to allow a user to authenticate with his username or his email -address as they are both unique in the database. Unfortunately, the native -entity provider is only able to handle a single property to fetch the user from -the database. - -To accomplish this, create a custom entity provider that looks for a user -whose username *or* email field matches the submitted login username. -The good news is that a Doctrine repository object can act as an entity user -provider if it implements the -:class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`. This -interface comes with three methods to implement: ``loadUserByUsername($username)``, -``refreshUser(UserInterface $user)``, and ``supportsClass($class)``. For -more details, see :class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`. - -The code below shows the implementation of the -:class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface` in the -``UserRepository`` class:: - - // src/Acme/UserBundle/Entity/UserRepository.php - namespace Acme\UserBundle\Entity; - - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Security\Core\User\UserProviderInterface; - use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; - use Symfony\Component\Security\Core\Exception\UnsupportedUserException; - use Doctrine\ORM\EntityRepository; - use Doctrine\ORM\NoResultException; - - class UserRepository extends EntityRepository implements UserProviderInterface - { - public function loadUserByUsername($username) - { - $q = $this - ->createQueryBuilder('u') - ->where('u.username = :username OR u.email = :email') - ->setParameter('username', $username) - ->setParameter('email', $username) - ->getQuery() - ; - - try { - // The Query::getSingleResult() method throws an exception - // if there is no record matching the criteria. - $user = $q->getSingleResult(); - } catch (NoResultException $e) { - $message = sprintf( - 'Unable to find an active admin AcmeUserBundle:User object identified by "%s".', - $username - ); - throw new UsernameNotFoundException($message, 0, $e); - } - - return $user; - } - - public function refreshUser(UserInterface $user) - { - $class = get_class($user); - if (!$this->supportsClass($class)) { - throw new UnsupportedUserException( - sprintf( - 'Instances of "%s" are not supported.', - $class - ) - ); - } - - return $this->find($user->getId()); - } - - public function supportsClass($class) - { - return $this->getEntityName() === $class - || is_subclass_of($class, $this->getEntityName()); - } - } - -To finish the implementation, the configuration of the security layer must be -changed to tell Symfony to use the new custom entity provider instead of the -generic Doctrine entity provider. It's trivial to achieve by removing the -``property`` field in the ``security.providers.administrators.entity`` section -of the ``security.yml`` file. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - providers: - administrators: - entity: { class: AcmeUserBundle:User } - # ... - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - ..., - 'providers' => array( - 'administrator' => array( - 'entity' => array( - 'class' => 'AcmeUserBundle:User', - ), - ), - ), - ..., - )); - -By doing this, the security layer will use an instance of ``UserRepository`` and -call its ``loadUserByUsername()`` method to fetch a user from the database -whether he filled in his username or email address. - -Managing Roles in the Database ------------------------------- - -The end of this tutorial focuses on how to store and retrieve a list of roles -from the database. As mentioned previously, when your user is loaded, its -``getRoles()`` method returns the array of security roles that should be -assigned to the user. You can load this data from anywhere - a hardcoded -list used for all users (e.g. ``array('ROLE_USER')``), a Doctrine array -property called ``roles``, or via a Doctrine relationship, as you'll learn -about in this section. - -.. caution:: - - In a typical setup, you should always return at least 1 role from the ``getRoles()`` - method. By convention, a role called ``ROLE_USER`` is usually returned. - If you fail to return any roles, it may appear as if your user isn't - authenticated at all. - -In this example, the ``AcmeUserBundle:User`` entity class defines a -many-to-many relationship with a ``AcmeUserBundle:Group`` entity class. A user -can be related to several groups and a group can be composed of one or -more users. As a group is also a role, the previous ``getRoles()`` method now -returns the list of related groups:: - - // src/Acme/UserBundle/Entity/User.php - namespace Acme\UserBundle\Entity; - - use Doctrine\Common\Collections\ArrayCollection; - // ... - - class User implements AdvancedUserInterface, \Serializable - { - /** - * @ORM\ManyToMany(targetEntity="Group", inversedBy="users") - * - */ - private $groups; - - public function __construct() - { - $this->groups = new ArrayCollection(); - } - - // ... - - public function getRoles() - { - return $this->groups->toArray(); - } - - /** - * @see \Serializable::serialize() - */ - public function serialize() - { - return serialize(array( - $this->id, - )); - } - - /** - * @see \Serializable::unserialize() - */ - public function unserialize($serialized) - { - list ( - $this->id, - ) = unserialize($serialized); - } - } - -The ``AcmeUserBundle:Group`` entity class defines three table fields (``id``, -``name`` and ``role``). The unique ``role`` field contains the role name used by -the Symfony security layer to secure parts of the application. The most -important thing to notice is that the ``AcmeUserBundle:Group`` entity class -implements the :class:`Symfony\\Component\\Security\\Core\\Role\\RoleInterface` -that forces it to have a ``getRole()`` method:: - - // src/Acme/Bundle/UserBundle/Entity/Group.php - namespace Acme\UserBundle\Entity; - - use Symfony\Component\Security\Core\Role\RoleInterface; - use Doctrine\Common\Collections\ArrayCollection; - use Doctrine\ORM\Mapping as ORM; - - /** - * @ORM\Table(name="acme_groups") - * @ORM\Entity() - */ - class Group implements RoleInterface - { - /** - * @ORM\Column(name="id", type="integer") - * @ORM\Id() - * @ORM\GeneratedValue(strategy="AUTO") - */ - private $id; - - /** - * @ORM\Column(name="name", type="string", length=30) - */ - private $name; - - /** - * @ORM\Column(name="role", type="string", length=20, unique=true) - */ - private $role; - - /** - * @ORM\ManyToMany(targetEntity="User", mappedBy="groups") - */ - private $users; - - public function __construct() - { - $this->users = new ArrayCollection(); - } - - // ... getters and setters for each property - - /** - * @see RoleInterface - */ - public function getRole() - { - return $this->role; - } - } - -To improve performances and avoid lazy loading of groups when retrieving a user -from the custom entity provider, the best solution is to join the groups -relationship in the ``UserRepository::loadUserByUsername()`` method. This will -fetch the user and his associated roles / groups with a single query:: - - // src/Acme/UserBundle/Entity/UserRepository.php - namespace Acme\UserBundle\Entity; - - // ... - - class UserRepository extends EntityRepository implements UserProviderInterface - { - public function loadUserByUsername($username) - { - $q = $this - ->createQueryBuilder('u') - ->select('u, g') - ->leftJoin('u.groups', 'g') - ->where('u.username = :username OR u.email = :email') - ->setParameter('username', $username) - ->setParameter('email', $username) - ->getQuery(); - - // ... - } - - // ... - } - -The ``QueryBuilder::leftJoin()`` method joins and fetches related groups from -the ``AcmeUserBundle:User`` model class when a user is retrieved with his email -address or username. diff --git a/cookbook/security/force_https.rst b/cookbook/security/force_https.rst deleted file mode 100644 index 46736abf430..00000000000 --- a/cookbook/security/force_https.rst +++ /dev/null @@ -1,70 +0,0 @@ -.. index:: - single: Security; Force HTTPS - -How to force HTTPS or HTTP for Different URLs -============================================= - -You can force areas of your site to use the ``HTTPS`` protocol in the security -config. This is done through the ``access_control`` rules using the ``requires_channel`` -option. For example, if you want to force all URLs starting with ``/secure`` -to use ``HTTPS`` then you could use the following configuration: - -.. configuration-block:: - - .. code-block:: yaml - - access_control: - - path: ^/secure - roles: ROLE_ADMIN - requires_channel: https - - .. code-block:: xml - - - - - - .. code-block:: php - - 'access_control' => array( - array( - 'path' => '^/secure', - 'role' => 'ROLE_ADMIN', - 'requires_channel' => 'https', - ), - ), - -The login form itself needs to allow anonymous access, otherwise users will -be unable to authenticate. To force it to use ``HTTPS`` you can still use -``access_control`` rules by using the ``IS_AUTHENTICATED_ANONYMOUSLY`` -role: - -.. configuration-block:: - - .. code-block:: yaml - - access_control: - - path: ^/login - roles: IS_AUTHENTICATED_ANONYMOUSLY - requires_channel: https - - .. code-block:: xml - - - - - - .. code-block:: php - - 'access_control' => array( - array( - 'path' => '^/login', - 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', - 'requires_channel' => 'https', - ), - ), - -It is also possible to specify using ``HTTPS`` in the routing configuration -see :doc:`/cookbook/routing/scheme` for more details. diff --git a/cookbook/security/form_login.rst b/cookbook/security/form_login.rst deleted file mode 100644 index 37b9f38fce1..00000000000 --- a/cookbook/security/form_login.rst +++ /dev/null @@ -1,320 +0,0 @@ -.. index:: - single: Security; Customizing form login - -How to customize your Form Login -================================ - -Using a :ref:`form login` for authentication is -a common, and flexible, method for handling authentication in Symfony2. Pretty -much every aspect of the form login can be customized. The full, default -configuration is shown in the next section. - -Form Login Configuration Reference ----------------------------------- - -To see the full form login configuration reference, see -:doc:`/reference/configuration/security`. Some of the more interesting options -are explained below. - -Redirecting after Success -------------------------- - -You can change where the login form redirects after a successful login using -the various config options. By default the form will redirect to the URL the -user requested (i.e. the URL which triggered the login form being shown). -For example, if the user requested ``http://www.example.com/admin/post/18/edit``, -then after she successfully logs in, she will eventually be sent back to -``http://www.example.com/admin/post/18/edit``. -This is done by storing the requested URL in the session. -If no URL is present in the session (perhaps the user went -directly to the login page), then the user is redirected to the default page, -which is ``/`` (i.e. the homepage) by default. You can change this behavior -in several ways. - -.. note:: - - As mentioned, by default the user is redirected back to the page he originally - requested. Sometimes, this can cause problems, like if a background AJAX - request "appears" to be the last visited URL, causing the user to be - redirected there. For information on controlling this behavior, see - :doc:`/cookbook/security/target_path`. - -Changing the Default Page -~~~~~~~~~~~~~~~~~~~~~~~~~ - -First, the default page can be set (i.e. the page the user is redirected to -if no previous page was stored in the session). To set it to the -``default_security_target`` route use the following config: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - main: - form_login: - # ... - default_target_path: default_security_target - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'main' => array( - // ... - - 'form_login' => array( - // ... - 'default_target_path' => 'default_security_target', - ), - ), - ), - )); - -Now, when no URL is set in the session, users will be sent to the -``default_security_target`` route. - -Always Redirect to the Default Page -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can make it so that users are always redirected to the default page regardless -of what URL they had requested previously by setting the -``always_use_default_target_path`` option to true: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - main: - form_login: - # ... - always_use_default_target_path: true - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'main' => array( - // ... - - 'form_login' => array( - // ... - 'always_use_default_target_path' => true, - ), - ), - ), - )); - -Using the Referring URL -~~~~~~~~~~~~~~~~~~~~~~~ - -In case no previous URL was stored in the session, you may wish to try using -the ``HTTP_REFERER`` instead, as this will often be the same. You can do -this by setting ``use_referer`` to true (it defaults to false): - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - main: - form_login: - # ... - use_referer: true - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'main' => array( - // ... - - 'form_login' => array( - // ... - 'use_referer' => true, - ), - ), - ), - )); - -.. versionadded:: 2.1 - As of 2.1, if the referer is equal to the ``login_path`` option, the - user will be redirected to the ``default_target_path``. - -Control the Redirect URL from inside the Form -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can also override where the user is redirected to via the form itself by -including a hidden field with the name ``_target_path``. For example, to -redirect to the URL defined by some ``account`` route, use the following: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #} - {% if error %} -
    {{ error.message }}
    - {% endif %} - -
    - - - - - - - - - -
    - - .. code-block:: html+php - - - -
    getMessage() ?>
    - - -
    - - - - - - - - - -
    - -Now, the user will be redirected to the value of the hidden form field. The -value attribute can be a relative path, absolute URL, or a route name. You -can even change the name of the hidden form field by changing the ``target_path_parameter`` -option to another value. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - main: - form_login: - target_path_parameter: redirect_url - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'main' => array( - 'form_login' => array( - 'target_path_parameter' => redirect_url, - ), - ), - ), - )); - -Redirecting on Login Failure -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In addition to redirecting the user after a successful login, you can also set -the URL that the user should be redirected to after a failed login (e.g. an -invalid username or password was submitted). By default, the user is redirected -back to the login form itself. You can set this to a different route (e.g. -``login_failure``) with the following config: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - main: - form_login: - # ... - failure_path: login_failure - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'main' => array( - // ... - - 'form_login' => array( - // ... - 'failure_path' => 'login_failure', - ), - ), - ), - )); diff --git a/cookbook/security/index.rst b/cookbook/security/index.rst deleted file mode 100644 index a8edbdc4317..00000000000 --- a/cookbook/security/index.rst +++ /dev/null @@ -1,17 +0,0 @@ -Security -======== - -.. toctree:: - :maxdepth: 2 - - entity_provider - remember_me - voters - acl - acl_advanced - force_https - form_login - securing_services - custom_provider - custom_authentication_provider - target_path diff --git a/cookbook/security/remember_me.rst b/cookbook/security/remember_me.rst deleted file mode 100644 index 7bb3fb5f786..00000000000 --- a/cookbook/security/remember_me.rst +++ /dev/null @@ -1,212 +0,0 @@ -.. index:: - single: Security; "Remember me" - -How to add "Remember Me" Login Functionality -============================================ - -Once a user is authenticated, their credentials are typically stored in the -session. This means that when the session ends they will be logged out and -have to provide their login details again next time they wish to access the -application. You can allow users to choose to stay logged in for longer than -the session lasts using a cookie with the ``remember_me`` firewall option. -The firewall needs to have a secret key configured, which is used to encrypt -the cookie's content. It also has several options with default values which -are shown here: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - firewalls: - main: - remember_me: - key: "%secret%" - lifetime: 31536000 # 365 days in seconds - path: / - domain: ~ # Defaults to the current domain from $_SERVER - - .. code-block:: xml - - - - - - path = "/" - domain = "" - /> - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'main' => array( - 'remember_me' => array( - 'key' => '%secret%', - 'lifetime' => 31536000, // 365 days in seconds - 'path' => '/', - 'domain' => '', // Defaults to the current domain from $_SERVER - ), - ), - ), - )); - -It's a good idea to provide the user with the option to use or not use the -remember me functionality, as it will not always be appropriate. The usual -way of doing this is to add a checkbox to the login form. By giving the checkbox -the name ``_remember_me``, the cookie will automatically be set when the checkbox -is checked and the user successfully logs in. So, your specific login form -might ultimately look like this: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #} - {% if error %} -
    {{ error.message }}
    - {% endif %} - -
    - - - - - - - - - - -
    - - .. code-block:: html+php - - - -
    getMessage() ?>
    - - -
    - - - - - - - - - - -
    - -The user will then automatically be logged in on subsequent visits while -the cookie remains valid. - -Forcing the User to Re-authenticate before accessing certain Resources ----------------------------------------------------------------------- - -When the user returns to your site, he/she is authenticated automatically based -on the information stored in the remember me cookie. This allows the user -to access protected resources as if the user had actually authenticated upon -visiting the site. - -In some cases, however, you may want to force the user to actually re-authenticate -before accessing certain resources. For example, you might allow "remember me" -users to see basic account information, but then require them to actually -re-authenticate before modifying that information. - -The security component provides an easy way to do this. In addition to roles -explicitly assigned to them, users are automatically given one of the following -roles depending on how they are authenticated: - -* ``IS_AUTHENTICATED_ANONYMOUSLY`` - automatically assigned to a user who is - in a firewall protected part of the site but who has not actually logged in. - This is only possible if anonymous access has been allowed. - -* ``IS_AUTHENTICATED_REMEMBERED`` - automatically assigned to a user who - was authenticated via a remember me cookie. - -* ``IS_AUTHENTICATED_FULLY`` - automatically assigned to a user that has - provided their login details during the current session. - -You can use these to control access beyond the explicitly assigned roles. - -.. note:: - - If you have the ``IS_AUTHENTICATED_REMEMBERED`` role, then you also - have the ``IS_AUTHENTICATED_ANONYMOUSLY`` role. If you have the ``IS_AUTHENTICATED_FULLY`` - role, then you also have the other two roles. In other words, these roles - represent three levels of increasing "strength" of authentication. - -You can use these additional roles for finer grained control over access to -parts of a site. For example, you may want your user to be able to view their -account at ``/account`` when authenticated by cookie but to have to provide -their login details to be able to edit the account details. You can do this -by securing specific controller actions using these roles. The edit action -in the controller could be secured using the service context. - -In the following example, the action is only allowed if the user has the -``IS_AUTHENTICATED_FULLY`` role. - -.. code-block:: php - - // ... - use Symfony\Component\Security\Core\Exception\AccessDeniedException - - public function editAction() - { - if (false === $this->get('security.context')->isGranted( - 'IS_AUTHENTICATED_FULLY' - )) { - throw new AccessDeniedException(); - } - - // ... - } - -You can also choose to install and use the optional JMSSecurityExtraBundle_, -which can secure your controller using annotations: - -.. code-block:: php - - use JMS\SecurityExtraBundle\Annotation\Secure; - - /** - * @Secure(roles="IS_AUTHENTICATED_FULLY") - */ - public function editAction($name) - { - // ... - } - -.. tip:: - - If you also had an access control in your security configuration that - required the user to have a ``ROLE_USER`` role in order to access any - of the account area, then you'd have the following situation: - - * If a non-authenticated (or anonymously authenticated user) tries to - access the account area, the user will be asked to authenticate. - - * Once the user has entered his username and password, assuming the - user receives the ``ROLE_USER`` role per your configuration, the user - will have the ``IS_AUTHENTICATED_FULLY`` role and be able to access - any page in the account section, including the ``editAction`` controller. - - * If the user's session ends, when the user returns to the site, he will - be able to access every account page - except for the edit page - without - being forced to re-authenticate. However, when he tries to access the - ``editAction`` controller, he will be forced to re-authenticate, since - he is not, yet, fully authenticated. - -For more information on securing services or methods in this way, -see :doc:`/cookbook/security/securing_services`. - -.. _JMSSecurityExtraBundle: https://github.com/schmittjoh/JMSSecurityExtraBundle diff --git a/cookbook/security/securing_services.rst b/cookbook/security/securing_services.rst deleted file mode 100644 index 810b83fc9b6..00000000000 --- a/cookbook/security/securing_services.rst +++ /dev/null @@ -1,271 +0,0 @@ -.. index:: - single: Security; Securing any service - single: Security; Securing any method - -How to secure any Service or Method in your Application -======================================================= - -In the security chapter, you can see how to :ref:`secure a controller` -by requesting the ``security.context`` service from the Service Container -and checking the current user's role:: - - // ... - use Symfony\Component\Security\Core\Exception\AccessDeniedException; - - public function helloAction($name) - { - if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) { - throw new AccessDeniedException(); - } - - // ... - } - -You can also secure *any* service in a similar way by injecting the ``security.context`` -service into it. For a general introduction to injecting dependencies into -services see the :doc:`/book/service_container` chapter of the book. For -example, suppose you have a ``NewsletterManager`` class that sends out emails -and you want to restrict its use to only users who have some ``ROLE_NEWSLETTER_ADMIN`` -role. Before you add security, the class looks something like this: - -.. code-block:: php - - // src/Acme/HelloBundle/Newsletter/NewsletterManager.php - namespace Acme\HelloBundle\Newsletter; - - class NewsletterManager - { - - public function sendNewsletter() - { - // ... where you actually do the work - } - - // ... - } - -Your goal is to check the user's role when the ``sendNewsletter()`` method is -called. The first step towards this is to inject the ``security.context`` -service into the object. Since it won't make sense *not* to perform the security -check, this is an ideal candidate for constructor injection, which guarantees -that the security context object will be available inside the ``NewsletterManager`` -class:: - - namespace Acme\HelloBundle\Newsletter; - - use Symfony\Component\Security\Core\SecurityContextInterface; - - class NewsletterManager - { - protected $securityContext; - - public function __construct(SecurityContextInterface $securityContext) - { - $this->securityContext = $securityContext; - } - - // ... - } - -Then in your service configuration, you can inject the service: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/HelloBundle/Resources/config/services.yml - parameters: - newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager - - services: - newsletter_manager: - class: "%newsletter_manager.class%" - arguments: ["@security.context"] - - .. code-block:: xml - - - - Acme\HelloBundle\Newsletter\NewsletterManager - - - - - - - - - .. code-block:: php - - // src/Acme/HelloBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; - - $container->setParameter('newsletter_manager.class', 'Acme\HelloBundle\Newsletter\NewsletterManager'); - - $container->setDefinition('newsletter_manager', new Definition( - '%newsletter_manager.class%', - array(new Reference('security.context')) - )); - -The injected service can then be used to perform the security check when the -``sendNewsletter()`` method is called:: - - namespace Acme\HelloBundle\Newsletter; - - use Symfony\Component\Security\Core\Exception\AccessDeniedException; - use Symfony\Component\Security\Core\SecurityContextInterface; - // ... - - class NewsletterManager - { - protected $securityContext; - - public function __construct(SecurityContextInterface $securityContext) - { - $this->securityContext = $securityContext; - } - - public function sendNewsletter() - { - if (false === $this->securityContext->isGranted('ROLE_NEWSLETTER_ADMIN')) { - throw new AccessDeniedException(); - } - - // ... - } - - // ... - } - -If the current user does not have the ``ROLE_NEWSLETTER_ADMIN``, they will -be prompted to log in. - -Securing Methods Using Annotations ----------------------------------- - -You can also secure method calls in any service with annotations by using the -optional `JMSSecurityExtraBundle`_ bundle. This bundle is included in the -Symfony2 Standard Distribution. - -To enable the annotations functionality, :ref:`tag` -the service you want to secure with the ``security.secure_service`` tag -(you can also automatically enable this functionality for all services, see -the :ref:`sidebar` below): - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/HelloBundle/Resources/config/services.yml - # ... - - services: - newsletter_manager: - # ... - tags: - - { name: security.secure_service } - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - // src/Acme/HelloBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; - - $definition = new Definition( - '%newsletter_manager.class%', - array(new Reference('security.context')) - )); - $definition->addTag('security.secure_service'); - $container->setDefinition('newsletter_manager', $definition); - -You can then achieve the same results as above using an annotation:: - - namespace Acme\HelloBundle\Newsletter; - - use JMS\SecurityExtraBundle\Annotation\Secure; - // ... - - class NewsletterManager - { - - /** - * @Secure(roles="ROLE_NEWSLETTER_ADMIN") - */ - public function sendNewsletter() - { - // ... - } - - // ... - } - -.. note:: - - The annotations work because a proxy class is created for your class - which performs the security checks. This means that, whilst you can use - annotations on public and protected methods, you cannot use them with - private methods or methods marked final. - -The ``JMSSecurityExtraBundle`` also allows you to secure the parameters and return -values of methods. For more information, see the `JMSSecurityExtraBundle`_ -documentation. - -.. _securing-services-annotations-sidebar: - -.. sidebar:: Activating the Annotations Functionality for all Services - - When securing the method of a service (as shown above), you can either - tag each service individually, or activate the functionality for *all* - services at once. To do so, set the ``secure_all_services`` configuration - option to true: - - .. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - jms_security_extra: - # ... - secure_all_services: true - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('jms_security_extra', array( - // ... - - 'secure_all_services' => true, - )); - - The disadvantage of this method is that, if activated, the initial page - load may be very slow depending on how many services you have defined. - -.. _`JMSSecurityExtraBundle`: https://github.com/schmittjoh/JMSSecurityExtraBundle diff --git a/cookbook/security/target_path.rst b/cookbook/security/target_path.rst deleted file mode 100644 index 4d3113f4639..00000000000 --- a/cookbook/security/target_path.rst +++ /dev/null @@ -1,69 +0,0 @@ -.. index:: - single: Security; Target redirect path - -How to change the Default Target Path Behavior -============================================== - -By default, the security component retains the information of the last request -URI in a session variable named ``_security.main.target_path`` (with ``main`` being -the name of the firewall, defined in ``security.yml``). Upon a successful -login, the user is redirected to this path, as to help her continue from the -last known page she visited. - -On some occasions, this is unexpected. For example when the last request -URI was an HTTP POST against a route which is configured to allow only a POST -method, the user is redirected to this route only to get a 404 error. - -To get around this behavior, you would simply need to extend the ``ExceptionListener`` -class and override the default method named ``setTargetPath()``. - -First, override the ``security.exception_listener.class`` parameter in your -configuration file. This can be done from your main configuration file (in -`app/config`) or from a configuration file being imported from a bundle: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/HelloBundle/Resources/config/services.yml - parameters: - # ... - security.exception_listener.class: Acme\HelloBundle\Security\Firewall\ExceptionListener - - .. code-block:: xml - - - - - Acme\HelloBundle\Security\Firewall\ExceptionListener - - - .. code-block:: php - - // src/Acme/HelloBundle/Resources/config/services.php - // ... - $container->setParameter('security.exception_listener.class', 'Acme\HelloBundle\Security\Firewall\ExceptionListener'); - -Next, create your own ``ExceptionListener``:: - - // src/Acme/HelloBundle/Security/Firewall/ExceptionListener.php - namespace Acme\HelloBundle\Security\Firewall; - - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Security\Http\Firewall\ExceptionListener as BaseExceptionListener; - - class ExceptionListener extends BaseExceptionListener - { - protected function setTargetPath(Request $request) - { - // Do not save target path for XHR and non-GET requests - // You can add any more logic here you want - if ($request->isXmlHttpRequest() || 'GET' !== $request->getMethod()) { - return; - } - - $request->getSession()->set('_security.main.target_path', $request->getUri()); - } - } - -Add as much or few logic here as required for your scenario! \ No newline at end of file diff --git a/cookbook/security/voters.rst b/cookbook/security/voters.rst deleted file mode 100644 index 9960a032183..00000000000 --- a/cookbook/security/voters.rst +++ /dev/null @@ -1,204 +0,0 @@ -.. index:: - single: Security; Voters - -How to implement your own Voter to blacklist IP Addresses -========================================================= - -The Symfony2 security component provides several layers to authenticate users. -One of the layers is called a `voter`. A voter is a dedicated class that checks -if the user has the rights to be connected to the application. For instance, -Symfony2 provides a layer that checks if the user is fully authenticated or if -it has some expected roles. - -It is sometimes useful to create a custom voter to handle a specific case not -handled by the framework. In this section, you'll learn how to create a voter -that will allow you to blacklist users by their IP. - -The Voter Interface -------------------- - -A custom voter must implement -:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`, -which requires the following three methods: - -.. code-block:: php - - interface VoterInterface - { - function supportsAttribute($attribute); - function supportsClass($class); - function vote(TokenInterface $token, $object, array $attributes); - } - - -The ``supportsAttribute()`` method is used to check if the voter supports -the given user attribute (i.e: a role, an acl, etc.). - -The ``supportsClass()`` method is used to check if the voter supports the -current user token class. - -The ``vote()`` method must implement the business logic that verifies whether -or not the user is granted access. This method must return one of the following -values: - -* ``VoterInterface::ACCESS_GRANTED``: The user is allowed to access the application -* ``VoterInterface::ACCESS_ABSTAIN``: The voter cannot decide if the user is granted or not -* ``VoterInterface::ACCESS_DENIED``: The user is not allowed to access the application - -In this example, you'll check if the user's IP address matches against a list of -blacklisted addresses. If the user's IP is blacklisted, you'll return -``VoterInterface::ACCESS_DENIED``, otherwise you'll return -``VoterInterface::ACCESS_ABSTAIN`` as this voter's purpose is only to deny -access, not to grant access. - -Creating a Custom Voter ------------------------ - -To blacklist a user based on its IP, you can use the ``request`` service -and compare the IP address against a set of blacklisted IP addresses: - -.. code-block:: php - - // src/Acme/DemoBundle/Security/Authorization/Voter/ClientIpVoter.php - namespace Acme\DemoBundle\Security\Authorization\Voter; - - use Symfony\Component\DependencyInjection\ContainerInterface; - use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; - use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - - class ClientIpVoter implements VoterInterface - { - public function __construct(ContainerInterface $container, array $blacklistedIp = array()) - { - $this->container = $container; - $this->blacklistedIp = $blacklistedIp; - } - - public function supportsAttribute($attribute) - { - // you won't check against a user attribute, so return true - return true; - } - - public function supportsClass($class) - { - // your voter supports all type of token classes, so return true - return true; - } - - function vote(TokenInterface $token, $object, array $attributes) - { - $request = $this->container->get('request'); - if (in_array($request->getClientIp(), $this->blacklistedIp)) { - return VoterInterface::ACCESS_DENIED; - } - - return VoterInterface::ACCESS_ABSTAIN; - } - } - -That's it! The voter is done. The next step is to inject the voter into -the security layer. This can be done easily through the service container. - -Declaring the Voter as a Service --------------------------------- - -To inject the voter into the security layer, you must declare it as a service, -and tag it as a "security.voter": - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/AcmeBundle/Resources/config/services.yml - services: - security.access.blacklist_voter: - class: Acme\DemoBundle\Security\Authorization\Voter\ClientIpVoter - arguments: ["@service_container", [123.123.123.123, 171.171.171.171]] - public: false - tags: - - { name: security.voter } - - .. code-block:: xml - - - - - - 123.123.123.123 - 171.171.171.171 - - - - - .. code-block:: php - - // src/Acme/AcmeBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; - - $definition = new Definition( - 'Acme\DemoBundle\Security\Authorization\Voter\ClientIpVoter', - array( - new Reference('service_container'), - array('123.123.123.123', '171.171.171.171'), - ), - ); - $definition->addTag('security.voter'); - $definition->setPublic(false); - - $container->setDefinition('security.access.blacklist_voter', $definition); - -.. tip:: - - Be sure to import this configuration file from your main application - configuration file (e.g. ``app/config/config.yml``). For more information - see :ref:`service-container-imports-directive`. To read more about defining - services in general, see the :doc:`/book/service_container` chapter. - -Changing the Access Decision Strategy -------------------------------------- - -In order for the new voter to take effect, you need to change the default access -decision strategy, which, by default, grants access if *any* voter grants -access. - -In this case, choose the ``unanimous`` strategy. Unlike the ``affirmative`` -strategy (the default), with the ``unanimous`` strategy, if only one voter -denies access (e.g. the ``ClientIpVoter``), access is not granted to the -end user. - -To do that, override the default ``access_decision_manager`` section of your -application configuration file with the following code. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - access_decision_manager: - # strategy can be: affirmative, unanimous or consensus - strategy: unanimous - - .. code-block:: xml - - - - - - - - .. code-block:: php - - // app/config/security.xml - $container->loadFromExtension('security', array( - // strategy can be: affirmative, unanimous or consensus - 'access_decision_manager' => array( - 'strategy' => 'unanimous', - ), - )); - -That's it! Now, when deciding whether or not a user should have access, -the new voter will deny access to any user in the list of blacklisted IPs. diff --git a/cookbook/serializer.rst b/cookbook/serializer.rst deleted file mode 100644 index d4e9baeab00..00000000000 --- a/cookbook/serializer.rst +++ /dev/null @@ -1,111 +0,0 @@ -.. index:: - single: Serializer - -How to use the Serializer -========================= - -Serializing and deserializing to and from objects and different formats (e.g. -JSON or XML) is a very complex topic. Symfony comes with a -:doc:`Serializer Component`, which gives you some -tools that you can leverage for your solution. - -In fact, before you start, get familiar with the serializer, normalizers -and encoders by reading the :doc:`Serializer Component`. -You should also check out the `JMSSerializerBundle`_, which expands on the -functionality offered by Symfony's core serializer. - -Activating the Serializer -------------------------- - -.. versionadded:: 2.3 - The Serializer has always existed in Symfony, but prior to Symfony 2.3, - you needed to build the ``serializer`` service yourself. - -The ``serializer`` service is not available by default. To turn it on, activate -it in your configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - # ... - serializer: - enabled: true - - .. code-block:: xml - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - // ... - 'serializer' => array( - 'enabled' => true - ), - )); - -Adding Normalizers and Encoders -------------------------------- - -Once enabled, the ``serializer`` service will be available in the container -and will be loaded with two :ref:`encoders` -(:class:`Symfony\\Component\\Serializer\\Encoder\\JsonEncoder` and -:class:`Symfony\\Component\\Serializer\\Encoder\\XmlEncoder`) -but no :ref:`normalizers`, meaning you'll -need to load your own. - -You can load normalizers and/or encoders by tagging them as -:ref:`serializer.normalizer` and -:ref:`serializer.encoder`. It's also -possible to set the priority of the tag in order to decide the matching order. - -Here an example on how to load the load -the :class:`Symfony\\Component\\Serializer\\Normalizer\\GetSetMethodNormalizer`: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - services: - get_set_method_normalizer: - class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer - tags: - - { name: serializer.normalizer } - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/config.php - use Symfony\Component\DependencyInjection\Definition; - - $definition = new Definition( - 'Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer' - )); - $definition->addTag('serializer.normalizer'); - $container->setDefinition('get_set_method_normalizer', $definition); - -.. note:: - - The :class:`Symfony\\Component\\Serializer\\Normalizer\\GetSetMethodNormalizer` - is broken by design. As soon as you have a circular object graph, an - infinite loop is created when calling the getters. You're encouraged - to add your own normalizers that fit your use-case. - -.. _JMSSerializerBundle: http://jmsyst.com/bundles/JMSSerializerBundle \ No newline at end of file diff --git a/cookbook/service_container/compiler_passes.rst b/cookbook/service_container/compiler_passes.rst deleted file mode 100644 index 98801a5891e..00000000000 --- a/cookbook/service_container/compiler_passes.rst +++ /dev/null @@ -1,38 +0,0 @@ -.. index:: - single: Dependency Injection; Compiler passes - single: Service Container; Compiler passes - -How to work with Compiler Passes in Bundles -=========================================== - -Compiler passes give you an opportunity to manipulate other service -definitions that have been registered with the service container. You -can read about how to create them in the components section ":doc:`/components/dependency_injection/compilation`". -To register a compiler pass from a bundle you need to add it to the build -method of the bundle definition class:: - - // src/Acme/MailerBundle/AcmeMailerBundle.php - namespace Acme\MailerBundle; - - use Symfony\Component\HttpKernel\Bundle\Bundle; - use Symfony\Component\DependencyInjection\ContainerBuilder; - - use Acme\MailerBundle\DependencyInjection\Compiler\CustomCompilerPass; - - class AcmeMailerBundle extends Bundle - { - public function build(ContainerBuilder $container) - { - parent::build($container); - - $container->addCompilerPass(new CustomCompilerPass()); - } - } - -One of the most common use-cases of compiler passes is to work with tagged services -(read more about tags in the components section ":doc:`/components/dependency_injection/tags`"). -If you are using custom tags in a bundle then by convention, tag names consist -of the name of the bundle (lowercase, underscores as separators), followed -by a dot, and finally the "real" name. For example, if you want to introduce -some sort of "transport" tag in your AcmeMailerBundle, you should call it -``acme_mailer.transport``. diff --git a/cookbook/service_container/event_listener.rst b/cookbook/service_container/event_listener.rst deleted file mode 100644 index 7e0b5410f79..00000000000 --- a/cookbook/service_container/event_listener.rst +++ /dev/null @@ -1,128 +0,0 @@ -.. index:: - single: Events; Create listener - -How to create an Event Listener -=============================== - -Symfony has various events and hooks that can be used to trigger custom -behavior in your application. Those events are thrown by the HttpKernel -component and can be viewed in the :class:`Symfony\\Component\\HttpKernel\\KernelEvents` class. - -To hook into an event and add your own custom logic, you have to create -a service that will act as an event listener on that event. In this entry, -you will create a service that will act as an Exception Listener, allowing -you to modify how exceptions are shown by your application. The ``KernelEvents::EXCEPTION`` -event is just one of the core kernel events:: - - // src/Acme/DemoBundle/EventListener/AcmeExceptionListener.php - namespace Acme\DemoBundle\EventListener; - - use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; - - class AcmeExceptionListener - { - public function onKernelException(GetResponseForExceptionEvent $event) - { - // You get the exception object from the received event - $exception = $event->getException(); - $message = sprintf( - 'My Error says: %s with code: %s', - $exception->getMessage(), - $exception->getCode() - ); - - // Customize your response object to display the exception details - $response = new Response(); - $response->setContent($message); - - // HttpExceptionInterface is a special type of exception that - // holds status code and header details - if ($exception instanceof HttpExceptionInterface) { - $response->setStatusCode($exception->getStatusCode()); - $response->headers->replace($exception->getHeaders()); - } else { - $response->setStatusCode(500); - } - - // Send the modified response object to the event - $event->setResponse($response); - } - } - -.. tip:: - - Each event receives a slightly different type of ``$event`` object. For - the ``kernel.exception`` event, it is :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent`. - To see what type of object each event listener receives, see :class:`Symfony\\Component\\HttpKernel\\KernelEvents`. - -Now that the class is created, you just 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": - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - services: - kernel.listener.your_listener_name: - class: Acme\DemoBundle\EventListener\AcmeExceptionListener - tags: - - { name: kernel.event_listener, event: kernel.exception, method: onKernelException } - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container - ->register('kernel.listener.your_listener_name', 'Acme\DemoBundle\EventListener\AcmeExceptionListener') - ->addTag('kernel.event_listener', array('event' => 'kernel.exception', 'method' => 'onKernelException')) - ; - -.. note:: - - There is an additional tag option ``priority`` that is optional and defaults - to 0. This value can be from -255 to 255, and the listeners will be executed - in the order of their priority. This is useful when you need to guarantee - that one listener is executed before another. - -Request events, checking types ------------------------------- - -A single page can make several requests (one master request, and then multiple -sub-requests), which is why when working with the ``KernelEvents::REQUEST`` -event, you might need to check the type of the request. This can be easily -done as follow:: - - // src/Acme/DemoBundle/EventListener/AcmeRequestListener.php - namespace Acme\DemoBundle\EventListener; - - use Symfony\Component\HttpKernel\Event\GetResponseEvent; - use Symfony\Component\HttpKernel\HttpKernel; - - class AcmeRequestListener - { - public function onKernelRequest(GetResponseEvent $event) - { - if (HttpKernel::MASTER_REQUEST != $event->getRequestType()) { - // don't do anything if it's not the master request - return; - } - - // ... - } - } - -.. tip:: - - Two types of request are available in the :class:`Symfony\\Component\\HttpKernel\\HttpKernelInterface` - interface: ``HttpKernelInterface::MASTER_REQUEST`` and - ``HttpKernelInterface::SUB_REQUEST``. diff --git a/cookbook/service_container/index.rst b/cookbook/service_container/index.rst deleted file mode 100644 index be8ad17868b..00000000000 --- a/cookbook/service_container/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Service Container -================= - -.. toctree:: - :maxdepth: 2 - - event_listener - scopes - compiler_passes diff --git a/cookbook/service_container/scopes.rst b/cookbook/service_container/scopes.rst deleted file mode 100644 index e779bc933b9..00000000000 --- a/cookbook/service_container/scopes.rst +++ /dev/null @@ -1,345 +0,0 @@ -.. index:: - single: Dependency Injection; Scopes - -How to work with Scopes -======================= - -This entry is all about scopes, a somewhat advanced topic related to the -:doc:`/book/service_container`. If you've ever gotten an error mentioning -"scopes" when creating services, or need to create a service that depends -on the ``request`` service, then this entry is for you. - -Understanding Scopes --------------------- - -The scope of a service controls how long an instance of a service is used -by the container. The Dependency Injection component provides two generic -scopes: - -- ``container`` (the default one): The same instance is used each time you - request it from this container. - -- ``prototype``: A new instance is created each time you request the service. - -The -:class:`Symfony\\Component\\HttpKernel\\DependencyInjection\\ContainerAwareHttpKernel` -also defines a third scope: ``request``. This scope is tied to the request, -meaning a new instance is created for each subrequest and is unavailable -outside the request (for instance in the CLI). - -Scopes add a constraint on the dependencies of a service: a service cannot -depend on services from a narrower scope. For example, if you create a generic -``my_foo`` service, but try to inject the ``request`` service, you will receive -a :class:`Symfony\\Component\\DependencyInjection\\Exception\\ScopeWideningInjectionException` -when compiling the container. Read the sidebar below for more details. - -.. sidebar:: Scopes and Dependencies - - Imagine you've configured a ``my_mailer`` service. You haven't configured - the scope of the service, so it defaults to ``container``. In other words, - every time you ask the container for the ``my_mailer`` service, you get - the same object back. This is usually how you want your services to work. - - Imagine, however, that you need the ``request`` service in your ``my_mailer`` - service, maybe because you're reading the URL of the current request. - So, you add it as a constructor argument. Let's look at why this presents - a problem: - - * When requesting ``my_mailer``, an instance of ``my_mailer`` (let's call - it *MailerA*) is created and the ``request`` service (let's call it - *RequestA*) is passed to it. Life is good! - - * You've now made a subrequest in Symfony, which is a fancy way of saying - that you've called, for example, the ``{{ render(...) }}`` Twig function, - which executes another controller. Internally, the old ``request`` service - (*RequestA*) is actually replaced by a new request instance (*RequestB*). - This happens in the background, and it's totally normal. - - * In your embedded controller, you once again ask for the ``my_mailer`` - service. Since your service is in the ``container`` scope, the same - instance (*MailerA*) is just re-used. But here's the problem: the - *MailerA* instance still contains the old *RequestA* object, which - is now **not** the correct request object to have (*RequestB* is now - the current ``request`` service). This is subtle, but the mis-match could - cause major problems, which is why it's not allowed. - - So, that's the reason *why* scopes exist, and how they can cause - problems. Keep reading to find out the common solutions. - -.. note:: - - A service can of course depend on a service from a wider scope without - any issue. - -Using a Service from a narrower Scope -------------------------------------- - -If your service has a dependency on a scoped service (like the ``request``), -you have three ways to deal with it: - -* Use setter injection if the dependency is "synchronized"; this is the - recommended way and the best solution for the ``request`` instance as it is - synchronized with the ``request`` scope (see - :ref:`using-synchronized-service`). - -* Put your service in the same scope as the dependency (or a narrower one). If - you depend on the ``request`` service, this means putting your new service - in the ``request`` scope (see :ref:`changing-service-scope`); - -* Pass the entire container to your service and retrieve your dependency from - the container each time you need it to be sure you have the right instance - -- your service can live in the default ``container`` scope (see - :ref:`passing-container`); - -Each scenario is detailed in the following sections. - -.. _using-synchronized-service: - -Using a synchronized Service -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.3 - Synchronized services are new in Symfony 2.3. - -Injecting the container or setting your service to a narrower scope have -drawbacks. For synchronized services (like the ``request``), using setter -injection is the best option as it has no drawbacks and everything works -without any special code in your service or in your definition:: - - // src/Acme/HelloBundle/Mail/Mailer.php - namespace Acme\HelloBundle\Mail; - - use Symfony\Component\HttpFoundation\Request; - - class Mailer - { - protected $request; - - public function setRequest(Request $request = null) - { - $this->request = $request; - } - - public function sendEmail() - { - if (null === $this->request) { - // throw an error? - } - - // ... do something using the request here - } - } - -Whenever the ``request`` scope is entered or left, the service container will -automatically call the ``setRequest()`` method with the current ``request`` -instance. - -You might have noticed that the ``setRequest()`` method accepts ``null`` as a -valid value for the ``request`` argument. That's because when leaving the -``request`` scope, the ``request`` instance can be ``null`` (for the master -request for instance). Of course, you should take care of this possibility in -your code. This should also be taken into account when declaring your service: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/HelloBundle/Resources/config/services.yml - services: - greeting_card_manager: - class: Acme\HelloBundle\Mail\GreetingCardManager - calls: - - [setRequest, ['@?request']] - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // src/Acme/HelloBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\ContainerInterface; - - $definition = $container->setDefinition( - 'greeting_card_manager', - new Definition('Acme\HelloBundle\Mail\GreetingCardManager') - ) - ->addMethodCall('setRequest', array( - new Reference('request', ContainerInterface::NULL_ON_INVALID_REFERENCE, false) - )); - -.. tip:: - - You can declare your own ``synchronized`` services very easily; here is - the declaration of the ``request`` service for reference: - - .. configuration-block:: - - .. code-block:: yaml - - services: - request: - scope: request - synthetic: true - synchronized: true - - .. code-block:: xml - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\ContainerInterface; - - $definition = $container->setDefinition('request') - ->setScope('request') - ->setSynthetic(true) - ->setSynchronized(true); - -.. _changing-service-scope: - -Changing the Scope of your Service -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Changing the scope of a service should be done in its definition: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/HelloBundle/Resources/config/services.yml - services: - greeting_card_manager: - class: Acme\HelloBundle\Mail\GreetingCardManager - scope: request - arguments: [@request] - - .. code-block:: xml - - - - - - - - .. code-block:: php - - // src/Acme/HelloBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; - - $definition = $container->setDefinition( - 'greeting_card_manager', - new Definition( - 'Acme\HelloBundle\Mail\GreetingCardManager', - array(new Reference('request'), - )) - )->setScope('request'); - -.. _passing-container: - -Passing the Container as a Dependency of your Service -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Setting the scope to a narrower one is not always possible (for instance, a -twig extension must be in the ``container`` scope as the Twig environment -needs it as a dependency). In these cases, you can pass the entire container -into your service:: - - // src/Acme/HelloBundle/Mail/Mailer.php - namespace Acme\HelloBundle\Mail; - - use Symfony\Component\DependencyInjection\ContainerInterface; - - class Mailer - { - protected $container; - - public function __construct(ContainerInterface $container) - { - $this->container = $container; - } - - public function sendEmail() - { - $request = $this->container->get('request'); - // ... do something using the request here - } - } - -.. caution:: - - Take care not to store the request in a property of the object for a - future call of the service as it would cause the same issue described - in the first section (except that Symfony cannot detect that you are - wrong). - -The service config for this class would look something like this: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/HelloBundle/Resources/config/services.yml - parameters: - # ... - my_mailer.class: Acme\HelloBundle\Mail\Mailer - services: - my_mailer: - class: "%my_mailer.class%" - arguments: ["@service_container"] - # scope: container can be omitted as it is the default - - .. code-block:: xml - - - - - Acme\HelloBundle\Mail\Mailer - - - - - - - - - .. code-block:: php - - // src/Acme/HelloBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; - - // ... - $container->setParameter('my_mailer.class', 'Acme\HelloBundle\Mail\Mailer'); - - $container->setDefinition('my_mailer', new Definition( - '%my_mailer.class%', - array(new Reference('service_container')) - )); - -.. note:: - - Injecting the whole container into a service is generally not a good - idea (only inject what you need). - -.. tip:: - - If you define a controller as a service then you can get the ``Request`` - object without injecting the container by having it passed in as an - argument of your action method. See - :ref:`book-controller-request-argument` for details. diff --git a/cookbook/session/index.rst b/cookbook/session/index.rst deleted file mode 100644 index e7550bb723c..00000000000 --- a/cookbook/session/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Sessions -======== - -.. toctree:: - :maxdepth: 2 - - php_bridge diff --git a/cookbook/session/php_bridge.rst b/cookbook/session/php_bridge.rst deleted file mode 100644 index 6783bf398e2..00000000000 --- a/cookbook/session/php_bridge.rst +++ /dev/null @@ -1,36 +0,0 @@ -.. index:: - single: Sessions - -Bridge a legacy application with Symfony Sessions -------------------------------------------------- - -.. versionadded:: 2.3 - The ability to integrate with a legacy PHP session was added in Symfony 2.3. - -If you're integrating the Symfony full-stack Framework into a legacy application -that starts the session with ``session_start()``, you may still be able to -use Symfony's session management by using the PHP Bridge session. - -If the application has sets it's own PHP save handler, you can specify null -for the ``handler_id``: - -.. code-block:: yaml - - framework: - session: - storage_id: session.storage.php_bridge - handler_id: ~ - -Otherwise, if the problem is simply that you cannot avoid the application -starting the session with ``session_start()``, you can still make use of -a Symfony based session save handler by specifying the save handler as in -the example below: - -.. code-block:: yaml - - framework: - session: - storage_id: session.storage.php_bridge - handler_id: session.handler.native_file - -For more details, see :doc:`/components/http_foundation/session_php_bridge`. \ No newline at end of file diff --git a/cookbook/symfony1.rst b/cookbook/symfony1.rst deleted file mode 100644 index 57e6cdae880..00000000000 --- a/cookbook/symfony1.rst +++ /dev/null @@ -1,359 +0,0 @@ -.. index:: - single: symfony1 - -How Symfony2 differs from symfony1 -================================== - -The Symfony2 framework embodies a significant evolution when compared with -the first version of the framework. Fortunately, with the MVC architecture -at its core, the skills used to master a symfony1 project continue to be -very relevant when developing in Symfony2. Sure, ``app.yml`` is gone, but -routing, controllers and templates all remain. - -This chapter walks through the differences between symfony1 and Symfony2. -As you'll see, many tasks are tackled in a slightly different way. You'll -come to appreciate these minor differences as they promote stable, predictable, -testable and decoupled code in your Symfony2 applications. - -So, sit back and relax as you travel from "then" to "now". - -Directory Structure -------------------- - -When looking at a Symfony2 project - for example, the `Symfony2 Standard Edition`_ - -you'll notice a very different directory structure than in symfony1. The -differences, however, are somewhat superficial. - -The ``app/`` Directory -~~~~~~~~~~~~~~~~~~~~~~ - -In symfony1, your project has one or more applications, and each lives inside -the ``apps/`` directory (e.g. ``apps/frontend``). By default in Symfony2, -you have just one application represented by the ``app/`` directory. Like -in symfony1, the ``app/`` directory contains configuration specific to that -application. It also contains application-specific cache, log and template -directories as well as a ``Kernel`` class (``AppKernel``), which is the base -object that represents the application. - -Unlike symfony1, almost no PHP code lives in the ``app/`` directory. This -directory is not meant to house modules or library files as it did in symfony1. -Instead, it's simply the home of configuration and other resources (templates, -translation files). - -The ``src/`` Directory -~~~~~~~~~~~~~~~~~~~~~~ - -Put simply, your actual code goes here. In Symfony2, all actual application-code -lives inside a bundle (roughly equivalent to a symfony1 plugin) and, by default, -each bundle lives inside the ``src`` directory. In that way, the ``src`` -directory is a bit like the ``plugins`` directory in symfony1, but much more -flexible. Additionally, while *your* bundles will live in the ``src/`` directory, -third-party bundles will live somewhere in the ``vendor/`` directory. - -To get a better picture of the ``src/`` directory, let's first think of a -symfony1 application. First, part of your code likely lives inside one or -more applications. Most commonly these include modules, but could also include -any other PHP classes you put in your application. You may have also created -a ``schema.yml`` file in the ``config`` directory of your project and built -several model files. Finally, to help with some common functionality, you're -using several third-party plugins that live in the ``plugins/`` directory. -In other words, the code that drives your application lives in many different -places. - -In Symfony2, life is much simpler because *all* Symfony2 code must live in -a bundle. In the pretend symfony1 project, all the code *could* be moved -into one or more plugins (which is a very good practice, in fact). Assuming -that all modules, PHP classes, schema, routing configuration, etc were moved -into a plugin, the symfony1 ``plugins/`` directory would be very similar -to the Symfony2 ``src/`` directory. - -Put simply again, the ``src/`` directory is where your code, assets, -templates and most anything else specific to your project will live. - -The ``vendor/`` Directory -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``vendor/`` directory is basically equivalent to the ``lib/vendor/`` -directory in symfony1, which was the conventional directory for all vendor -libraries and bundles. By default, you'll find the Symfony2 library files in -this directory, along with several other dependent libraries such as Doctrine2, -Twig and Swiftmailer. 3rd party Symfony2 bundles live somewhere in the -``vendor/``. - -The ``web/`` Directory -~~~~~~~~~~~~~~~~~~~~~~ - -Not much has changed in the ``web/`` directory. The most noticeable difference -is the absence of the ``css/``, ``js/`` and ``images/`` directories. This -is intentional. Like with your PHP code, all assets should also live inside -a bundle. With the help of a console command, the ``Resources/public/`` -directory of each bundle is copied or symbolically-linked to the ``web/bundles/`` -directory. This allows you to keep assets organized inside your bundle, but -still make them available to the public. To make sure that all bundles are -available, run the following command: - -.. code-block:: bash - - $ php app/console assets:install web - -.. note:: - - This command is the Symfony2 equivalent to the symfony1 ``plugin:publish-assets`` - command. - -Autoloading ------------ - -One of the advantages of modern frameworks is never needing to worry about -requiring files. By making use of an autoloader, you can refer to any class -in your project and trust that it's available. Autoloading has changed in -Symfony2 to be more universal, faster, and independent of needing to clear -your cache. - -In symfony1, autoloading was done by searching the entire project for the -presence of PHP class files and caching this information in a giant array. -That array told symfony1 exactly which file contained each class. In the -production environment, this caused you to need to clear the cache when classes -were added or moved. - -In Symfony2, a tool named `Composer`_ handles this process. -The idea behind the autoloader is simple: the name of your class (including -the namespace) must match up with the path to the file containing that class. -Take the ``FrameworkExtraBundle`` from the Symfony2 Standard Edition as an -example:: - - namespace Sensio\Bundle\FrameworkExtraBundle; - - use Symfony\Component\HttpKernel\Bundle\Bundle; - // ... - - class SensioFrameworkExtraBundle extends Bundle - { - // ... - } - -The file itself lives at -``vendor/sensio/framework-extra-bundle/Sensio/Bundle/FrameworkExtraBundle/SensioFrameworkExtraBundle.php``. -As you can see, the location of the file follows the namespace of the class. -Specifically, the namespace, ``Sensio\Bundle\FrameworkExtraBundle``, spells out -the directory that the file should live in -(``vendor/sensio/framework-extra-bundle/Sensio/Bundle/FrameworkExtraBundle/``). -Composer can then look for the file at this specific place and load it very fast. - -If the file did *not* live at this exact location, you'd receive a -``Class "Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle" does not exist.`` -error. In Symfony2, a "class does not exist" means that the suspect class -namespace and physical location do not match. Basically, Symfony2 is looking -in one exact location for that class, but that location doesn't exist (or -contains a different class). In order for a class to be autoloaded, you -**never need to clear your cache** in Symfony2. - -As mentioned before, for the autoloader to work, it needs to know that the -``Sensio`` namespace lives in the ``vendor/bundles`` directory and that, for -example, the ``Doctrine`` namespace lives in the ``vendor/doctrine/orm/lib/`` -directory. This mapping is entirely controlled by Composer. Each -third-party library you load through composer has their settings defined -and Composer takes care of everything for you. - -For this to work, all third-party libraries used by your project must be -defined in the ``composer.json`` file. - -If you look at the ``HelloController`` from the Symfony2 Standard Edition you -can see that it lives in the ``Acme\DemoBundle\Controller`` namespace. Yet, the -``AcmeDemoBundle`` is not defined in your ``composer.json`` file. Nonetheless are -the files autoloaded. This is because you can tell composer to autoload files -from specific directories without defining a dependency: - -.. code-block:: yaml - - "autoload": { - "psr-0": { "": "src/" } - } - -Using the Console ------------------ - -In symfony1, the console is in the root directory of your project and is -called ``symfony``: - -.. code-block:: bash - - $ php symfony - -In Symfony2, the console is now in the app sub-directory and is called -``console``: - -.. code-block:: bash - - $ php app/console - -Applications ------------- - -In a symfony1 project, it is common to have several applications: one for the -frontend and one for the backend for instance. - -In a Symfony2 project, you only need to create one application (a blog -application, an intranet application, ...). Most of the time, if you want to -create a second application, you might instead create another project and -share some bundles between them. - -And if you need to separate the frontend and the backend features of some -bundles, you can create sub-namespaces for controllers, sub-directories for -templates, different semantic configurations, separate routing configurations, -and so on. - -Of course, there's nothing wrong with having multiple applications in your -project, that's entirely up to you. A second application would mean a new -directory, e.g. ``my_app/``, with the same basic setup as the ``app/`` directory. - -.. tip:: - - Read the definition of a :term:`Project`, an :term:`Application`, and a - :term:`Bundle` in the glossary. - -Bundles and Plugins -------------------- - -In a symfony1 project, a plugin could contain configuration, modules, PHP -libraries, assets and anything else related to your project. In Symfony2, -the idea of a plugin is replaced by the "bundle". A bundle is even more powerful -than a plugin because the core Symfony2 framework is brought in via a series -of bundles. In Symfony2, bundles are first-class citizens that are so flexible -that even core code itself is a bundle. - -In symfony1, a plugin must be enabled inside the ``ProjectConfiguration`` -class:: - - // config/ProjectConfiguration.class.php - public function setup() - { - // some plugins here - $this->enableAllPluginsExcept(array(...)); - } - -In Symfony2, the bundles are activated inside the application kernel:: - - // app/AppKernel.php - public function registerBundles() - { - $bundles = array( - new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), - new Symfony\Bundle\TwigBundle\TwigBundle(), - ..., - new Acme\DemoBundle\AcmeDemoBundle(), - ); - - return $bundles; - } - -Routing (``routing.yml``) and Configuration (``config.yml``) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In symfony1, the ``routing.yml`` and ``app.yml`` configuration files were -automatically loaded inside any plugin. In Symfony2, routing and application -configuration inside a bundle must be included manually. For example, to -include a routing resource from a bundle called ``AcmeDemoBundle``, you can -do the following: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - _hello: - resource: "@AcmeDemoBundle/Resources/config/routing.yml" - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\RouteCollection; - - $collection = new RouteCollection(); - $collection->addCollection($loader->import("@AcmeHelloBundle/Resources/config/routing.php")); - - return $collection; - -This will load the routes found in the ``Resources/config/routing.yml`` file -of the ``AcmeDemoBundle``. The special ``@AcmeDemoBundle`` is a shortcut syntax -that, internally, resolves to the full path to that bundle. - -You can use this same strategy to bring in configuration from a bundle: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - imports: - - { resource: "@AcmeDemoBundle/Resources/config/config.yml" } - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $this->import('@AcmeDemoBundle/Resources/config/config.php') - -In Symfony2, configuration is a bit like ``app.yml`` in symfony1, except much -more systematic. With ``app.yml``, you could simply create any keys you wanted. -By default, these entries were meaningless and depended entirely on how you -used them in your application: - -.. code-block:: yaml - - # some app.yml file from symfony1 - all: - email: - from_address: foo.bar@example.com - -In Symfony2, you can also create arbitrary entries under the ``parameters`` -key of your configuration: - -.. configuration-block:: - - .. code-block:: yaml - - parameters: - email.from_address: foo.bar@example.com - - .. code-block:: xml - - - foo.bar@example.com - - - .. code-block:: php - - $container->setParameter('email.from_address', 'foo.bar@example.com'); - -You can now access this from a controller, for example:: - - public function helloAction($name) - { - $fromAddress = $this->container->getParameter('email.from_address'); - } - -In reality, the Symfony2 configuration is much more powerful and is used -primarily to configure objects that you can use. For more information, see -the chapter titled ":doc:`/book/service_container`". - -.. _`Composer`: http://getcomposer.org -.. _`Symfony2 Standard Edition`: https://github.com/symfony/symfony-standard diff --git a/cookbook/templating/PHP.rst b/cookbook/templating/PHP.rst deleted file mode 100644 index 40f03611fd0..00000000000 --- a/cookbook/templating/PHP.rst +++ /dev/null @@ -1,324 +0,0 @@ -.. index:: - single: PHP Templates - -How to use PHP instead of Twig for Templates -============================================ - -Even if Symfony2 defaults to Twig for its template engine, you can still use -plain PHP code if you want. Both templating engines are supported equally in -Symfony2. Symfony2 adds some nice features on top of PHP to make writing -templates with PHP more powerful. - -Rendering PHP Templates ------------------------ - -If you want to use the PHP templating engine, first, make sure to enable it in -your application configuration file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - # ... - templating: { engines: ['twig', 'php'] } - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - $container->loadFromExtension('framework', array( - // ... - - 'templating' => array( - 'engines' => array('twig', 'php'), - ), - )); - -You can now render a PHP template instead of a Twig one simply by using the -``.php`` extension in the template name instead of ``.twig``. The controller -below renders the ``index.html.php`` template:: - - // src/Acme/HelloBundle/Controller/HelloController.php - - // ... - - public function indexAction($name) - { - return $this->render('AcmeHelloBundle:Hello:index.html.php', array('name' => $name)); - } - -You can also use the :doc:`/bundles/SensioFrameworkExtraBundle/annotations/view` -shortcut to render the default ``AcmeHelloBundle:Hello:index.html.php`` template:: - - // src/Acme/HelloBundle/Controller/HelloController.php - - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; - - // ... - - /** - * @Template(engine="php") - */ - public function indexAction($name) - { - return array('name' => $name); - } - -.. index:: - single: Templating; Layout - single: Layout - -Decorating Templates --------------------- - -More often than not, templates in a project share common elements, like the -well-known header and footer. In Symfony2, this problem is thought about -differently: a template can be decorated by another one. - -The ``index.html.php`` template is decorated by ``layout.html.php``, thanks to -the ``extend()`` call: - -.. code-block:: html+php - - - extend('AcmeHelloBundle::layout.html.php') ?> - - Hello ! - -The ``AcmeHelloBundle::layout.html.php`` notation sounds familiar, doesn't it? It -is the same notation used to reference a template. The ``::`` part simply -means that the controller element is empty, so the corresponding file is -directly stored under ``views/``. - -Now, let's have a look at the ``layout.html.php`` file: - -.. code-block:: html+php - - - extend('::base.html.php') ?> - -

    Hello Application

    - - output('_content') ?> - -The layout is itself decorated by another one (``::base.html.php``). Symfony2 -supports multiple decoration levels: a layout can itself be decorated by -another one. When the bundle part of the template name is empty, views are -looked for in the ``app/Resources/views/`` directory. This directory store -global views for your entire project: - -.. code-block:: html+php - - - - - - - <?php $view['slots']->output('title', 'Hello Application') ?> - - - output('_content') ?> - - - -For both layouts, the ``$view['slots']->output('_content')`` expression is -replaced by the content of the child template, ``index.html.php`` and -``layout.html.php`` respectively (more on slots in the next section). - -As you can see, Symfony2 provides methods on a mysterious ``$view`` object. In -a template, the ``$view`` variable is always available and refers to a special -object that provides a bunch of methods that makes the template engine tick. - -.. index:: - single: Templating; Slot - single: Slot - -Working with Slots ------------------- - -A slot is a snippet of code, defined in a template, and reusable in any layout -decorating the template. In the ``index.html.php`` template, define a -``title`` slot: - -.. code-block:: html+php - - - extend('AcmeHelloBundle::layout.html.php') ?> - - set('title', 'Hello World Application') ?> - - Hello ! - -The base layout already has the code to output the title in the header: - -.. code-block:: html+php - - - - - <?php $view['slots']->output('title', 'Hello Application') ?> - - -The ``output()`` method inserts the content of a slot and optionally takes a -default value if the slot is not defined. And ``_content`` is just a special -slot that contains the rendered child template. - -For large slots, there is also an extended syntax: - -.. code-block:: html+php - - start('title') ?> - Some large amount of HTML - stop() ?> - -.. index:: - single: Templating; Include - -Including other Templates -------------------------- - -The best way to share a snippet of template code is to define a template that -can then be included into other templates. - -Create a ``hello.html.php`` template: - -.. code-block:: html+php - - - Hello ! - -And change the ``index.html.php`` template to include it: - -.. code-block:: html+php - - - extend('AcmeHelloBundle::layout.html.php') ?> - - render('AcmeHelloBundle:Hello:hello.html.php', array('name' => $name)) ?> - -The ``render()`` method evaluates and returns the content of another template -(this is the exact same method as the one used in the controller). - -.. index:: - single: Templating; Embedding pages - -Embedding other Controllers ---------------------------- - -And what if you want to embed the result of another controller in a template? -That's very useful when working with Ajax, or when the embedded template needs -some variable not available in the main template. - -If you create a ``fancy`` action, and want to include it into the -``index.html.php`` template, simply use the following code: - -.. code-block:: html+php - - - render('AcmeHelloBundle:Hello:fancy', array( - 'name' => $name, - 'color' => 'green' - )) ?> - -Here, the ``AcmeHelloBundle:Hello:fancy`` string refers to the ``fancy`` action of the -``Hello`` controller:: - - // src/Acme/HelloBundle/Controller/HelloController.php - - class HelloController extends Controller - { - public function fancyAction($name, $color) - { - // create some object, based on the $color variable - $object = ...; - - return $this->render('AcmeHelloBundle:Hello:fancy.html.php', array( - 'name' => $name, - 'object' => $object - )); - } - - // ... - } - -But where is the ``$view['actions']`` array element defined? Like -``$view['slots']``, it's called a template helper, and the next section tells -you more about those. - -.. index:: - single: Templating; Helpers - -Using Template Helpers ----------------------- - -The Symfony2 templating system can be easily extended via helpers. Helpers are -PHP objects that provide features useful in a template context. ``actions`` and -``slots`` are two of the built-in Symfony2 helpers. - -Creating Links between Pages -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Speaking of web applications, creating links between pages is a must. Instead -of hardcoding URLs in templates, the ``router`` helper knows how to generate -URLs based on the routing configuration. That way, all your URLs can be easily -updated by changing the configuration: - -.. code-block:: html+php - - - Greet Thomas! - - -The ``generate()`` method takes the route name and an array of parameters as -arguments. The route name is the main key under which routes are referenced -and the parameters are the values of the placeholders defined in the route -pattern: - -.. code-block:: yaml - - # src/Acme/HelloBundle/Resources/config/routing.yml - hello: # The route name - path: /hello/{name} - defaults: { _controller: AcmeHelloBundle:Hello:index } - -Using Assets: images, JavaScripts, and stylesheets -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -What would the Internet be without images, JavaScripts, and stylesheets? -Symfony2 provides the ``assets`` tag to deal with them easily: - -.. code-block:: html+php - - - - - -The ``assets`` helper's main purpose is to make your application more -portable. Thanks to this helper, you can move the application root directory -anywhere under your web root directory without changing anything in your -template's code. - -Output Escaping ---------------- - -When using PHP templates, escape variables whenever they are displayed to the -user:: - - escape($var) ?> - -By default, the ``escape()`` method assumes that the variable is outputted -within an HTML context. The second argument lets you change the context. For -instance, to output something in a JavaScript script, use the ``js`` context:: - - escape($var, 'js') ?> diff --git a/cookbook/templating/global_variables.rst b/cookbook/templating/global_variables.rst deleted file mode 100644 index 4067cfb40fb..00000000000 --- a/cookbook/templating/global_variables.rst +++ /dev/null @@ -1,86 +0,0 @@ -.. index:: - single: Templating; Global variables - -How to Inject Variables into all Templates (i.e. Global Variables) -================================================================== - -Sometimes you want a variable to be accessible to all the templates you use. -This is possible inside your ``app/config/config.yml`` file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - twig: - # ... - globals: - ga_tracking: UA-xxxxx-x - - .. code-block:: xml - - - - - UA-xxxxx-x - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('twig', array( - // ... - 'globals' => array( - 'ga_tracking' => 'UA-xxxxx-x', - ), - )); - -Now, the variable ``ga_tracking`` is available in all Twig templates: - -.. code-block:: html+jinja - -

    The google tracking code is: {{ ga_tracking }}

    - -It's that easy! You can also take advantage of the built-in :ref:`book-service-container-parameters` -system, which lets you isolate or reuse the value: - -.. code-block:: yaml - - # app/config/parameters.yml - parameters: - ga_tracking: UA-xxxxx-x - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - twig: - globals: - ga_tracking: "%ga_tracking%" - - .. code-block:: xml - - - - %ga_tracking% - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('twig', array( - 'globals' => array( - 'ga_tracking' => '%ga_tracking%', - ), - )); - -The same variable is available exactly as before. - -More Complex Global Variables ------------------------------ - -If the global variable you want to set is more complicated - say an object - -then you won't be able to use the above method. Instead, you'll need to create -a :ref:`Twig Extension` and return the -global variable as one of the entries in the ``getGlobals`` method. diff --git a/cookbook/templating/index.rst b/cookbook/templating/index.rst deleted file mode 100644 index 46d6fe37012..00000000000 --- a/cookbook/templating/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Templating -========== - -.. toctree:: - :maxdepth: 2 - - global_variables - namespaced_paths - PHP - twig_extension - render_without_controller diff --git a/cookbook/templating/namespaced_paths.rst b/cookbook/templating/namespaced_paths.rst deleted file mode 100644 index 8e33b38ef66..00000000000 --- a/cookbook/templating/namespaced_paths.rst +++ /dev/null @@ -1,83 +0,0 @@ -.. index:: - single: Templating; Namespaced Twig Paths - -How to use and Register namespaced Twig Paths -============================================= - -.. versionadded:: 2.2 - Namespaced path support was added in 2.2. - -Usually, when you refer to a template, you'll use the ``MyBundle:Subdir:filename.html.twig`` -format (see :ref:`template-naming-locations`). - -Twig also natively offers a feature called "namespaced paths", and support -is built-in automatically for all of your bundles. - -Take the following paths as an example: - -.. code-block:: jinja - - {% extends "AcmeDemoBundle::layout.html.twig" %} - {% include "AcmeDemoBundle:Foo:bar.html.twig" %} - -With namespaced paths, the following works as well: - -.. code-block:: jinja - - {% extends "@AcmeDemo/layout.html.twig" %} - {% include "@AcmeDemo/Foo/bar.html.twig" %} - -Both paths are valid and functional by default in Symfony2. - -.. tip:: - - As an added bonus, the namespaced syntax is faster. - -Registering your own namespaces -------------------------------- - -You can also register your own custom namespaces. Suppose that you're using -some third-party library that includes Twig templates that live in -``vendor/acme/foo-project/templates``. First, register a namespace for this -directory: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - twig: - # ... - paths: - "%kernel.root_dir%/../vendor/acme/foo-bar/templates": foo_bar - - .. code-block:: xml - - - - - - - %kernel.root_dir%/../vendor/acme/foo-bar/templates - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('twig', array( - 'paths' => array( - '%kernel.root_dir%/../vendor/acme/foo-bar/templates' => 'foo_bar', - ); - )); - -The registered namespace is called ``foo_bar``, which refers to the -``vendor/acme/foo-project/templates`` directory. Assuming there's a file -called ``sidebar.twig`` in that directory, you can use it easily: - -.. code-block:: jinja - - {% include '@foo_bar/side.bar.twig` %} \ No newline at end of file diff --git a/cookbook/templating/render_without_controller.rst b/cookbook/templating/render_without_controller.rst deleted file mode 100644 index f2a44f7e4e1..00000000000 --- a/cookbook/templating/render_without_controller.rst +++ /dev/null @@ -1,137 +0,0 @@ -.. index:: - single: Templating; Render template without custom controller - -How to render a Template without a custom Controller -==================================================== - -Usually, when you need to create a page, you need to create a controller -and render a template from within that controller. But if you're rendering -a simple template that doesn't need any data passed into it, you can avoid -creating the controller entirely, by using the built-in ``FrameworkBundle:Template:template`` -controller. - -For example, suppose you want to render a ``AcmeBundle:Static:privacy.html.twig`` -template, which doesn't require that any variables are passed to it. You -can do this without creating a controller: - -.. configuration-block:: - - .. code-block:: yaml - - acme_privacy: - path: /privacy - defaults: - _controller: FrameworkBundle:Template:template - template: 'AcmeBundle:Static:privacy.html.twig' - - .. code-block:: xml - - - - - - - FrameworkBundle:Template:template - AcmeBundle:Static:privacy.html.twig - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('acme_privacy', new Route('/privacy', array( - '_controller' => 'FrameworkBundle:Template:template', - 'template' => 'AcmeBundle:Static:privacy.html.twig', - ))); - - return $collection; - -The ``FrameworkBundle:Template:template`` controller will simply render whatever -template you've passed as the ``template`` default value. - -You can of course also use this trick when rendering embedded controllers -from within a template. But since the purpose of rendering a controller from -within a template is typically to prepare some data in a custom controller, -this is probably only useful if you'd like to cache this page partial (see -:ref:`cookbook-templating-no-controller-caching`). - -.. configuration-block:: - - .. code-block:: html+jinja - - {{ render(url('acme_privacy')) }} - - .. code-block:: html+php - - render( - $view['router']->generate('acme_privacy', array(), true) - ) ?> - -.. _cookbook-templating-no-controller-caching: - -Caching the static Template ---------------------------- - -.. versionadded:: 2.2 - The ability to cache templates rendered via ``FrameworkBundle:Template:template`` - is new in Symfony 2.2. - -Since templates that are rendered in this way are typically static, it might -make sense to cache them. Fortunately, this is easy! By configuring a few -other variables in your route, you can control exactly how your page is cached: - -.. configuration-block:: - - .. code-block:: yaml - - acme_privacy: - path: /privacy - defaults: - _controller: FrameworkBundle:Template:template - template: 'AcmeBundle:Static:privacy.html.twig' - maxAge: 86400 - sharedMaxAge: 86400 - - .. code-block:: xml - - - - - - - FrameworkBundle:Template:template - AcmeBundle:Static:privacy.html.twig - 86400 - 86400 - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('acme_privacy', new Route('/privacy', array( - '_controller' => 'FrameworkBundle:Template:template', - 'template' => 'AcmeBundle:Static:privacy.html.twig', - 'maxAge' => 86400, - 'sharedMaxAge' => 86400, - ))); - - return $collection; - -The ``maxAge`` and ``sharedMaxAge`` values are used to modify the Response -object created in the controller. For more information on caching, see -:doc:`/book/http_cache`. - -There is also a ``private`` variable (not shown here). By default, the Response -will be made public, as long as ``maxAge`` or ``sharedMaxAge`` are passed. -If set to ``true``, the Response will be marked as private. \ No newline at end of file diff --git a/cookbook/templating/twig_extension.rst b/cookbook/templating/twig_extension.rst deleted file mode 100644 index 25b629cb81c..00000000000 --- a/cookbook/templating/twig_extension.rst +++ /dev/null @@ -1,133 +0,0 @@ -.. index:: - single: Twig extensions - -How to write a custom Twig Extension -==================================== - -The main motivation for writing an extension is to move often used code -into a reusable class like adding support for internationalization. -An extension can define tags, filters, tests, operators, global variables, -functions, and node visitors. - -Creating an extension also makes for a better separation of code that is -executed at compilation time and code needed at runtime. As such, it makes -your code faster. - -.. tip:: - - Before writing your own extensions, have a look at the - `Twig official extension repository`_. - -Create the Extension Class --------------------------- - -.. note:: - - This cookbook describes how to write a custom Twig extension as of - Twig 1.12. If you are using an older version, please read - `Twig extensions documentation legacy`_. - -To get your custom functionality you must first create a Twig Extension class. -As an example you'll create a price filter to format a given number into price:: - - // src/Acme/DemoBundle/Twig/AcmeExtension.php - namespace Acme\DemoBundle\Twig; - - class AcmeExtension extends \Twig_Extension - { - public function getFilters() - { - return array( - new \Twig_SimpleFilter('price', array($this, 'priceFilter')), - ); - } - - public function priceFilter($number, $decimals = 0, $decPoint = '.', $thousandsSep = ',') - { - $price = number_format($number, $decimals, $decPoint, $thousandsSep); - $price = '$'.$price; - - return $price; - } - - public function getName() - { - return 'acme_extension'; - } - } - -.. tip:: - - Along with custom filters, you can also add custom `functions` and register - `global variables`. - -Register an Extension as a Service ----------------------------------- - -Now you must let the Service Container know about your newly created Twig Extension: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/DemoBundle/Resources/config/services.yml - services: - acme.twig.acme_extension: - class: Acme\DemoBundle\Twig\AcmeExtension - tags: - - { name: twig.extension } - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // src/Acme/DemoBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; - - $container - ->register('acme.twig.acme_extension', '\Acme\DemoBundle\Twig\AcmeExtension') - ->addTag('twig.extension'); - -.. note:: - - Keep in mind that Twig Extensions are not lazily loaded. This means that - there's a higher chance that you'll get a **CircularReferenceException** - or a **ScopeWideningInjectionException** if any services - (or your Twig Extension in this case) are dependent on the request service. - For more information take a look at :doc:`/cookbook/service_container/scopes`. - -Using the custom Extension --------------------------- - -Using your newly created Twig Extension is no different than any other: - -.. code-block:: jinja - - {# outputs $5,500.00 #} - {{ '5500'|price }} - -Passing other arguments to your filter: - -.. code-block:: jinja - - {# outputs $5500,2516 #} - {{ '5500.25155'|price(4, ',', '') }} - -Learning further ----------------- - -For a more in-depth look into Twig Extensions, please take a look at the -`Twig extensions documentation`_. - -.. _`Twig official extension repository`: https://github.com/fabpot/Twig-extensions -.. _`Twig extensions documentation`: http://twig.sensiolabs.org/doc/advanced.html#creating-an-extension -.. _`global variables`: http://twig.sensiolabs.org/doc/advanced.html#id1 -.. _`functions`: http://twig.sensiolabs.org/doc/advanced.html#id2 -.. _`Twig extensions documentation legacy`: http://twig.sensiolabs.org/doc/advanced_legacy.html#creating-an-extension diff --git a/cookbook/testing/bootstrap.rst b/cookbook/testing/bootstrap.rst deleted file mode 100644 index 25c8dea6d53..00000000000 --- a/cookbook/testing/bootstrap.rst +++ /dev/null @@ -1,46 +0,0 @@ -How to customize the Bootstrap Process before running Tests -=========================================================== - -Sometimes when running tests, you need to do additional bootstrap work before -running those tests. For example, if you're running a functional test and -have introduced a new translation resource, then you will need to clear your -cache before running those tests. This cookbook covers how to do that. - -First, add the following file:: - - // app/tests.bootstrap.php - if (isset($_ENV['BOOTSTRAP_CLEAR_CACHE_ENV'])) { - passthru(sprintf( - 'php "%s/console" cache:clear --env=%s --no-warmup', - __DIR__, - $_ENV['BOOTSTRAP_CLEAR_CACHE_ENV'] - )); - } - - require __DIR__.'/bootstrap.php.cache'; - -Replace the test bootstrap file ``bootstrap.php.cache`` in ``app/phpunit.xml.dist`` -with ``tests.bootstrap.php``: - -.. code-block:: xml - - - - - - -Now, you can define in your ``phpunit.xml.dist`` file which environment you want the -cache to be cleared: - -.. code-block:: xml - - - - - - -This now becomes an environment variable (i.e. ``$_ENV``) that's available -in the custom bootstrap file (``tests.bootstrap.php``). diff --git a/cookbook/testing/database.rst b/cookbook/testing/database.rst deleted file mode 100644 index 151e57c0696..00000000000 --- a/cookbook/testing/database.rst +++ /dev/null @@ -1,150 +0,0 @@ -.. index:: - single: Tests; Database - -How to test code that interacts with the Database -================================================= - -If your code interacts with the database, e.g. reads data from or stores data -into it, you need to adjust your tests to take this into account. There are -many ways how to deal with this. In a unit test, you can create a mock for -a ``Repository`` and use it to return expected objects. In a functional test, -you may need to prepare a test database with predefined values to ensure that -your test always has the same data to work with. - -.. note:: - - If you want to test your queries directly, see :doc:`/cookbook/testing/doctrine`. - -Mocking the ``Repository`` in a Unit Test ------------------------------------------ - -If you want to test code which depends on a doctrine ``Repository`` in isolation, -you need to mock the ``Repository``. Normally you inject the ``EntityManager`` -into your class and use it to get the repository. This makes things a little -more difficult as you need to mock both the ``EntityManager`` and your repository -class. - -.. tip:: - - It is possible (and a good idea) to inject your repository directly by - registering your repository as a :doc:`factory service` - This is a little bit more work to setup, but makes testing easier as you - only need to mock the repository. - -Suppose the class you want to test looks like this:: - - namespace Acme\DemoBundle\Salary; - - use Doctrine\Common\Persistence\ObjectManager; - - class SalaryCalculator - { - private $entityManager; - - public function __construct(ObjectManager $entityManager) - { - $this->entityManager = $entityManager; - } - - public function calculateTotalSalary($id) - { - $employeeRepository = $this->entityManager->getRepository('AcmeDemoBundle::Employee'); - $employee = $userRepository->find($id); - - return $employee->getSalary() + $employee->getBonus(); - } - } - -Since the ``ObjectManager`` gets injected into the class through the constructor, -it's easy to pass a mock object within a test:: - - use Acme\DemoBundle\Salary\SalaryCalculator; - - class SalaryCalculatorTest extends \PHPUnit_Framework_TestCase - { - - public function testCalculateTotalSalary() - { - // First, mock the object to be used in the test - $employee = $this->getMock('\Acme\DemoBundle\Entity\Employee'); - $employee->expects($this->once()) - ->method('getSalary') - ->will($this->returnValue(1000)); - $employee->expects($this->once()) - ->method('getBonus') - ->will($this->returnValue(1100)); - - // Now, mock the repository so it returns the mock of the employee - $employeeRepository = $this->getMockBuilder('\Doctrine\ORM\EntityRepository') - ->disableOriginalConstructor() - ->getMock(); - $employeeRepository->expects($this->once()) - ->method('find') - ->will($this->returnValue($employee)); - - // Last, mock the EntityManager to return the mock of the repository - $entityManager = $this->getMockBuilder('\Doctrine\Common\Persistence\ObjectManager') - ->disableOriginalConstructor() - ->getMock(); - $entityManager->expects($this->once()) - ->method('getRepository') - ->will($this->returnValue($employeeRepository)); - - $salaryCalculator = new SalaryCalculator($entityManager); - $this->assertEquals(1100, $salaryCalculator->calculateTotalSalary(1)); - } - } - -In this example, you are building the mocks from the inside out, first creating -the employee which gets returned by the ``Repository``, which itself gets -returned by the ``EntityManager``. This way, no real class is involved in -testing. - -Changing database Settings for functional Tests ------------------------------------------------ - -If you have functional tests, you want them to interact with a real database. -Most of the time you want to use a dedicated database connection to make sure -not to overwrite data you entered when developing the application and also -to be able to clear the database before every test. - -To do this, you can specify a database configuration which overwrites the default -configuration: - -.. code-block:: yaml - - # app/config/config_test.yml - doctrine: - # ... - dbal: - host: localhost - dbname: testdb - user: testdb - password: testdb - -.. code-block:: xml - - - - - - -.. code-block:: php - - // app/config/config_test.php - $configuration->loadFromExtension('doctrine', array( - 'dbal' => array( - 'host' => 'localhost', - 'dbname' => 'testdb', - 'user' => 'testdb', - 'password' => 'testdb', - ), - )); - -Make sure that your database runs on localhost and has the defined database and -user credentials set up. diff --git a/cookbook/testing/doctrine.rst b/cookbook/testing/doctrine.rst deleted file mode 100644 index ef9467861a3..00000000000 --- a/cookbook/testing/doctrine.rst +++ /dev/null @@ -1,66 +0,0 @@ -.. index:: - single: Tests; Doctrine - -How to test Doctrine Repositories -================================= - -Unit testing Doctrine repositories in a Symfony project is not recommended. -When you're dealing with a repository, you're really dealing with something -that's meant to be tested against a real database connection. - -Fortunately, you can easily test your queries against a real database, as -described below. - -.. _cookbook-doctrine-repo-functional-test: - -Functional Testing ------------------- - -If you need to actually execute a query, you will need to boot the kernel -to get a valid connection. In this case, you'll extend the ``WebTestCase``, -which makes all of this quite easy:: - - // src/Acme/StoreBundle/Tests/Entity/ProductRepositoryFunctionalTest.php - namespace Acme\StoreBundle\Tests\Entity; - - use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - - class ProductRepositoryFunctionalTest extends WebTestCase - { - /** - * @var \Doctrine\ORM\EntityManager - */ - private $em; - - /** - * {@inheritDoc} - */ - public function setUp() - { - static::$kernel = static::createKernel(); - static::$kernel->boot(); - $this->em = static::$kernel->getContainer() - ->get('doctrine') - ->getManager() - ; - } - - public function testSearchByCategoryName() - { - $products = $this->em - ->getRepository('AcmeStoreBundle:Product') - ->searchByCategoryName('foo') - ; - - $this->assertCount(1, $products); - } - - /** - * {@inheritDoc} - */ - protected function tearDown() - { - parent::tearDown(); - $this->em->close(); - } - } diff --git a/cookbook/testing/http_authentication.rst b/cookbook/testing/http_authentication.rst deleted file mode 100644 index 0b00422e912..00000000000 --- a/cookbook/testing/http_authentication.rst +++ /dev/null @@ -1,56 +0,0 @@ -.. index:: - single: Tests; HTTP authentication - -How to simulate HTTP Authentication in a Functional Test -======================================================== - -If your application needs HTTP authentication, pass the username and password -as server variables to ``createClient()``:: - - $client = static::createClient(array(), array( - 'PHP_AUTH_USER' => 'username', - 'PHP_AUTH_PW' => 'pa$$word', - )); - -You can also override it on a per request basis:: - - $client->request('DELETE', '/post/12', array(), array(), array( - 'PHP_AUTH_USER' => 'username', - 'PHP_AUTH_PW' => 'pa$$word', - )); - -When your application is using a ``form_login``, you can simplify your tests -by allowing your test configuration to make use of HTTP authentication. This -way you can use the above to authenticate in tests, but still have your users -login via the normal ``form_login``. The trick is to include the ``http_basic`` -key in your firewall, along with the ``form_login`` key: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_test.yml - security: - firewalls: - your_firewall_name: - http_basic: - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/config_test.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'your_firewall_name' => array( - 'http_basic' => array(), - ), - ), - )); diff --git a/cookbook/testing/index.rst b/cookbook/testing/index.rst deleted file mode 100644 index 49967eaeee1..00000000000 --- a/cookbook/testing/index.rst +++ /dev/null @@ -1,13 +0,0 @@ -Testing -======= - -.. toctree:: - :maxdepth: 2 - - http_authentication - simulating_authentication - insulating_clients - profiling - database - doctrine - bootstrap diff --git a/cookbook/testing/insulating_clients.rst b/cookbook/testing/insulating_clients.rst deleted file mode 100644 index 3411968eb80..00000000000 --- a/cookbook/testing/insulating_clients.rst +++ /dev/null @@ -1,41 +0,0 @@ -.. index:: - single: Tests; Insulating clients - -How to test the Interaction of several Clients -============================================== - -If you need to simulate an interaction between different Clients (think of a -chat for instance), create several Clients:: - - $harry = static::createClient(); - $sally = static::createClient(); - - $harry->request('POST', '/say/sally/Hello'); - $sally->request('GET', '/messages'); - - $this->assertEquals(201, $harry->getResponse()->getStatusCode()); - $this->assertRegExp('/Hello/', $sally->getResponse()->getContent()); - -This works except when your code maintains a global state or if it depends on -a third-party library that has some kind of global state. In such a case, you -can insulate your clients:: - - $harry = static::createClient(); - $sally = static::createClient(); - - $harry->insulate(); - $sally->insulate(); - - $harry->request('POST', '/say/sally/Hello'); - $sally->request('GET', '/messages'); - - $this->assertEquals(201, $harry->getResponse()->getStatusCode()); - $this->assertRegExp('/Hello/', $sally->getResponse()->getContent()); - -Insulated clients transparently execute their requests in a dedicated and -clean PHP process, thus avoiding any side-effects. - -.. tip:: - - As an insulated client is slower, you can keep one client in the main - process, and insulate the other ones. diff --git a/cookbook/testing/profiling.rst b/cookbook/testing/profiling.rst deleted file mode 100644 index d478c75ab47..00000000000 --- a/cookbook/testing/profiling.rst +++ /dev/null @@ -1,75 +0,0 @@ -.. index:: - single: Tests; Profiling - -How to use the Profiler in a Functional Test -============================================ - -It's highly recommended that a functional test only tests the Response. But if -you write functional tests that monitor your production servers, you might -want to write tests on the profiling data as it gives you a great way to check -various things and enforce some metrics. - -The Symfony2 :ref:`Profiler ` gathers a lot of data for -each request. Use this data to check the number of database calls, the time -spent in the framework, ... But before writing assertions, enable the profiler -and check that the profiler is indeed available (it is enabled by default in -the ``test`` environment):: - - class HelloControllerTest extends WebTestCase - { - public function testIndex() - { - $client = static::createClient(); - - // Enable the profiler for the next request (it does nothing if the profiler is not available) - $client->enableProfiler(); - - $crawler = $client->request('GET', '/hello/Fabien'); - - // ... write some assertions about the Response - - // Check that the profiler is enabled - if ($profile = $client->getProfile()) { - // check the number of requests - $this->assertLessThan( - 10, - $profile->getCollector('db')->getQueryCount() - ); - - // check the time spent in the framework - $this->assertLessThan( - 500, - $profile->getCollector('time')->getTotalTime() - ); - } - } - } - -If a test fails because of profiling data (too many DB queries for instance), -you might want to use the Web Profiler to analyze the request after the tests -finish. It's easy to achieve if you embed the token in the error message:: - - $this->assertLessThan( - 30, - $profile->get('db')->getQueryCount(), - sprintf( - 'Checks that query count is less than 30 (token %s)', - $profile->getToken() - ) - ); - -.. caution:: - - The profiler store can be different depending on the environment - (especially if you use the SQLite store, which is the default configured - one). - -.. note:: - - The profiler information is available even if you insulate the client or - if you use an HTTP layer for your tests. - -.. tip:: - - Read the API for built-in :doc:`data collectors` - to learn more about their interfaces. diff --git a/cookbook/testing/simulating_authentication.rst b/cookbook/testing/simulating_authentication.rst deleted file mode 100644 index 1be4eda5536..00000000000 --- a/cookbook/testing/simulating_authentication.rst +++ /dev/null @@ -1,61 +0,0 @@ -.. index:: - single: Tests; Simulating authentication - -How to simulate Authentication with a Token in a Functional Test -================================================================ - -Authenticating requests in functional tests might slow down the suite. -It could become an issue especially when ``form_login`` is used, since -it requires additional requests to fill in and submit the form. - -One of the solutions is to configure your firewall to use ``http_basic`` in -the test environment as explained in -:doc:`/cookbook/testing/http_authentication`. -Another way would be to create a token yourself and store it in a session. -While doing this, you have to make sure that appropriate cookie is sent -with a request. The following example demonstrates this technique:: - - // src/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php - namespace Acme\DemoBundle\Tests\Controller; - - use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - use Symfony\Component\BrowserKit\Cookie; - use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; - - class DemoControllerTest extends WebTestCase - { - private $client = null; - - public function setUp() - { - $this->client = static::createClient(); - } - - public function testSecuredHello() - { - $this->logIn(); - - $this->client->request('GET', '/demo/secured/hello/Fabien'); - - $this->assertTrue($this->client->getResponse()->isSuccessful()); - $this->assertGreaterThan(0, $crawler->filter('html:contains("Hello Fabien")')->count()); - } - - private function logIn() - { - $session = $this->client->getContainer()->get('session'); - - $firewall = 'secured_area'; - $token = new UsernamePasswordToken('admin', null, $firewall, array('ROLE_ADMIN')); - $session->set('_security_'.$firewall, serialize($token)); - $session->save(); - - $cookie = new Cookie($session->getName(), $session->getId()); - $this->client->getCookieJar()->set($cookie); - } - } - -.. note:: - - The technique described in :doc:`/cookbook/testing/http_authentication`. - is cleaner and therefore preferred way. diff --git a/cookbook/validation/custom_constraint.rst b/cookbook/validation/custom_constraint.rst deleted file mode 100644 index f658deafec3..00000000000 --- a/cookbook/validation/custom_constraint.rst +++ /dev/null @@ -1,251 +0,0 @@ -.. index:: - single: Validation; Custom constraints - -How to create a Custom Validation Constraint -============================================ - -You can create a custom constraint by extending the base constraint class, -:class:`Symfony\\Component\\Validator\\Constraint`. -As an example you're going to create a simple validator that checks if a string -contains only alphanumeric characters. - -Creating Constraint class -------------------------- - -First you need to create a Constraint class and extend :class:`Symfony\\Component\\Validator\\Constraint`:: - - // src/Acme/DemoBundle/Validator/Constraints/ContainsAlphanumeric.php - namespace Acme\DemoBundle\Validator\Constraints; - - use Symfony\Component\Validator\Constraint; - - /** - * @Annotation - */ - class ContainsAlphanumeric extends Constraint - { - public $message = 'The string "%string%" contains an illegal character: it can only contain letters or numbers.'; - } - -.. note:: - - The ``@Annotation`` annotation is necessary for this new constraint in - order to make it available for use in classes via annotations. - Options for your constraint are represented as public properties on the - constraint class. - -Creating the Validator itself ------------------------------ - -As you can see, a constraint class is fairly minimal. The actual validation is -performed by another "constraint validator" class. The constraint validator -class is specified by the constraint's ``validatedBy()`` method, which -includes some simple default logic:: - - // in the base Symfony\Component\Validator\Constraint class - public function validatedBy() - { - return get_class($this).'Validator'; - } - -In other words, if you create a custom ``Constraint`` (e.g. ``MyConstraint``), -Symfony2 will automatically look for another class, ``MyConstraintValidator`` -when actually performing the validation. - -The validator class is also simple, and only has one required method: ``validate``:: - - // src/Acme/DemoBundle/Validator/Constraints/ContainsAlphanumericValidator.php - namespace Acme\DemoBundle\Validator\Constraints; - - use Symfony\Component\Validator\Constraint; - use Symfony\Component\Validator\ConstraintValidator; - - class ContainsAlphanumericValidator extends ConstraintValidator - { - public function validate($value, Constraint $constraint) - { - if (!preg_match('/^[a-zA-Za0-9]+$/', $value, $matches)) { - $this->context->addViolation($constraint->message, array('%string%' => $value)); - } - } - } - -.. note:: - - The ``validate`` method does not return a value; instead, it adds violations - to the validator's ``context`` property with an ``addViolation`` method - call if there are validation failures. Therefore, a value could be considered - as being valid if it causes no violations to be added to the context. - The first parameter of the ``addViolation`` call is the error message to - use for that violation. - -Using the new Validator ------------------------ - -Using custom validators is very easy, just as the ones provided by Symfony2 itself: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\DemoBundle\Entity\AcmeEntity: - properties: - name: - - NotBlank: ~ - - Acme\DemoBundle\Validator\Constraints\ContainsAlphanumeric: ~ - - .. code-block:: php-annotations - - // src/Acme/DemoBundle/Entity/AcmeEntity.php - use Symfony\Component\Validator\Constraints as Assert; - use Acme\DemoBundle\Validator\Constraints as AcmeAssert; - - class AcmeEntity - { - // ... - - /** - * @Assert\NotBlank - * @AcmeAssert\ContainsAlphanumeric - */ - protected $name; - - // ... - } - - .. code-block:: xml - - - - - - - - - - - - - - .. code-block:: php - - // src/Acme/DemoBundle/Entity/AcmeEntity.php - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\NotBlank; - use Acme\DemoBundle\Validator\Constraints\ContainsAlphanumeric; - - class AcmeEntity - { - public $name; - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('name', new NotBlank()); - $metadata->addPropertyConstraint('name', new ContainsAlphanumeric()); - } - } - -If your constraint contains options, then they should be public properties -on the custom Constraint class you created earlier. These options can be -configured like options on core Symfony constraints. - -Constraint Validators with Dependencies -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If your constraint validator has dependencies, such as a database connection, -it will need to be configured as a service in the dependency injection -container. This service must include the ``validator.constraint_validator`` -tag and an ``alias`` attribute: - -.. configuration-block:: - - .. code-block:: yaml - - services: - validator.unique.your_validator_name: - class: Fully\Qualified\Validator\Class\Name - tags: - - { name: validator.constraint_validator, alias: alias_name } - - .. code-block:: xml - - - - - - - .. code-block:: php - - $container - ->register('validator.unique.your_validator_name', 'Fully\Qualified\Validator\Class\Name') - ->addTag('validator.constraint_validator', array('alias' => 'alias_name')); - -Your constraint class should now use this alias to reference the appropriate -validator:: - - public function validatedBy() - { - return 'alias_name'; - } - -As mentioned above, Symfony2 will automatically look for a class named after -the constraint, with ``Validator`` appended. If your constraint validator -is defined as a service, it's important that you override the -``validatedBy()`` method to return the alias used when defining your service, -otherwise Symfony2 won't use the constraint validator service, and will -instantiate the class instead, without any dependencies injected. - -Class Constraint Validator -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Beside validating a class property, a constraint can have a class scope by -providing a target:: - - public function getTargets() - { - return self::CLASS_CONSTRAINT; - } - -With this, the validator ``validate()`` method gets an object as its first argument:: - - class ProtocolClassValidator extends ConstraintValidator - { - public function validate($protocol, Constraint $constraint) - { - if ($protocol->getFoo() != $protocol->getBar()) { - $this->context->addViolationAt('foo', $constraint->message, array(), null); - } - } - } - -Note that a class constraint validator is applied to the class itself, and -not to the property: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\DemoBundle\Entity\AcmeEntity: - constraints: - Acme\DemoBundle\Validator\Constraints\ContainsAlphanumeric: ~ - - .. code-block:: php-annotations - - /** - * @AcmeAssert\ContainsAlphanumeric - */ - class AcmeEntity - { - // ... - } - - .. code-block:: xml - - - - - diff --git a/cookbook/validation/index.rst b/cookbook/validation/index.rst deleted file mode 100644 index 3712610011d..00000000000 --- a/cookbook/validation/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Validation -========== - -.. toctree:: - :maxdepth: 2 - - custom_constraint diff --git a/cookbook/web_services/index.rst b/cookbook/web_services/index.rst deleted file mode 100644 index e92c057cc4a..00000000000 --- a/cookbook/web_services/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Web Services -============ - -.. toctree:: - :maxdepth: 2 - - php_soap_extension diff --git a/cookbook/web_services/php_soap_extension.rst b/cookbook/web_services/php_soap_extension.rst deleted file mode 100644 index 246f3b64fbc..00000000000 --- a/cookbook/web_services/php_soap_extension.rst +++ /dev/null @@ -1,197 +0,0 @@ -.. index:: - single: Web Services; SOAP - -How to Create a SOAP Web Service in a Symfony2 Controller -========================================================= - -Setting up a controller to act as a SOAP server is simple with a couple -tools. You must, of course, have the `PHP SOAP`_ extension installed. -As the PHP SOAP extension can not currently generate a WSDL, you must either -create one from scratch or use a 3rd party generator. - -.. note:: - - There are several SOAP server implementations available for use with - PHP. `Zend SOAP`_ and `NuSOAP`_ are two examples. Although the PHP SOAP - extension is used in these examples, the general idea should still - be applicable to other implementations. - -SOAP works by exposing the methods of a PHP object to an external entity -(i.e. the person using the SOAP service). To start, create a class - ``HelloService`` - -which represents the functionality that you'll expose in your SOAP service. -In this case, the SOAP service will allow the client to call a method called -``hello``, which happens to send an email:: - - // src/Acme/SoapBundle/Services/HelloService.php - namespace Acme\SoapBundle\Services; - - class HelloService - { - private $mailer; - - public function __construct(\Swift_Mailer $mailer) - { - $this->mailer = $mailer; - } - - public function hello($name) - { - - $message = \Swift_Message::newInstance() - ->setTo('me@example.com') - ->setSubject('Hello Service') - ->setBody($name . ' says hi!'); - - $this->mailer->send($message); - - - return 'Hello, '.$name; - } - } - -Next, you can train Symfony to be able to create an instance of this class. -Since the class sends an e-mail, it's been designed to accept a ``Swift_Mailer`` -instance. Using the Service Container, you can configure Symfony to construct -a ``HelloService`` object properly: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - services: - hello_service: - class: Acme\SoapBundle\Services\HelloService - arguments: ["@mailer"] - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container - ->register('hello_service', 'Acme\SoapBundle\Services\HelloService') - ->addArgument(new Reference('mailer')); - - -Below is an example of a controller that is capable of handling a SOAP -request. If ``indexAction()`` is accessible via the route ``/soap``, then the -WSDL document can be retrieved via ``/soap?wsdl``. - -.. code-block:: php - - namespace Acme\SoapBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\HttpFoundation\Response; - - class HelloServiceController extends Controller - { - public function indexAction() - { - $server = new \SoapServer('/path/to/hello.wsdl'); - $server->setObject($this->get('hello_service')); - - $response = new Response(); - $response->headers->set('Content-Type', 'text/xml; charset=ISO-8859-1'); - - ob_start(); - $server->handle(); - $response->setContent(ob_get_clean()); - - return $response; - } - } - -Take note of the calls to ``ob_start()`` and ``ob_get_clean()``. These -methods control `output buffering`_ which allows you to "trap" the echoed -output of ``$server->handle()``. This is necessary because Symfony expects -your controller to return a ``Response`` object with the output as its "content". -You must also remember to set the "Content-Type" header to "text/xml", as -this is what the client will expect. So, you use ``ob_start()`` to start -buffering the STDOUT and use ``ob_get_clean()`` to dump the echoed output -into the content of the Response and clear the output buffer. Finally, you're -ready to return the ``Response``. - -Below is an example calling the service using `NuSOAP`_ client. This example -assumes that the ``indexAction`` in the controller above is accessible via the -route ``/soap``:: - - $client = new \Soapclient('http://example.com/app.php/soap?wsdl', true); - - $result = $client->call('hello', array('name' => 'Scott')); - -An example WSDL is below. - -.. code-block:: xml - - - - - - - - - - - - - - - - - - - - - - Hello World - - - - - - - - - - - - - - - - - - - - - - - - - - - - -.. _`PHP SOAP`: http://php.net/manual/en/book.soap.php -.. _`NuSOAP`: http://sourceforge.net/projects/nusoap -.. _`output buffering`: http://php.net/manual/en/book.outcontrol.php -.. _`Zend SOAP`: http://framework.zend.com/manual/en/zend.soap.server.html diff --git a/cookbook/workflow/_vendor_deps.rst.inc b/cookbook/workflow/_vendor_deps.rst.inc deleted file mode 100644 index 90b8dfa264a..00000000000 --- a/cookbook/workflow/_vendor_deps.rst.inc +++ /dev/null @@ -1,77 +0,0 @@ -Managing Vendor Libraries with composer.json --------------------------------------------- - -How does it work? -~~~~~~~~~~~~~~~~~ - -Every Symfony project uses a group of third-party "vendor" libraries. One -way or another the goal is to download these files into your ``vendor/`` -directory and, ideally, to give you some sane way to manage the exact version -you need for each. - -By default, these libraries are downloaded by running a ``php composer.phar install`` -"downloader" binary. This ``composer.phar`` file is from a library called -`Composer`_ and you can read more about installing it in the :ref:`Installation` -chapter. - -The ``composer.phar`` file reads from the ``composer.json`` file at the root -of your project. This is an JSON-formatted file, which holds a list of each -of the external packages you need, the version to be downloaded and more. -The ``composer.phar`` file also reads from a ``composer.lock`` file, which -allows you to pin each library to an **exact** version. In fact, if a ``composer.lock`` -file exists, the versions inside will override those in ``composer.json``. -To upgrade your libraries to new versions, run ``php composer.phar update``. - -.. tip:: - - If you want to add a new package to your application, modify the ``composer.json`` - file: - - .. code-block:: json - - { - "require": { - ... - "doctrine/doctrine-fixtures-bundle": "@dev" - } - } - - and then execute the ``update`` command for this specific package, i.e.: - - .. code-block:: bash - - $ php composer.phar update doctrine/doctrine-fixtures-bundle - - You can also combine both steps into a single command: - - .. code-block:: bash - - $ php composer.phar require doctrine/doctrine-fixtures-bundle:@dev - -To learn more about Composer, see `GetComposer.org`_: - -It's important to realize that these vendor libraries are *not* actually part -of *your* repository. Instead, they're simply un-tracked files that are downloaded -into the ``vendor/``. But since all the information needed to download these -files is saved in ``composer.json`` and ``composer.lock`` (which *are* stored -in the repository), any other developer can use the project, run ``php composer.phar install``, -and download the exact same set of vendor libraries. This means that you're -controlling exactly what each vendor library looks like, without needing to -actually commit them to *your* repository. - -So, whenever a developer uses your project, he/she should run the ``php composer.phar install`` -script to ensure that all of the needed vendor libraries are downloaded. - -.. sidebar:: Upgrading Symfony - - Since Symfony is just a group of third-party libraries and third-party - libraries are entirely controlled through ``composer.json`` and ``composer.lock``, - upgrading Symfony means simply upgrading each of these files to match - their state in the latest Symfony Standard Edition. - - Of course, if you've added new entries to ``composer.json``, be sure - to replace only the original parts (i.e. be sure not to also delete any of - your custom entries). - -.. _Composer: http://getcomposer.org/ -.. _GetComposer.org: http://getcomposer.org/ diff --git a/cookbook/workflow/index.rst b/cookbook/workflow/index.rst deleted file mode 100644 index 6b2382ae235..00000000000 --- a/cookbook/workflow/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Workflow -======== - -.. toctree:: - :maxdepth: 2 - - new_project_git - new_project_svn diff --git a/cookbook/workflow/new_project_git.rst b/cookbook/workflow/new_project_git.rst deleted file mode 100644 index 672a012f1c7..00000000000 --- a/cookbook/workflow/new_project_git.rst +++ /dev/null @@ -1,123 +0,0 @@ -.. index:: - single: Workflow; Git - -How to Create and store a Symfony2 Project in git -================================================= - -.. tip:: - - Though this entry is specifically about git, the same generic principles - will apply if you're storing your project in Subversion. - -Once you've read through :doc:`/book/page_creation` and become familiar with -using Symfony, you'll no-doubt be ready to start your own project. In this -cookbook article, you'll learn the best way to start a new Symfony2 project -that's stored using the `git`_ source control management system. - -Initial Project Setup ---------------------- - -To get started, you'll need to download Symfony and initialize your local -git repository: - -1. Download the `Symfony2 Standard Edition`_ without vendors. - -2. Unzip/untar the distribution. It will create a folder called Symfony with - your new project structure, config files, etc. Rename it to whatever you like. - -3. Create a new file called ``.gitignore`` at the root of your new project - (e.g. next to the ``composer.json`` file) and paste the following into it. Files - matching these patterns will be ignored by git: - - .. code-block:: text - - /web/bundles/ - /app/bootstrap* - /app/cache/* - /app/logs/* - /vendor/ - /app/config/parameters.yml - -.. tip:: - - You may also want to create a .gitignore file that can be used system-wide, - in which case, you can find more information here: `Github .gitignore`_ - This way you can exclude files/folders often used by your IDE for all of your projects. - -4. Copy ``app/config/parameters.yml`` to ``app/config/parameters.yml.dist``. - The ``parameters.yml`` file is ignored by git (see above) so that machine-specific - settings like database passwords aren't committed. By creating the ``parameters.yml.dist`` - file, new developers can quickly clone the project, copy this file to - ``parameters.yml``, customize it, and start developing. - -5. Initialize your git repository: - - .. code-block:: bash - - $ git init - -6. Add all of the initial files to git: - - .. code-block:: bash - - $ git add . - -7. Create an initial commit with your started project: - - .. code-block:: bash - - $ git commit -m "Initial commit" - -8. Finally, download all of the third-party vendor libraries by - executing composer. For details, see :ref:`installation-updating-vendors`. - -At this point, you have a fully-functional Symfony2 project that's correctly -committed to git. You can immediately begin development, committing the new -changes to your git repository. - -You can continue to follow along with the :doc:`/book/page_creation` chapter -to learn more about how to configure and develop inside your application. - -.. tip:: - - The Symfony2 Standard Edition comes with some example functionality. To - remove the sample code, follow the instructions in the - ":doc:`/cookbook/bundles/remove`" article. - -.. _cookbook-managing-vendor-libraries: - -.. include:: _vendor_deps.rst.inc - -Vendors and Submodules -~~~~~~~~~~~~~~~~~~~~~~ - -Instead of using the ``composer.json`` system for managing your vendor -libraries, you may instead choose to use native `git submodules`_. There -is nothing wrong with this approach, though the ``composer.json`` system -is the official way to solve this problem and probably much easier to -deal with. Unlike git submodules, ``Composer`` is smart enough to calculate -which libraries depend on which other libraries. - -Storing your Project on a Remote Server ---------------------------------------- - -You now have a fully-functional Symfony2 project stored in git. However, -in most cases, you'll also want to store your project on a remote server -both for backup purposes, and so that other developers can collaborate on -the project. - -The easiest way to store your project on a remote server is via `GitHub`_. -Public repositories are free, however you will need to pay a monthly fee -to host private repositories. - -Alternatively, you can store your git repository on any server by creating -a `barebones repository`_ and then pushing to it. One library that helps -manage this is `Gitolite`_. - -.. _`git`: http://git-scm.com/ -.. _`Symfony2 Standard Edition`: http://symfony.com/download -.. _`git submodules`: http://git-scm.com/book/en/Git-Tools-Submodules -.. _`GitHub`: https://github.com/ -.. _`barebones repository`: http://git-scm.com/book/en/Git-Basics-Getting-a-Git-Repository -.. _`Gitolite`: https://github.com/sitaramc/gitolite -.. _`Github .gitignore`: https://help.github.com/articles/ignoring-files diff --git a/cookbook/workflow/new_project_svn.rst b/cookbook/workflow/new_project_svn.rst deleted file mode 100644 index 9c94a714318..00000000000 --- a/cookbook/workflow/new_project_svn.rst +++ /dev/null @@ -1,150 +0,0 @@ -.. index:: - single: Workflow; Subversion - -How to Create and store a Symfony2 Project in Subversion -======================================================== - -.. tip:: - - This entry is specifically about Subversion, and based on principles found - in :doc:`/cookbook/workflow/new_project_git`. - -Once you've read through :doc:`/book/page_creation` and become familiar with -using Symfony, you'll no-doubt be ready to start your own project. The -preferred method to manage Symfony2 projects is using `git`_ but some prefer -to use `Subversion`_ which is totally fine!. In this cookbook article, you'll -learn how to manage your project using `svn`_ in a similar manner you -would do with `git`_. - -.. tip:: - - This is **a** method to tracking your Symfony2 project in a Subversion - repository. There are several ways to do and this one is simply one that - works. - -The Subversion Repository -------------------------- - -For this article it's assumed that your repository layout follows the -widespread standard structure: - -.. code-block:: text - - myproject/ - branches/ - tags/ - trunk/ - -.. tip:: - - Most subversion hosting should follow this standard practice. This - is the recommended layout in `Version Control with Subversion`_ and the - layout used by most free hosting (see :ref:`svn-hosting`). - -Initial Project Setup ---------------------- - -To get started, you'll need to download Symfony2 and get the basic Subversion setup: - -1. Download the `Symfony2 Standard Edition`_ with or without vendors. - -2. Unzip/untar the distribution. It will create a folder called Symfony with - your new project structure, config files, etc. Rename it to whatever you - like. - -3. Checkout the Subversion repository that will host this project. Let's say it - is hosted on `Google code`_ and called ``myproject``: - - .. code-block:: bash - - $ svn checkout http://myproject.googlecode.com/svn/trunk myproject - -4. Copy the Symfony2 project files in the subversion folder: - - .. code-block:: bash - - $ mv Symfony/* myproject/ - -5. Let's now set the ignore rules. Not everything *should* be stored in your - subversion repository. Some files (like the cache) are generated and - others (like the database configuration) are meant to be customized - on each machine. This makes use of the ``svn:ignore`` property, so that - specific files can be ignored. - - .. code-block:: bash - - $ cd myproject/ - $ svn add --depth=empty app app/cache app/logs app/config web - - $ svn propset svn:ignore "vendor" . - $ svn propset svn:ignore "bootstrap*" app/ - $ svn propset svn:ignore "parameters.yml" app/config/ - $ svn propset svn:ignore "*" app/cache/ - $ svn propset svn:ignore "*" app/logs/ - - $ svn propset svn:ignore "bundles" web - - $ svn ci -m "commit basic Symfony ignore list (vendor, app/bootstrap*, app/config/parameters.yml, app/cache/*, app/logs/*, web/bundles)" - -6. The rest of the files can now be added and committed to the project: - - .. code-block:: bash - - $ svn add --force . - $ svn ci -m "add basic Symfony Standard 2.X.Y" - -7. Copy ``app/config/parameters.yml`` to ``app/config/parameters.yml.dist``. - The ``parameters.yml`` file is ignored by svn (see above) so that - machine-specific settings like database passwords aren't committed. By - creating the ``parameters.yml.dist`` file, new developers can quickly clone - the project, copy this file to ``parameters.yml``, customize it, and start - developing. - -8. Finally, download all of the third-party vendor libraries by - executing composer. For details, see :ref:`installation-updating-vendors`. - -.. tip:: - - If you rely on any "dev" versions, then git may be used to install - those libraries, since there is no archive available for download. - -At this point, you have a fully-functional Symfony2 project stored in your -Subversion repository. The development can start with commits in the Subversion -repository. - -You can continue to follow along with the :doc:`/book/page_creation` chapter -to learn more about how to configure and develop inside your application. - -.. tip:: - - The Symfony2 Standard Edition comes with some example functionality. To - remove the sample code, follow the instructions in the - ":doc:`/cookbook/bundles/remove`" article. - -.. include:: _vendor_deps.rst.inc - -.. _svn-hosting: - -Subversion hosting solutions ----------------------------- - -The biggest difference between `git`_ and `svn`_ is that Subversion *needs* a -central repository to work. You then have several solutions: - -- Self hosting: create your own repository and access it either through the - filesystem or the network. To help in this task you can read `Version Control - with Subversion`_. - -- Third party hosting: there are a lot of serious free hosting solutions - available like `GitHub`_, `Google code`_, `SourceForge`_ or `Gna`_. Some of them offer - git hosting as well. - -.. _`git`: http://git-scm.com/ -.. _`svn`: http://subversion.apache.org/ -.. _`Subversion`: http://subversion.apache.org/ -.. _`Symfony2 Standard Edition`: http://symfony.com/download -.. _`Version Control with Subversion`: http://svnbook.red-bean.com/ -.. _`GitHub`: https://github.com/ -.. _`Google code`: http://code.google.com/hosting/ -.. _`SourceForge`: http://sourceforge.net/ -.. _`Gna`: http://gna.org/ diff --git a/create_framework/dependency_injection.rst b/create_framework/dependency_injection.rst new file mode 100644 index 00000000000..de3c4e11e4e --- /dev/null +++ b/create_framework/dependency_injection.rst @@ -0,0 +1,259 @@ +The DependencyInjection Component +================================= + +In the previous chapter, we emptied the ``Simplex\Framework`` class by +extending the ``HttpKernel`` class from the eponymous component. Seeing this +empty class, you might be tempted to move some code from the front controller +to it:: + + // example.com/src/Simplex/Framework.php + namespace Simplex; + + use Symfony\Component\EventDispatcher\EventDispatcher; + use Symfony\Component\HttpFoundation; + use Symfony\Component\HttpFoundation\RequestStack; + use Symfony\Component\HttpKernel; + use Symfony\Component\Routing; + + class Framework extends HttpKernel\HttpKernel + { + public function __construct($routes) + { + $context = new Routing\RequestContext(); + $matcher = new Routing\Matcher\UrlMatcher($routes, $context); + $requestStack = new RequestStack(); + + $controllerResolver = new HttpKernel\Controller\ControllerResolver(); + $argumentResolver = new HttpKernel\Controller\ArgumentResolver(); + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber(new HttpKernel\EventListener\ErrorListener( + 'Calendar\Controller\ErrorController::exception' + )); + $dispatcher->addSubscriber(new HttpKernel\EventListener\RouterListener($matcher, $requestStack)); + $dispatcher->addSubscriber(new HttpKernel\EventListener\ResponseListener('UTF-8')); + $dispatcher->addSubscriber(new StringResponseListener()); + + parent::__construct($dispatcher, $controllerResolver, $requestStack, $argumentResolver); + } + } + +The front controller code would become more concise:: + + // example.com/web/front.php + require_once __DIR__.'/../vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + + $request = Request::createFromGlobals(); + $routes = include __DIR__.'/../src/app.php'; + + $framework = new Simplex\Framework($routes); + + $framework->handle($request)->send(); + +Having a concise front controller allows you to have several front controllers +for a single application. Why would it be useful? To allow having different +configuration for the development environment and the production one for +instance. In the development environment, you might want to have error +reporting turned on and errors displayed in the browser to ease debugging:: + + ini_set('display_errors', 1); + error_reporting(-1); + +... but you certainly won't want that same configuration on the production +environment. Having two different front controllers gives you the opportunity +to have a slightly different configuration for each of them. + +So, moving code from the front controller to the framework class makes our +framework more configurable, but at the same time, it introduces a lot of +issues: + +* We are not able to register custom listeners anymore as the dispatcher is + not available outside the Framework class (a workaround could be the + adding of a ``Framework::getEventDispatcher()`` method); + +* We have lost the flexibility we had before; you cannot change the + implementation of the ``UrlMatcher`` or of the ``ControllerResolver`` + anymore; + +* Related to the previous point, we cannot test our framework without much + effort anymore as it's impossible to mock internal objects; + +* We cannot change the charset passed to ``ResponseListener`` anymore (a + workaround could be to pass it as a constructor argument). + +The previous code did not exhibit the same issues because we used dependency +injection; all dependencies of our objects were injected into their +constructors (for instance, the event dispatchers were injected into the +framework so that we had total control of its creation and configuration). + +Does it mean that we have to make a choice between flexibility, customization, +ease of testing and not to copy and paste the same code into each application +front controller? As you might expect, there is a solution. We can solve all +these issues and some more by using the Symfony dependency injection +container: + +.. code-block:: terminal + + $ composer require symfony/dependency-injection + +Create a new file to host the dependency injection container configuration:: + + // example.com/src/container.php + use Simplex\Framework; + use Symfony\Component\DependencyInjection; + use Symfony\Component\DependencyInjection\Reference; + use Symfony\Component\EventDispatcher; + use Symfony\Component\HttpFoundation; + use Symfony\Component\HttpKernel; + use Symfony\Component\Routing; + + $container = new DependencyInjection\ContainerBuilder(); + $container->register('context', Routing\RequestContext::class); + $container->register('matcher', Routing\Matcher\UrlMatcher::class) + ->setArguments([$routes, new Reference('context')]) + ; + $container->register('request_stack', HttpFoundation\RequestStack::class); + $container->register('controller_resolver', HttpKernel\Controller\ControllerResolver::class); + $container->register('argument_resolver', HttpKernel\Controller\ArgumentResolver::class); + + $container->register('listener.router', HttpKernel\EventListener\RouterListener::class) + ->setArguments([new Reference('matcher'), new Reference('request_stack')]) + ; + $container->register('listener.response', HttpKernel\EventListener\ResponseListener::class) + ->setArguments(['UTF-8']) + ; + $container->register('listener.exception', HttpKernel\EventListener\ErrorListener::class) + ->setArguments(['Calendar\Controller\ErrorController::exception']) + ; + $container->register('dispatcher', EventDispatcher\EventDispatcher::class) + ->addMethodCall('addSubscriber', [new Reference('listener.router')]) + ->addMethodCall('addSubscriber', [new Reference('listener.response')]) + ->addMethodCall('addSubscriber', [new Reference('listener.exception')]) + ; + $container->register('framework', Framework::class) + ->setArguments([ + new Reference('dispatcher'), + new Reference('controller_resolver'), + new Reference('request_stack'), + new Reference('argument_resolver'), + ]) + ; + + return $container; + +The goal of this file is to configure your objects and their dependencies. +Nothing is instantiated during this configuration step. This is purely a +static description of the objects you need to manipulate and how to create +them. Objects will be created on-demand when you access them from the +container or when the container needs them to create other objects. + +For instance, to create the router listener, we tell Symfony that its class +name is ``Symfony\Component\HttpKernel\EventListener\RouterListener`` and +that its constructor takes a matcher object (``new Reference('matcher')``). As +you can see, each object is referenced by a name, a string that uniquely +identifies each object. The name allows us to get an object and to reference +it in other object definitions. + +.. note:: + + By default, every time you get an object from the container, it returns + the exact same instance. That's because a container manages your "global" + objects. + +The front controller is now only about wiring everything together:: + + // example.com/web/front.php + require_once __DIR__.'/../vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + + $routes = include __DIR__.'/../src/app.php'; + $container = include __DIR__.'/../src/container.php'; + + $request = Request::createFromGlobals(); + + $response = $container->get('framework')->handle($request); + + $response->send(); + +As all the objects are now created in the dependency injection container, the +framework code should be the previous simple version:: + + // example.com/src/Simplex/Framework.php + namespace Simplex; + + use Symfony\Component\HttpKernel\HttpKernel; + + class Framework extends HttpKernel + { + } + +.. note:: + + If you want a light alternative for your container, consider `Pimple`_, a + simple dependency injection container in about 60 lines of PHP code. + +Now, here is how you can register a custom listener in the front controller:: + + // ... + use Simplex\StringResponseListener; + + $container->register('listener.string_response', StringResponseListener::class); + $container->getDefinition('dispatcher') + ->addMethodCall('addSubscriber', [new Reference('listener.string_response')]) + ; + +Besides describing your objects, the dependency injection container can also be +configured via parameters. Let's create one that defines if we are in debug +mode or not:: + + $container->setParameter('debug', true); + + echo $container->getParameter('debug'); + +These parameters can be used when defining object definitions. Let's make the +charset configurable:: + + // ... + $container->register('listener.response', HttpKernel\EventListener\ResponseListener::class) + ->setArguments(['%charset%']) + ; + +After this change, you must set the charset before using the response listener +object:: + + $container->setParameter('charset', 'UTF-8'); + +Instead of relying on the convention that the routes are defined by the +``$routes`` variables, let's use a parameter again:: + + // ... + $container->register('matcher', Routing\Matcher\UrlMatcher::class) + ->setArguments(['%routes%', new Reference('context')]) + ; + +And the related change in the front controller:: + + $container->setParameter('routes', include __DIR__.'/../src/app.php'); + +We have barely scratched the surface of what you can do with the +container: from class names as parameters, to overriding existing object +definitions, from shared service support to dumping a container to a plain PHP class, +and much more. The Symfony dependency injection container is really powerful +and is able to manage any kind of PHP class. + +Don't yell at me if you don't want to use a dependency injection container in +your framework. If you don't like it, don't use it. It's your framework, not +mine. + +This is (already) the last chapter of this book on creating a framework on top +of the Symfony components. I'm aware that many topics have not been covered +in great details, but hopefully it gives you enough information to get started +on your own and to better understand how the Symfony framework works +internally. + +Have fun! + +.. _`Pimple`: https://github.com/silexphp/Pimple diff --git a/create_framework/event_dispatcher.rst b/create_framework/event_dispatcher.rst new file mode 100644 index 00000000000..650e4c7554e --- /dev/null +++ b/create_framework/event_dispatcher.rst @@ -0,0 +1,296 @@ +The EventDispatcher Component +============================= + +Our framework is still missing a major characteristic of any good framework: +*extensibility*. Being extensible means that the developer should be able to +hook into the framework life cycle to modify the way the request is handled. + +What kind of hooks are we talking about? Authentication or caching for +instance. To be flexible, hooks must be plug-and-play; the ones you "register" +for an application are different from the next one depending on your specific +needs. Many software have a similar concept like Drupal or WordPress. In some +languages, there is even a standard like `WSGI`_ in Python or `Rack`_ in Ruby. + +As there is no standard for PHP, we are going to use a well-known design +pattern, the *Mediator*, to allow any kind of behaviors to be attached to our +framework; the Symfony EventDispatcher Component implements a lightweight +version of this pattern: + +.. code-block:: terminal + + $ composer require symfony/event-dispatcher + +How does it work? The *dispatcher*, the central object of the event dispatcher +system, notifies *listeners* of an *event* dispatched to it. Put another way: +your code dispatches an event to the dispatcher, the dispatcher notifies all +registered listeners for the event, and each listener does whatever it wants +with the event. + +As an example, let's create a listener that transparently adds the Google +Analytics code to all responses. + +To make it work, the framework must dispatch an event just before returning +the Response instance:: + + // example.com/src/Simplex/Framework.php + namespace Simplex; + + use Symfony\Component\EventDispatcher\EventDispatcher; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; + use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; + use Symfony\Component\Routing\Exception\ResourceNotFoundException; + use Symfony\Component\Routing\Matcher\UrlMatcherInterface; + + class Framework + { + public function __construct( + private EventDispatcher $dispatcher, + private UrlMatcherInterface $matcher, + private ControllerResolverInterface $controllerResolver, + private ArgumentResolverInterface $argumentResolver, + ) { + } + + public function handle(Request $request): Response + { + $this->matcher->getContext()->fromRequest($request); + + try { + $request->attributes->add($this->matcher->match($request->getPathInfo())); + + $controller = $this->controllerResolver->getController($request); + $arguments = $this->argumentResolver->getArguments($request, $controller); + + $response = call_user_func_array($controller, $arguments); + } catch (ResourceNotFoundException $exception) { + $response = new Response('Not Found', 404); + } catch (\Exception $exception) { + $response = new Response('An error occurred', 500); + } + + // dispatch a response event + $this->dispatcher->dispatch(new ResponseEvent($response, $request), 'response'); + + return $response; + } + } + +Each time the framework handles a Request, a ``ResponseEvent`` event is +now dispatched:: + + // example.com/src/Simplex/ResponseEvent.php + namespace Simplex; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Contracts\EventDispatcher\Event; + + class ResponseEvent extends Event + { + public function __construct( + private Response $response, + private Request $request, + ) { + } + + public function getResponse(): Response + { + return $this->response; + } + + public function getRequest(): Request + { + return $this->request; + } + } + +The last step is the creation of the dispatcher in the front controller and +the registration of a listener for the ``response`` event:: + + // example.com/web/front.php + require_once __DIR__.'/../vendor/autoload.php'; + + // ... + + use Symfony\Component\EventDispatcher\EventDispatcher; + + $dispatcher = new EventDispatcher(); + $dispatcher->addListener('response', function (Simplex\ResponseEvent $event): void { + $response = $event->getResponse(); + + if ($response->isRedirection() + || ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html')) + || 'html' !== $event->getRequest()->getRequestFormat() + ) { + return; + } + + $response->setContent($response->getContent().'GA CODE'); + }); + + $controllerResolver = new ControllerResolver(); + $argumentResolver = new ArgumentResolver(); + + $framework = new Simplex\Framework($dispatcher, $matcher, $controllerResolver, $argumentResolver); + $response = $framework->handle($request); + + $response->send(); + +.. note:: + + The listener is just a proof of concept and you should add the Google + Analytics code just before the body tag. + +As you can see, ``addListener()`` associates a valid PHP callback to a named +event (``response``); the event name must be the same as the one used in the +``dispatch()`` call. + +In the listener, we add the Google Analytics code only if the response is not +a redirection, if the requested format is HTML and if the response content +type is HTML (these conditions demonstrate the ease of manipulating the +Request and Response data from your code). + +So far so good, but let's add another listener on the same event. Let's say +that we want to set the ``Content-Length`` of the Response if it is not already +set:: + + $dispatcher->addListener('response', function (Simplex\ResponseEvent $event): void { + $response = $event->getResponse(); + $headers = $response->headers; + + if (!$headers->has('Content-Length') && !$headers->has('Transfer-Encoding')) { + $headers->set('Content-Length', strlen($response->getContent())); + } + }); + +Depending on whether you have added this piece of code before the previous +listener registration or after it, you will have the wrong or the right value +for the ``Content-Length`` header. Sometimes, the order of the listeners +matter but by default, all listeners are registered with the same priority, +``0``. To tell the dispatcher to run a listener early, change the priority to +a positive number; negative numbers can be used for low priority listeners. +Here, we want the ``Content-Length`` listener to be executed last, so change +the priority to ``-255``:: + + $dispatcher->addListener('response', function (Simplex\ResponseEvent $event): void { + $response = $event->getResponse(); + $headers = $response->headers; + + if (!$headers->has('Content-Length') && !$headers->has('Transfer-Encoding')) { + $headers->set('Content-Length', strlen($response->getContent())); + } + }, -255); + +.. tip:: + + When creating your framework, think about priorities (reserve some numbers + for internal listeners for instance) and document them thoroughly. + +Let's refactor the code a bit by moving the Google listener to its own class:: + + // example.com/src/Simplex/GoogleListener.php + namespace Simplex; + + class GoogleListener + { + public function onResponse(ResponseEvent $event): void + { + $response = $event->getResponse(); + + if ($response->isRedirection() + || ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html')) + || 'html' !== $event->getRequest()->getRequestFormat() + ) { + return; + } + + $response->setContent($response->getContent().'GA CODE'); + } + } + +And do the same with the other listener:: + + // example.com/src/Simplex/ContentLengthListener.php + namespace Simplex; + + class ContentLengthListener + { + public function onResponse(ResponseEvent $event): void + { + $response = $event->getResponse(); + $headers = $response->headers; + + if (!$headers->has('Content-Length') && !$headers->has('Transfer-Encoding')) { + $headers->set('Content-Length', strlen($response->getContent())); + } + } + } + +Our front controller should now look like the following:: + + $dispatcher = new EventDispatcher(); + $dispatcher->addListener('response', [new Simplex\ContentLengthListener(), 'onResponse'], -255); + $dispatcher->addListener('response', [new Simplex\GoogleListener(), 'onResponse']); + +Even if the code is now nicely wrapped in classes, there is still a slight +issue: the knowledge of the priorities is "hardcoded" in the front controller, +instead of being in the listeners themselves. For each application, you have +to remember to set the appropriate priorities. Moreover, the listener method +names are also exposed here, which means that refactoring our listeners would +mean changing all the applications that rely on those listeners. The solution +to this dilemma is to use subscribers instead of listeners:: + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber(new Simplex\ContentLengthListener()); + $dispatcher->addSubscriber(new Simplex\GoogleListener()); + +A subscriber knows about all the events it is interested in and pass this +information to the dispatcher via the ``getSubscribedEvents()`` method. Have a +look at the new version of the ``GoogleListener``:: + + // example.com/src/Simplex/GoogleListener.php + namespace Simplex; + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + + class GoogleListener implements EventSubscriberInterface + { + // ... + + public static function getSubscribedEvents(): array + { + return ['response' => 'onResponse']; + } + } + +And here is the new version of ``ContentLengthListener``:: + + // example.com/src/Simplex/ContentLengthListener.php + namespace Simplex; + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + + class ContentLengthListener implements EventSubscriberInterface + { + // ... + + public static function getSubscribedEvents(): array + { + return ['response' => ['onResponse', -255]]; + } + } + +.. tip:: + + A single subscriber can host as many listeners as you want on as many + events as needed. + +To make your framework truly flexible, don't hesitate to add more events; and +to make it more awesome out of the box, add more listeners. Again, this book +is not about creating a generic framework, but one that is tailored to your +needs. Stop whenever you see fit, and further evolve the code from there. + +.. _`WSGI`: https://www.python.org/dev/peps/pep-0333/#middleware-components-that-play-both-sides +.. _`Rack`: https://github.com/rack/rack diff --git a/create_framework/front_controller.rst b/create_framework/front_controller.rst new file mode 100644 index 00000000000..fded71a7b1c --- /dev/null +++ b/create_framework/front_controller.rst @@ -0,0 +1,233 @@ +The Front Controller +==================== + +Up until now, our application is simplistic as there is only one page. To +spice things up a little bit, let's go crazy and add another page that says +goodbye:: + + // framework/bye.php + require_once __DIR__.'/vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + $request = Request::createFromGlobals(); + + $response = new Response('Goodbye!'); + $response->send(); + +As you can see for yourself, much of the code is exactly the same as the one +we have written for the first page. Let's extract the common code that we can +share between all our pages. Code sharing sounds like a good plan to create +our first "real" framework! + +The PHP way of doing the refactoring would probably be the creation of an +include file:: + + // framework/init.php + require_once __DIR__.'/vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + $request = Request::createFromGlobals(); + $response = new Response(); + +Let's see it in action:: + + // framework/index.php + require_once __DIR__.'/init.php'; + + $name = $request->query->get('name', 'World'); + + $response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'))); + $response->send(); + +And for the "Goodbye" page:: + + // framework/bye.php + require_once __DIR__.'/init.php'; + + $response->setContent('Goodbye!'); + $response->send(); + +We have indeed moved most of the shared code into a central place, but it does +not feel like a good abstraction, does it? We still have the ``send()`` method +for all pages, our pages do not look like templates and we are still not able +to test this code properly. + +Moreover, adding a new page means that we need to create a new PHP script, the name of +which is exposed to the end user via the URL +(``http://127.0.0.1:4321/bye.php``). There is a direct mapping between the PHP +script name and the client URL. This is because the dispatching of the request +is done by the web server directly. It might be a good idea to move this +dispatching to our code for better flexibility. This can be achieved by routing +all client requests to a single PHP script. + +.. tip:: + + Exposing a single PHP script to the end user is a design pattern called + the ":ref:`front controller `". + +Such a script might look like the following:: + + // framework/front.php + require_once __DIR__.'/vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + $request = Request::createFromGlobals(); + $response = new Response(); + + $map = [ + '/hello' => __DIR__.'/hello.php', + '/bye' => __DIR__.'/bye.php', + ]; + + $path = $request->getPathInfo(); + if (isset($map[$path])) { + require $map[$path]; + } else { + $response->setStatusCode(404); + $response->setContent('Not Found'); + } + + $response->send(); + +And here is for instance the new ``hello.php`` script:: + + // framework/hello.php + $name = $request->query->get('name', 'World'); + $response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'))); + +In the ``front.php`` script, ``$map`` associates URL paths with their +corresponding PHP script paths. + +As a bonus, if the client asks for a path that is not defined in the URL map, +we return a custom 404 page. You are now in control of your website. + +To access a page, you must now use the ``front.php`` script: + +* ``http://127.0.0.1:4321/front.php/hello?name=Fabien`` + +* ``http://127.0.0.1:4321/front.php/bye`` + +``/hello`` and ``/bye`` are the page *paths*. + +.. tip:: + + Most web servers like Apache or nginx are able to rewrite the incoming URLs + and remove the front controller script so that your users will be able to + type ``http://127.0.0.1:4321/hello?name=Fabien``, which looks much better. + +The trick is the usage of the ``Request::getPathInfo()`` method which returns +the path of the Request by removing the front controller script name including +its sub-directories (only if needed -- see above tip). + +.. tip:: + + You don't even need to set up a web server to test the code. Instead, + replace the ``$request = Request::createFromGlobals();`` call to something + like ``$request = Request::create('/hello?name=Fabien');`` where the + argument is the URL path you want to simulate. + +Now that the web server always accesses the same script (``front.php``) for all +pages, we can secure the code further by moving all other PHP files outside of the +web root directory: + +.. code-block:: text + + example.com + ├── composer.json + ├── composer.lock + ├── src + │ └── pages + │ ├── hello.php + │ └── bye.php + ├── vendor + │ └── autoload.php + └── web + └── front.php + +Now, configure your web server root directory to point to ``web/`` and all +other files will no longer be accessible from the client. + +To test your changes in a browser (``http://localhost:4321/hello?name=Fabien``), +run the :doc:`Symfony Local Web Server `: + +.. code-block:: terminal + + $ symfony server:start --port=4321 --passthru=front.php + +.. note:: + + For this new structure to work, you will have to adjust some paths in + various PHP files; the changes are left as an exercise for the reader. + +The last thing that is repeated in each page is the call to ``setContent()``. +We can convert all pages to "templates" by echoing the content and calling +the ``setContent()`` directly from the front controller script:: + + // example.com/web/front.php + + // ... + + $path = $request->getPathInfo(); + if (isset($map[$path])) { + ob_start(); + include $map[$path]; + $response->setContent(ob_get_clean()); + } else { + $response->setStatusCode(404); + $response->setContent('Not Found'); + } + + // ... + +And the ``hello.php`` script can now be converted to a template: + +.. code-block:: html+php + + + query->get('name', 'World') ?> + + Hello + +We have the first version of our framework:: + + // example.com/web/front.php + require_once __DIR__.'/../vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + $request = Request::createFromGlobals(); + $response = new Response(); + + $map = [ + '/hello' => __DIR__.'/../src/pages/hello.php', + '/bye' => __DIR__.'/../src/pages/bye.php', + ]; + + $path = $request->getPathInfo(); + if (isset($map[$path])) { + ob_start(); + include $map[$path]; + $response->setContent(ob_get_clean()); + } else { + $response->setStatusCode(404); + $response->setContent('Not Found'); + } + + $response->send(); + +Adding a new page is a two-step process: add an entry in the map and create a +PHP template in ``src/pages/``. From a template, get the Request data via the +``$request`` variable and tweak the Response headers via the ``$response`` +variable. + +.. note:: + + If you decide to stop here, you can probably enhance your framework by + extracting the URL map to a configuration file. diff --git a/create_framework/http_foundation.rst b/create_framework/http_foundation.rst new file mode 100644 index 00000000000..219119164b4 --- /dev/null +++ b/create_framework/http_foundation.rst @@ -0,0 +1,301 @@ +The HttpFoundation Component +============================ + +Before diving into the framework creation process, let's first step back and +let's take a look at why you would like to use a framework instead of keeping +your plain-old PHP applications as is. Why using a framework is actually a good +idea, even for the simplest snippet of code and why creating your framework on +top of the Symfony components is better than creating a framework from scratch. + +.. note:: + + We won't talk about the traditional benefits of using a framework when + working on big applications with more than a few developers; the Internet + already has plenty of good resources on that topic. + +Even if the "application" we wrote in the previous chapter was simple enough, +it suffers from a few problems:: + + // framework/index.php + $name = $_GET['name']; + + printf('Hello %s', $name); + +First, if the ``name`` query parameter is not defined in the URL query string, +you will get a PHP warning; so let's fix it:: + + // framework/index.php + $name = $_GET['name'] ?? 'World'; + + printf('Hello %s', $name); + +Then, this *application is not secure*. Can you believe it? Even this simple +snippet of PHP code is vulnerable to one of the most widespread Internet +security issue, XSS (Cross-Site Scripting). Here is a more secure version:: + + $name = $_GET['name'] ?? 'World'; + + header('Content-Type: text/html; charset=utf-8'); + + printf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8')); + +.. note:: + + As you might have noticed, securing your code with ``htmlspecialchars`` is + tedious and error prone. That's one of the reasons why using a template + engine like `Twig`_, where auto-escaping is enabled by default, might be a + good idea (and explicit escaping is also less painful with the usage of a + simple ``e`` filter). + +As you can see for yourself, the simple code we had written first is not that +simple anymore if we want to avoid PHP warnings/notices and make the code +more secure. + +Beyond security, this code can be complex to test. Even if there is not +much to test, it strikes me that writing unit tests for the simplest possible +snippet of PHP code is not natural and feels ugly. Here is a tentative PHPUnit +unit test for the above code:: + + // framework/test.php + use PHPUnit\Framework\TestCase; + + class IndexTest extends TestCase + { + public function testHello(): void + { + $_GET['name'] = 'Fabien'; + + ob_start(); + include 'index.php'; + $content = ob_get_clean(); + + $this->assertEquals('Hello Fabien', $content); + } + } + +.. note:: + + If our application were just slightly bigger, we would have been able to + find even more problems. If you are curious about them, read the + :doc:`/introduction/from_flat_php_to_symfony` chapter of the book. + +At this point, if you are not convinced that security and testing are indeed +two very good reasons to stop writing code the old way and adopt a framework +instead (whatever adopting a framework means in this context), you can stop +reading this book now and go back to whatever code you were working on before. + +.. note:: + + Using a framework should give you more than just security and testability, + but the more important thing to keep in mind is that the framework you + choose must allow you to write better code faster. + +Going OOP with the HttpFoundation Component +------------------------------------------- + +Writing web code is about interacting with HTTP. So, the fundamental +principles of our framework should be around the `HTTP specification`_. + +The HTTP specification describes how a client (a browser for instance) +interacts with a server (our application via a web server). The dialog between +the client and the server is specified by well-defined *messages*, requests +and responses: *the client sends a request to the server and based on this +request, the server returns a response*. + +In PHP, the request is represented by global variables (``$_GET``, ``$_POST``, +``$_FILE``, ``$_COOKIE``, ``$_SESSION``...) and the response is generated by +functions (``echo``, ``header``, ``setcookie``, ...). + +The first step towards better code is probably to use an Object-Oriented +approach; that's the main goal of the Symfony HttpFoundation component: +replacing the default PHP global variables and functions by an Object-Oriented +layer. + +To use this component, add it as a dependency of the project: + +.. code-block:: terminal + + $ composer require symfony/http-foundation + +Running this command will also automatically download the Symfony +HttpFoundation component and install it under the ``vendor/`` directory. +A ``composer.json`` and a ``composer.lock`` file will be generated as well, +containing the new requirement. + +.. sidebar:: Class Autoloading + + When installing a new dependency, Composer also generates a + ``vendor/autoload.php`` file that allows any class to be `autoloaded`_. + Without autoloading, you would need to require the file where a class + is defined before being able to use it. But thanks to `PSR-4`_, + we can just let Composer and PHP do the hard work for us. + +Now, let's rewrite our application by using the ``Request`` and the +``Response`` classes:: + + // framework/index.php + require_once __DIR__.'/vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + $request = Request::createFromGlobals(); + + $name = $request->query->get('name', 'World'); + + $response = new Response(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'))); + + $response->send(); + +The ``createFromGlobals()`` method creates a ``Request`` object based on the +current PHP global variables. + +The ``send()`` method sends the ``Response`` object back to the client (it +first outputs the HTTP headers followed by the content). + +.. tip:: + + Before the ``send()`` call, we should have added a call to the + ``prepare()`` method (``$response->prepare($request);``) to ensure that + our Response were compliant with the HTTP specification. For instance, if + we were to call the page with the ``HEAD`` method, it would remove the + content of the Response. + +The main difference with the previous code is that you have total control of +the HTTP messages. You can create whatever request you want and you are in +charge of sending the response whenever you see fit. + +.. note:: + + We haven't explicitly set the ``Content-Type`` header in the rewritten + code as the charset of the Response object defaults to ``UTF-8``. + +With the ``Request`` class, you have all the request information at your +fingertips thanks to a nice and simple API:: + + // the URI being requested (e.g. /about) minus any query parameters + $request->getPathInfo(); + + // retrieves GET and POST variables respectively + $request->query->get('foo'); + $request->getPayload()->get('bar', 'default value if bar does not exist'); + + // retrieves SERVER variables + $request->server->get('HTTP_HOST'); + + // retrieves an instance of UploadedFile identified by foo + $request->files->get('foo'); + + // retrieves a COOKIE value + $request->cookies->get('PHPSESSID'); + + // retrieves a HTTP request header, with normalized, lowercase keys + $request->headers->get('host'); + $request->headers->get('content-type'); + + $request->getMethod(); // GET, POST, PUT, DELETE, HEAD + $request->getLanguages(); // an array of languages the client accepts + +You can also simulate a request:: + + $request = Request::create('/index.php?name=Fabien'); + +With the ``Response`` class, you can tweak the response:: + + $response = new Response(); + + $response->setContent('Hello world!'); + $response->setStatusCode(200); + $response->headers->set('Content-Type', 'text/html'); + + // configure the HTTP cache headers + $response->setMaxAge(10); + +.. tip:: + + To debug a response, cast it to a string; it will return the HTTP + representation of the response (headers and content). + +Last but not least, these classes, like every other class in the Symfony +code, have been `audited`_ for security issues by an independent company. And +being an Open-Source project also means that many other developers around the +world have read the code and have already fixed potential security problems. +When was the last time you ordered a professional security audit for your home-made +framework? + +Even something as simple as getting the client IP address can be insecure:: + + if ($myIp === $_SERVER['REMOTE_ADDR']) { + // the client is a known one, so give it some more privilege + } + +It works perfectly fine until you add a reverse proxy in front of the +production servers; at this point, you will have to change your code to make +it work on both your development machine (where you don't have a proxy) and +your servers:: + + if ($myIp === $_SERVER['HTTP_X_FORWARDED_FOR'] || $myIp === $_SERVER['REMOTE_ADDR']) { + // the client is a known one, so give it some more privilege + } + +Using the ``Request::getClientIp()`` method would have given you the right +behavior from day one (and it would have covered the case where you have +chained proxies):: + + $request = Request::createFromGlobals(); + + if ($myIp === $request->getClientIp()) { + // the client is a known one, so give it some more privilege + } + +And there is an added benefit: it is *secure* by default. What does it mean? +The ``$_SERVER['HTTP_X_FORWARDED_FOR']`` value cannot be trusted as it can be +manipulated by the end user when there is no proxy. So, if you are using this +code in production without a proxy, it becomes trivially easy to abuse your +system. That's not the case with the ``getClientIp()`` method as you must +explicitly trust your reverse proxies by calling ``setTrustedProxies()``:: + + Request::setTrustedProxies(['10.0.0.1'], Request::HEADER_X_FORWARDED_FOR); + + if ($myIp === $request->getClientIp()) { + // the client is a known one, so give it some more privilege + } + +So, the ``getClientIp()`` method works securely in all circumstances. You can +use it in all your projects, whatever the configuration is, it will behave +correctly and safely. That's one of the goals of using a framework. If you were +to write a framework from scratch, you would have to think about all these +cases by yourself. Why not use a technology that already works? + +.. note:: + + If you want to learn more about the HttpFoundation component, you can have + a look at the ``Symfony\Component\HttpFoundation`` API or read + its dedicated :doc:`documentation `. + +Believe it or not but we have our first framework. You can stop now if you want. +Using just the Symfony HttpFoundation component already allows you to write +better and more testable code. It also allows you to write code faster as many +day-to-day problems have already been solved for you. + +As a matter of fact, projects like Drupal have adopted the HttpFoundation +component; if it works for them, it will probably work for you. Don't reinvent +the wheel. + +I've almost forgotten 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`_, `Laravel`_ +and `ezPublish 5`_, and `more`_). + +.. _`Twig`: https://twig.symfony.com/ +.. _`HTTP specification`: https://tools.ietf.org/wg/httpbis/ +.. _`audited`: https://symfony.com/blog/symfony2-security-audit +.. _`applications using it`: https://symfony.com/components/HttpFoundation +.. _`Symfony`: https://symfony.com/ +.. _`Drupal 8`: https://www.drupal.org/ +.. _`phpBB 3`: https://www.phpbb.com/ +.. _`ezPublish 5`: https://ez.no/ +.. _`Laravel`: https://laravel.com/ +.. _`autoloaded`: https://www.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 new file mode 100644 index 00000000000..1c2857c9ed9 --- /dev/null +++ b/create_framework/http_kernel_controller_resolver.rst @@ -0,0 +1,205 @@ +The HttpKernel Component: the Controller Resolver +================================================= + +You might think that our framework is already pretty solid and you are +probably right. But let's see how we can improve it nonetheless. + +Right now, all our examples use procedural code, but remember that controllers +can be any valid PHP callbacks. Let's convert our controller to a proper +class:: + + class LeapYearController + { + public function index($request): Response + { + if (is_leap_year($request->attributes->get('year'))) { + return new Response('Yep, this is a leap year!'); + } + + return new Response('Nope, this is not a leap year.'); + } + } + +Update the route definition accordingly:: + + $routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', [ + 'year' => null, + '_controller' => [new LeapYearController(), 'index'], + ])); + +The move is pretty straightforward and makes a lot of sense as soon as you +create more pages but you might have noticed a non-desirable side effect... +The ``LeapYearController`` class is *always* instantiated, even if the +requested URL does not match the ``leap_year`` route. This is bad for one main +reason: performance-wise, all controllers for all routes must now be +instantiated for every request. It would be better if controllers were +lazy-loaded so that only the controller associated with the matched route is +instantiated. + +To solve this issue, and a bunch more, let's install and use the HttpKernel +component: + +.. code-block:: terminal + + $ composer require symfony/http-kernel + +The HttpKernel component has many interesting features, but the ones we need +right now are the *controller resolver* and *argument resolver*. A controller resolver knows how to +determine the controller to execute and the argument resolver determines the arguments to pass to it, +based on a Request object. All controller resolvers implement the following interface:: + + namespace Symfony\Component\HttpKernel\Controller; + + // ... + interface ControllerResolverInterface + { + public function getController(Request $request); + } + +The ``getController()`` method relies on the same convention as the one we +have defined earlier: the ``_controller`` request attribute must contain the +controller associated with the Request. Besides the built-in PHP callbacks, +``getController()`` also supports strings composed of a class name followed by +two colons and a method name as a valid callback, like 'class::method':: + + $routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', [ + 'year' => null, + '_controller' => 'LeapYearController::index', + ])); + +To make this code work, modify the framework code to use the controller +resolver from HttpKernel:: + + use Symfony\Component\HttpKernel; + + $controllerResolver = new HttpKernel\Controller\ControllerResolver(); + $argumentResolver = new HttpKernel\Controller\ArgumentResolver(); + + $controller = $controllerResolver->getController($request); + $arguments = $argumentResolver->getArguments($request, $controller); + + $response = call_user_func_array($controller, $arguments); + +.. note:: + + As an added bonus, the controller resolver properly handles the error + management for you: when you forget to define a ``_controller`` attribute + for a Route for instance. + +Now, let's see how the controller arguments are guessed. ``getArguments()`` +introspects the controller signature to determine which arguments to pass to +it by using the native PHP `reflection`_. This method is defined in the +following interface:: + + namespace Symfony\Component\HttpKernel\Controller; + + // ... + interface ArgumentResolverInterface + { + public function getArguments(Request $request, $controller); + } + +The ``index()`` method needs the Request object as an argument. +``getArguments()`` knows when to inject it properly if it is type-hinted +correctly:: + + public function index(Request $request) + + // won't work + public function index($request) + +More interesting, ``getArguments()`` is also able to inject any Request +attribute; if the argument has the same name as the corresponding +attribute:: + + public function index(int $year) + +You can also inject the Request and some attributes at the same time (as the +matching is done on the argument name or a type hint, the arguments order does +not matter):: + + public function index(Request $request, int $year) + + public function index(int $year, Request $request) + +Finally, you can also define default values for any argument that matches an +optional attribute of the Request:: + + public function index(int $year = 2012) + +Let's inject the ``$year`` request attribute for our controller:: + + class LeapYearController + { + public function index(int $year): Response + { + if (is_leap_year($year)) { + return new Response('Yep, this is a leap year!'); + } + + return new Response('Nope, this is not a leap year.'); + } + } + +The resolvers also take care of validating the controller callable and its +arguments. In case of a problem, it throws an exception with a nice message +explaining the problem (the controller class does not exist, the method is not +defined, an argument has no matching attribute, ...). + +.. note:: + + With the great flexibility of the default controller resolver and argument + resolver, you might wonder why someone would want to create another one + (why would there be an interface if not?). Two examples: in Symfony, + ``getController()`` is enhanced to support :doc:`controllers as services `; + and ``getArguments()`` provides an extension point to alter or enhance + the resolving of arguments. + +Let's conclude with the new version of our framework:: + + // example.com/web/front.php + require_once __DIR__.'/../vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel; + use Symfony\Component\Routing; + + function render_template(Request $request): Response + { + extract($request->attributes->all(), EXTR_SKIP); + ob_start(); + include sprintf(__DIR__.'/../src/pages/%s.php', $_route); + + return new Response(ob_get_clean()); + } + + $request = Request::createFromGlobals(); + $routes = include __DIR__.'/../src/app.php'; + + $context = new Routing\RequestContext(); + $context->fromRequest($request); + $matcher = new Routing\Matcher\UrlMatcher($routes, $context); + + $controllerResolver = new HttpKernel\Controller\ControllerResolver(); + $argumentResolver = new HttpKernel\Controller\ArgumentResolver(); + + try { + $request->attributes->add($matcher->match($request->getPathInfo())); + + $controller = $controllerResolver->getController($request); + $arguments = $argumentResolver->getArguments($request, $controller); + + $response = call_user_func_array($controller, $arguments); + } catch (Routing\Exception\ResourceNotFoundException $exception) { + $response = new Response('Not Found', 404); + } catch (Exception $exception) { + $response = new Response('An error occurred', 500); + } + + $response->send(); + +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://www.php.net/reflection diff --git a/create_framework/http_kernel_httpkernel_class.rst b/create_framework/http_kernel_httpkernel_class.rst new file mode 100644 index 00000000000..ecf9d4c7879 --- /dev/null +++ b/create_framework/http_kernel_httpkernel_class.rst @@ -0,0 +1,198 @@ +The HttpKernel Component: The HttpKernel Class +============================================== + +If you were to use our framework right now, you would probably have to add +support for custom error messages. We do have 404 and 500 error support but +the responses are hardcoded in the framework itself. Making them customizable +is straightforward though: dispatch a new event and listen to it. Doing it right +means that the listener has to call a regular controller. But what if the +error controller throws an exception? You will end up in an infinite loop. +There should be an easier way, right? + +Enter the ``HttpKernel`` class. Instead of solving the same problem over and +over again and instead of reinventing the wheel each time, the ``HttpKernel`` +class is a generic, extensible and flexible implementation of +``HttpKernelInterface``. + +This class is very similar to the framework class we have written so far: it +dispatches events at some strategic points during the handling of the request, +it uses a controller resolver to choose the controller to dispatch the request +to, and as an added bonus, it takes care of edge cases and provides great +feedback when a problem arises. + +Here is the new framework code:: + + // example.com/src/Simplex/Framework.php + namespace Simplex; + + use Symfony\Component\HttpKernel\HttpKernel; + + class Framework extends HttpKernel + { + } + +And the new front controller:: + + // example.com/web/front.php + require_once __DIR__.'/../vendor/autoload.php'; + + use Symfony\Component\EventDispatcher\EventDispatcher; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\RequestStack; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel; + use Symfony\Component\Routing; + + $request = Request::createFromGlobals(); + $requestStack = new RequestStack(); + $routes = include __DIR__.'/../src/app.php'; + + $context = new Routing\RequestContext(); + $matcher = new Routing\Matcher\UrlMatcher($routes, $context); + + $controllerResolver = new HttpKernel\Controller\ControllerResolver(); + $argumentResolver = new HttpKernel\Controller\ArgumentResolver(); + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber(new HttpKernel\EventListener\RouterListener($matcher, $requestStack)); + + $framework = new Simplex\Framework($dispatcher, $controllerResolver, $requestStack, $argumentResolver); + + $response = $framework->handle($request); + $response->send(); + +``RouterListener`` is an implementation of the same logic we had in our +framework: it matches the incoming request and populates the request +attributes with route parameters. + +Our code is now much more concise and surprisingly more robust and more +powerful than ever. For instance, use the built-in ``ErrorListener`` to +make your error management configurable:: + + $errorHandler = function (Symfony\Component\ErrorHandler\Exception\FlattenException $exception): Response { + $msg = 'Something went wrong! ('.$exception->getMessage().')'; + + return new Response($msg, $exception->getStatusCode()); + }; + $dispatcher->addSubscriber(new HttpKernel\EventListener\ErrorListener($errorHandler)); + +``ErrorListener`` gives you a ``FlattenException`` instance instead of the +thrown ``Exception`` or ``Error`` instance to ease exception manipulation and +display. It can take any valid controller as an exception handler, so you can +create an ErrorController class instead of using a Closure:: + + $listener = new HttpKernel\EventListener\ErrorListener( + 'Calendar\Controller\ErrorController::exception' + ); + $dispatcher->addSubscriber($listener); + +The error controller reads as follows:: + + // example.com/src/Calendar/Controller/ErrorController.php + namespace Calendar\Controller; + + use Symfony\Component\ErrorHandler\Exception\FlattenException; + use Symfony\Component\HttpFoundation\Response; + + class ErrorController + { + public function exception(FlattenException $exception): Response + { + $msg = 'Something went wrong! ('.$exception->getMessage().')'; + + return new Response($msg, $exception->getStatusCode()); + } + } + +*Voilà!* Clean and customizable error management without efforts. And if your +``ErrorController`` throws an exception, HttpKernel will handle it nicely. + +In chapter two, we talked about the ``Response::prepare()`` method, which +ensures that a Response is compliant with the HTTP specification. It is +probably a good idea to always call it just before sending the Response to the +client; that's what the ``ResponseListener`` does:: + + $dispatcher->addSubscriber(new HttpKernel\EventListener\ResponseListener('UTF-8')); + +And in your controller, return a ``StreamedResponse`` instance instead of a +``Response`` instance. + +.. tip:: + + Read the :doc:`/reference/events` reference to learn more about the events + dispatched by HttpKernel and how they allow you to change the flow of a + request. + +Now, let's create a listener, one that allows a controller to return a string +instead of a full Response object:: + + class LeapYearController + { + public function index(int $year): string + { + $leapYear = new LeapYear(); + if ($leapYear->isLeapYear($year)) { + return 'Yep, this is a leap year! '; + } + + return 'Nope, this is not a leap year.'; + } + } + +To implement this feature, we are going to listen to the ``kernel.view`` +event, which is triggered just after the controller has been called. Its goal +is to convert the controller return value to a proper Response instance, but +only if needed:: + + // example.com/src/Simplex/StringResponseListener.php + namespace Simplex; + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Event\ViewEvent; + + class StringResponseListener implements EventSubscriberInterface + { + public function onView(ViewEvent $event): void + { + $response = $event->getControllerResult(); + + if (is_string($response)) { + $event->setResponse(new Response($response)); + } + } + + public static function getSubscribedEvents(): array + { + return ['kernel.view' => 'onView']; + } + } + +The code is simple because the ``kernel.view`` event is only triggered when +the controller return value is not a Response and because setting the response +on the event stops the event propagation (our listener cannot interfere with +other view listeners). + +Don't forget to register it in the front controller:: + + $dispatcher->addSubscriber(new Simplex\StringResponseListener()); + +.. note:: + + If you forget to register the subscriber, HttpKernel will throw an + exception with a nice message: ``The controller must return a response + (Nope, this is not a leap year. given).``. + +At this point, our whole framework code is as compact as possible and it is +mainly composed of an assembly of existing libraries. Extending is a matter +of registering event listeners/subscribers. + +Hopefully, you now have a better understanding of why the simple looking +``HttpKernelInterface`` is so powerful. Its default implementation, +``HttpKernel``, gives you access to a lot of cool features, ready to be used +out of the box, with no efforts. And because HttpKernel is actually the code +that powers the Symfony framework, you have the best of both +worlds: a custom framework, tailored to your needs, but based on a rock-solid +and well maintained low-level architecture that has been proven to work for +many websites; a code that has been audited for security issues and that has +proven to scale well. diff --git a/create_framework/http_kernel_httpkernelinterface.rst b/create_framework/http_kernel_httpkernelinterface.rst new file mode 100644 index 00000000000..8d28fc9d24b --- /dev/null +++ b/create_framework/http_kernel_httpkernelinterface.rst @@ -0,0 +1,217 @@ +The HttpKernel Component: HttpKernelInterface +============================================= + +In the conclusion of the second chapter of this book, I've talked about one +great benefit of using the Symfony components: the *interoperability* between +all frameworks and applications using them. Let's do a big step towards this +goal by making our framework implement ``HttpKernelInterface``:: + + namespace Symfony\Component\HttpKernel; + + // ... + interface HttpKernelInterface + { + /** + * @return Response A Response instance + */ + public function handle( + Request $request, + int $type = self::MAIN_REQUEST, + bool $catch = true + ): Response; + } + +``HttpKernelInterface`` is probably the most important piece of code in the +HttpKernel component, no kidding. Frameworks and applications that implement +this interface are fully interoperable. Moreover, a lot of great features will +come with it for free. + +Update your framework so that it implements this interface:: + + // example.com/src/Framework.php + + // ... + use Symfony\Component\HttpKernel\HttpKernelInterface; + + class Framework implements HttpKernelInterface + { + // ... + + public function handle( + Request $request, + int $type = HttpKernelInterface::MAIN_REQUEST, + bool $catch = true + ) { + // ... + } + } + +With this change, a little goes a long way! Let's talk about one of +the most impressive upsides: transparent :doc:`HTTP caching ` support. + +The ``HttpCache`` class implements a fully-featured reverse proxy, written in +PHP; it implements ``HttpKernelInterface`` and wraps another +``HttpKernelInterface`` instance:: + + // example.com/web/front.php + + // ... + use Symfony\Component\HttpKernel; + + $framework = new Simplex\Framework($dispatcher, $matcher, $controllerResolver, $argumentResolver); + $framework = new HttpKernel\HttpCache\HttpCache( + $framework, + new HttpKernel\HttpCache\Store(__DIR__.'/../cache') + ); + + $response = $framework->handle($request); + $response->send(); + +That's all it takes to add HTTP caching support to our framework. Isn't it +amazing? + +Configuring the cache needs to be done via HTTP cache headers. For instance, +to cache a response for 10 seconds, use the ``Response::setTtl()`` method:: + + // example.com/src/Calendar/Controller/LeapYearController.php + + // ... + public function index(Request $request, int $year): Response + { + $leapYear = new LeapYear(); + if ($leapYear->isLeapYear($year)) { + $response = new Response('Yep, this is a leap year!'); + } else { + $response = new Response('Nope, this is not a leap year.'); + } + + $response->setTtl(10); + + return $response; + } + +.. tip:: + + If you are running your framework from the command line by simulating + requests (``Request::create('/is_leap_year/2012')``), you can debug Response + instances by dumping their string representation (``echo $response;``) as it + displays all headers as well as the response content. + +To validate that it works correctly, add a random number to the response +content and check that the number only changes every 10 seconds:: + + $response = new Response('Yep, this is a leap year! '.rand()); + +.. note:: + + When deploying to your production environment, keep using the Symfony + reverse proxy (great for shared hosting) or even better, switch to a more + efficient reverse proxy like `Varnish`_. + +Using HTTP cache headers to manage your application cache is very powerful and +allows you to tune finely your caching strategy as you can use both the +expiration and the validation models of the HTTP specification. If you are not +comfortable with these concepts, read the :doc:`HTTP caching ` chapter of the +Symfony documentation. + +The Response class contains methods that let you configure the HTTP cache. One +of the most powerful is ``setCache()`` as it abstracts the most frequently used +caching strategies into a single array:: + + $response->setCache([ + 'must_revalidate' => false, + 'no_cache' => false, + 'no_store' => false, + 'no_transform' => false, + 'public' => true, + 'private' => false, + 'proxy_revalidate' => false, + 'max_age' => 600, + 's_maxage' => 600, + 'immutable' => true, + 'last_modified' => new \DateTime(), + 'etag' => 'abcdef' + ]); + + // it is equivalent to the following code + $response->setPublic(); + $response->setMaxAge(600); + $response->setSharedMaxAge(600); + $response->setImmutable(); + $response->setLastModified(new \DateTime()); + $response->setEtag('abcde'); + +When using the validation model, the ``isNotModified()`` method allows you to +cut on the response time by short-circuiting the response generation as early as +possible:: + + $response->setETag('whatever_you_compute_as_an_etag'); + + if ($response->isNotModified($request)) { + return $response; + } + + $response->setContent('The computed content of the response'); + + return $response; + +Using HTTP caching is great, but what if you cannot cache the whole page? What +if you can cache everything but some sidebar that is more dynamic that the +rest of the content? Edge Side Includes (`ESI`_) to the rescue! Instead of +generating the whole content in one go, ESI allows you to mark a region of a +page as being the content of a sub-request call: + +.. code-block:: html + + This is the content of your page + + Is 2012 a leap year? + + Some other content + +For ESI tags to be supported by HttpCache, you need to pass it an instance of +the ``ESI`` class. The ``ESI`` class automatically parses ESI tags and makes +sub-requests to convert them to their proper content:: + + $framework = new HttpKernel\HttpCache\HttpCache( + $framework, + new HttpKernel\HttpCache\Store(__DIR__.'/../cache'), + new HttpKernel\HttpCache\Esi() + ); + +.. note:: + + For ESI to work, you need to use a reverse proxy that supports it like the + Symfony implementation. `Varnish`_ is the best alternative and it is + Open-Source. + +When using complex HTTP caching strategies and/or many ESI include tags, it +can be hard to understand why and when a resource should be cached or not. To +ease debugging, you can enable the debug mode:: + + $framework = new HttpKernel\HttpCache\HttpCache( + $framework, + new HttpKernel\HttpCache\Store(__DIR__.'/../cache'), + new HttpKernel\HttpCache\Esi(), + ['debug' => true] + ); + +The debug mode adds a ``X-Symfony-Cache`` header to each response that +describes what the cache layer did: + +.. code-block:: text + + X-Symfony-Cache: GET /is_leap_year/2012: stale, invalid, store + + X-Symfony-Cache: GET /is_leap_year/2012: fresh + +HttpCache has many features like support for the +``stale-while-revalidate`` and ``stale-if-error`` HTTP Cache-Control +extensions as defined in RFC 5861. + +With the addition of a single interface, our framework can now benefit from +the many features built into the HttpKernel component; HTTP caching being just +one of them but an important one as it can make your applications fly! + +.. _`ESI`: https://en.wikipedia.org/wiki/Edge_Side_Includes +.. _`Varnish`: https://varnish-cache.org/ diff --git a/create_framework/index.rst b/create_framework/index.rst new file mode 100644 index 00000000000..342a95960ec --- /dev/null +++ b/create_framework/index.rst @@ -0,0 +1,17 @@ +Create your own PHP Framework +============================= + +.. toctree:: + + introduction + http_foundation + front_controller + routing + templating + http_kernel_controller_resolver + separation_of_concerns + unit_testing + event_dispatcher + http_kernel_httpkernelinterface + http_kernel_httpkernel_class + dependency_injection diff --git a/create_framework/introduction.rst b/create_framework/introduction.rst new file mode 100644 index 00000000000..7a1e6b2ad50 --- /dev/null +++ b/create_framework/introduction.rst @@ -0,0 +1,117 @@ +Introduction +============ + +`Symfony`_ is a reusable set of standalone, decoupled and cohesive PHP +components that solve common web development problems. + +Instead of using these low-level components, you can use the ready-to-be-used +Symfony full-stack web framework, which is based on these components... or +you can create your very own framework. This tutorial is about the latter. + +Why would you Like to Create your Own Framework? +------------------------------------------------ + +Why would you like to create your own framework in the first place? If you +look around, everybody will tell you that it's a bad thing to reinvent the +wheel and that you'd better choose an existing framework and forget about +creating your own altogether. Most of the time, they are right but there are +a few good reasons to start creating your own framework: + +* To learn more about the low level architecture of modern web frameworks in + general and about the Symfony full-stack framework internals in particular; + +* To create a framework tailored to your very specific needs (just be sure + first that your needs are really specific); + +* To experiment creating a framework for fun (in a learn-and-throw-away + approach); + +* To refactor an old/existing application that needs a good dose of recent web + development best practices; + +* To prove to the world that you can actually create a framework on your own (... + but with little effort). + +This tutorial will gently guide you through the creation of a web framework, +one step at a time. At each step, you will have a fully-working framework that +you can use as is or as a start for your very own. It will start with a simple +framework and more features will be added with time. Eventually, you will have +a fully-featured full-stack web framework. + +And each step will be the occasion to learn more about some of the Symfony +Components. + +Many modern web frameworks advertise themselves as being MVC frameworks. This +tutorial won't talk about the MVC pattern, as the Symfony Components are able to +create any type of frameworks, not just the ones that follow the MVC +architecture. Anyway, if you have a look at the MVC semantics, this book is +about how to create the Controller part of a framework. For the Model and the +View, it really depends on your personal taste and you can use any existing +third-party libraries (Doctrine, Propel or plain-old PDO for the Model; PHP or +Twig for the View). + +When creating a framework, following the MVC pattern is not the right goal. The +main goal should be the **Separation of Concerns**; this is probably the only +design pattern that you should really care about. The fundamental principles of +the Symfony Components are focused on the HTTP specification. As such, the +framework that you are going to create should be more accurately labelled as a +HTTP framework or Request/Response framework. + +Before You Start +---------------- + +Reading about how to create a framework is not enough. You will have to follow +along and actually type all the examples included in this tutorial. For that, +you need a recent version of PHP (7.4 or later is good enough), a web server +(like Apache, nginx or PHP's built-in web server), a good knowledge of PHP and +an understanding of Object Oriented Programming. + +Ready to go? Read on! + +Bootstrapping +------------- + +Before you can even think of creating the first framework, you need to think +about some conventions: where you will store the code, how you will name the +classes, how you will reference external dependencies, etc. + +To store your new framework, create a directory somewhere on your machine: + +.. code-block:: terminal + + $ mkdir framework + $ cd framework + +Dependency Management +~~~~~~~~~~~~~~~~~~~~~ + +To install the Symfony Components that you need for your framework, you are going +to use `Composer`_, a project dependency manager for PHP. If you don't have it +yet, `download and install Composer`_ now. + +Our Project +----------- + +Instead of creating our framework from scratch, we are going to write the same +"application" over and over again, adding one abstraction at a time. Let's +start with the simplest web application we can think of in PHP:: + + // framework/index.php + $name = $_GET['name']; + + printf('Hello %s', $name); + +You can use the :doc:`Symfony Local Web Server ` to test +this great application in a browser +(``http://localhost:8000/index.php?name=Fabien``): + +.. code-block:: terminal + + $ symfony server:start + +In the :doc:`next chapter `, we are going to +introduce the HttpFoundation Component and see what it brings us. + +.. _`Symfony`: https://symfony.com/ +.. _`Composer`: https://getcomposer.org/ +.. _`download and install Composer`: https://getcomposer.org/download/ diff --git a/create_framework/map.rst.inc b/create_framework/map.rst.inc new file mode 100644 index 00000000000..0f3bc41cbab --- /dev/null +++ b/create_framework/map.rst.inc @@ -0,0 +1,12 @@ +* :doc:`/create_framework/introduction` +* :doc:`/create_framework/http_foundation` +* :doc:`/create_framework/front_controller` +* :doc:`/create_framework/routing` +* :doc:`/create_framework/templating` +* :doc:`/create_framework/http_kernel_controller_resolver` +* :doc:`/create_framework/separation_of_concerns` +* :doc:`/create_framework/unit_testing` +* :doc:`/create_framework/event_dispatcher` +* :doc:`/create_framework/http_kernel_httpkernelinterface` +* :doc:`/create_framework/http_kernel_httpkernel_class` +* :doc:`/create_framework/dependency_injection` diff --git a/create_framework/routing.rst b/create_framework/routing.rst new file mode 100644 index 00000000000..71e3a8250e1 --- /dev/null +++ b/create_framework/routing.rst @@ -0,0 +1,228 @@ +The Routing Component +===================== + +Before we start diving into the Routing component, let's refactor our current +framework just a little to make templates even more readable:: + + // example.com/web/front.php + require_once __DIR__.'/../vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + $request = Request::createFromGlobals(); + + $map = [ + '/hello' => 'hello', + '/bye' => 'bye', + ]; + + $path = $request->getPathInfo(); + if (isset($map[$path])) { + ob_start(); + extract($request->query->all(), EXTR_SKIP); + include sprintf(__DIR__.'/../src/pages/%s.php', $map[$path]); + $response = new Response(ob_get_clean()); + } else { + $response = new Response('Not Found', 404); + } + + $response->send(); + +As we now extract the request query parameters, simplify the ``hello.php`` +template as follows: + +.. code-block:: html+php + + + Hello + +Now, we are in good shape to add new features. + +One very important aspect of any website is the form of its URLs. Thanks to +the URL map, we have decoupled the URL from the code that generates the +associated response, but it is not yet flexible enough. For instance, we might +want to support dynamic paths to allow embedding data directly into the URL +(e.g. ``/hello/Fabien``) instead of relying on a query string (e.g. ``/hello?name=Fabien``). + +To support this feature, add the Symfony Routing component as a dependency: + +.. code-block:: terminal + + $ composer require symfony/routing + +Instead of an array for the URL map, the Routing component relies on a +``RouteCollection`` instance:: + + use Symfony\Component\Routing\RouteCollection; + + $routes = new RouteCollection(); + +Let's add a route that describes the ``/hello/SOMETHING`` URL and add another +one for the simple ``/bye`` one:: + + use Symfony\Component\Routing\Route; + + $routes->add('hello', new Route('/hello/{name}', ['name' => 'World'])); + $routes->add('bye', new Route('/bye')); + +Each entry in the collection is defined by a name (``hello``) and a ``Route`` +instance, which is defined by a route pattern (``/hello/{name}``) and an array +of default values for route attributes (``['name' => 'World']``). + +.. note:: + + Read the :doc:`Routing documentation ` to learn more about + its many features like URL generation, attribute requirements, HTTP + method enforcement, loaders for YAML or XML files, dumpers to PHP or + Apache rewrite rules for enhanced performance and much more. + +Based on the information stored in the ``RouteCollection`` instance, a +``UrlMatcher`` instance can match URL paths:: + + use Symfony\Component\Routing\Matcher\UrlMatcher; + use Symfony\Component\Routing\RequestContext; + + $context = new RequestContext(); + $context->fromRequest($request); + $matcher = new UrlMatcher($routes, $context); + + $attributes = $matcher->match($request->getPathInfo()); + +The ``match()`` method takes a request path and returns an array of attributes +(notice that the matched route is automatically stored under the special +``_route`` attribute):: + + $matcher->match('/bye'); + /* Result: + [ + '_route' => 'bye', + ]; + */ + + $matcher->match('/hello/Fabien'); + /* Result: + [ + 'name' => 'Fabien', + '_route' => 'hello', + ]; + */ + + $matcher->match('/hello'); + /* Result: + [ + 'name' => 'World', + '_route' => 'hello', + ]; + */ + +.. note:: + + Even if we don't strictly need the request context in our examples, it is + used in real-world applications to enforce method requirements and more. + +The URL matcher throws an exception when none of the routes match:: + + $matcher->match('/not-found'); + + // throws a Symfony\Component\Routing\Exception\ResourceNotFoundException + +With this knowledge in mind, let's write the new version of our framework:: + + // example.com/web/front.php + require_once __DIR__.'/../vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing; + + $request = Request::createFromGlobals(); + $routes = include __DIR__.'/../src/app.php'; + + $context = new Routing\RequestContext(); + $context->fromRequest($request); + $matcher = new Routing\Matcher\UrlMatcher($routes, $context); + + try { + extract($matcher->match($request->getPathInfo()), EXTR_SKIP); + ob_start(); + include sprintf(__DIR__.'/../src/pages/%s.php', $_route); + + $response = new Response(ob_get_clean()); + } catch (Routing\Exception\ResourceNotFoundException $exception) { + $response = new Response('Not Found', 404); + } catch (Exception $exception) { + $response = new Response('An error occurred', 500); + } + + $response->send(); + +There are a few new things in the code: + +* Route names are used for template names; + +* ``500`` errors are now managed correctly; + +* Request attributes are extracted to keep our templates simple: + +.. code-block:: html+php + + // example.com/src/pages/hello.php + Hello + +* Route configuration has been moved to its own file:: + + // example.com/src/app.php + use Symfony\Component\Routing; + + $routes = new Routing\RouteCollection(); + $routes->add('hello', new Routing\Route('/hello/{name}', ['name' => 'World'])); + $routes->add('bye', new Routing\Route('/bye')); + + return $routes; + +We now have a clear separation between the configuration (everything +specific to our application in ``app.php``) and the framework (the generic +code that powers our application in ``front.php``). + +With less than 30 lines of code, we have a new framework, more powerful and +more flexible than the previous one. Enjoy! + +Using the Routing component has one big additional benefit: the ability to +generate URLs based on Route definitions. When using both URL matching and URL +generation in your code, changing the URL patterns should have no other +impact. You can use the generator this way:: + + use Symfony\Component\Routing; + + $generator = new Routing\Generator\UrlGenerator($routes, $context); + + echo $generator->generate('hello', ['name' => 'Fabien']); + // outputs /hello/Fabien + +The code should be self-explanatory; and thanks to the context, you can even +generate absolute URLs:: + + use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + + echo $generator->generate( + 'hello', + ['name' => 'Fabien'], + UrlGeneratorInterface::ABSOLUTE_URL + ); + // outputs something like http://example.com/somewhere/hello/Fabien + +.. tip:: + + Concerned about performance? Based on your route definitions, create a + highly optimized URL matcher class that can replace the default + ``UrlMatcher``:: + + use Symfony\Component\Routing\Matcher\CompiledUrlMatcher; + use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper; + + // $compiledRoutes is a plain PHP array that describes all routes in a performant data format + // you can (and should) cache it, typically by exporting it to a PHP file + $compiledRoutes = (new CompiledUrlMatcherDumper($routes))->getCompiledRoutes(); + + $matcher = new CompiledUrlMatcher($compiledRoutes, $context); diff --git a/create_framework/separation_of_concerns.rst b/create_framework/separation_of_concerns.rst new file mode 100644 index 00000000000..5238b3aac42 --- /dev/null +++ b/create_framework/separation_of_concerns.rst @@ -0,0 +1,176 @@ +The Separation of Concerns +========================== + +One down-side of our framework right now is that we need to copy and paste the +code in ``front.php`` each time we create a new website. 60 lines of code is +not that much, but it would be nice if we could wrap this code into a proper +class. It would bring us better *reusability* and easier testing to name just +a few benefits. + +If you have a closer look at the code, ``front.php`` has one input, the +Request and one output, the Response. Our framework class will follow this +simple principle: the logic is about creating the Response associated with a +Request. + +Let's create our very own namespace for our framework: ``Simplex``. Move the +request handling logic into its own ``Simplex\Framework`` class:: + + // example.com/src/Simplex/Framework.php + namespace Simplex; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Controller\ArgumentResolver; + use Symfony\Component\HttpKernel\Controller\ControllerResolver; + use Symfony\Component\Routing\Exception\ResourceNotFoundException; + use Symfony\Component\Routing\Matcher\UrlMatcher; + + class Framework + { + public function __construct( + private UrlMatcher $matcher, + private ControllerResolver $controllerResolver, + private ArgumentResolver $argumentResolver, + ) { + } + + public function handle(Request $request): Response + { + $this->matcher->getContext()->fromRequest($request); + + try { + $request->attributes->add($this->matcher->match($request->getPathInfo())); + + $controller = $this->controllerResolver->getController($request); + $arguments = $this->argumentResolver->getArguments($request, $controller); + + return call_user_func_array($controller, $arguments); + } catch (ResourceNotFoundException $exception) { + return new Response('Not Found', 404); + } catch (\Exception $exception) { + return new Response('An error occurred', 500); + } + } + } + +And update ``example.com/web/front.php`` accordingly:: + + // example.com/web/front.php + + // ... + $request = Request::createFromGlobals(); + $routes = include __DIR__.'/../src/app.php'; + + $context = new Routing\RequestContext(); + $matcher = new Routing\Matcher\UrlMatcher($routes, $context); + + $controllerResolver = new ControllerResolver(); + $argumentResolver = new ArgumentResolver(); + + $framework = new Simplex\Framework($matcher, $controllerResolver, $argumentResolver); + $response = $framework->handle($request); + + $response->send(); + +To wrap up the refactoring, let's move everything but routes definition from +``example.com/src/app.php`` into yet another namespace: ``Calendar``. + +For the classes defined under the ``Simplex`` and ``Calendar`` namespaces to +be autoloaded, update the ``composer.json`` file: + +.. code-block:: json + + { + "...": "...", + "autoload": { + "psr-4": { "": "src/" } + } + } + +.. note:: + + For the Composer autoloader to be updated, run ``composer dump-autoload``. + +Move the controller to ``Calendar\Controller\LeapYearController``:: + + // example.com/src/Calendar/Controller/LeapYearController.php + namespace Calendar\Controller; + + use Calendar\Model\LeapYear; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + class LeapYearController + { + public function index(Request $request, int $year): Response + { + $leapYear = new LeapYear(); + if ($leapYear->isLeapYear($year)) { + return new Response('Yep, this is a leap year!'); + } + + return new Response('Nope, this is not a leap year.'); + } + } + +And move the ``is_leap_year()`` function to its own class too:: + + // example.com/src/Calendar/Model/LeapYear.php + namespace Calendar\Model; + + class LeapYear + { + public function isLeapYear(?int $year = null): bool + { + if (null === $year) { + $year = date('Y'); + } + + return 0 == $year % 400 || (0 == $year % 4 && 0 != $year % 100); + } + } + +Don't forget to update the ``example.com/src/app.php`` file accordingly:: + + $routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', [ + 'year' => null, + '_controller' => 'Calendar\Controller\LeapYearController::index', + ])); + +To sum up, here is the new file layout: + +.. code-block:: text + + example.com + ├── composer.json + ├── composer.lock + ├── src + │ ├── app.php + │ └── Simplex + │ └── Framework.php + │ └── Calendar + │ └── Controller + │ │ └── LeapYearController.php + │ └── Model + │ └── LeapYear.php + ├── vendor + │ └── autoload.php + └── web + └── front.php + +That's it! Our application has now four different layers and each of them has +a well-defined goal: + +* ``web/front.php``: The front controller; the only exposed PHP code that + makes the interface with the client (it gets the Request and sends the + Response) and provides the boiler-plate code to initialize the framework and + our application; + +* ``src/Simplex``: The reusable framework code that abstracts the handling of + incoming Requests (by the way, it makes your controllers/templates better + testable -- more about that later on); + +* ``src/Calendar``: Our application specific code (the controllers and the + model); + +* ``src/app.php``: The application configuration/framework customization. diff --git a/create_framework/templating.rst b/create_framework/templating.rst new file mode 100644 index 00000000000..282e75cbc94 --- /dev/null +++ b/create_framework/templating.rst @@ -0,0 +1,183 @@ +Templating +========== + +The astute reader has noticed that our framework hardcodes the way specific +"code" (the templates) is run. For simple pages like the ones we have created +so far, that's not a problem, but if you want to add more logic, you would be +forced to put the logic into the template itself, which is probably not a good +idea, especially if you still have the separation of concerns principle in +mind. + +Let's separate the template code from the logic by adding a new layer: the +controller: *The controller's mission is to generate a Response based on the +information conveyed by the client's Request.* + +Change the template rendering part of the framework to read as follows:: + + // example.com/web/front.php + + // ... + try { + $request->attributes->add($matcher->match($request->getPathInfo())); + $response = call_user_func('render_template', $request); + } catch (Routing\Exception\ResourceNotFoundException $exception) { + $response = new Response('Not Found', 404); + } catch (Exception $exception) { + $response = new Response('An error occurred', 500); + } + +As the rendering is now done by an external function (``render_template()`` +here), we need to pass to it the attributes extracted from the URL. We could +have passed them as an additional argument to ``render_template()``, but +instead, let's use another feature of the ``Request`` class called +*attributes*: Request attributes is a way to attach additional information +about the Request that is not directly related to the HTTP Request data. + +You can now create the ``render_template()`` function, a generic controller +that renders a template when there is no specific logic. To keep the same +template as before, request attributes are extracted before the template is +rendered:: + + function render_template(Request $request): Response + { + extract($request->attributes->all(), EXTR_SKIP); + ob_start(); + include sprintf(__DIR__.'/../src/pages/%s.php', $_route); + + return new Response(ob_get_clean()); + } + +As ``render_template`` is used as an argument to the PHP ``call_user_func()`` +function, we can replace it with any valid PHP `callbacks`_. This allows us to +use a function, an anonymous function or a method of a class as a +controller... your choice. + +As a convention, for each route, the associated controller is configured via +the ``_controller`` route attribute:: + + $routes->add('hello', new Routing\Route('/hello/{name}', [ + 'name' => 'World', + '_controller' => 'render_template', + ])); + + try { + $request->attributes->add($matcher->match($request->getPathInfo())); + $response = call_user_func($request->attributes->get('_controller'), $request); + } catch (Routing\Exception\ResourceNotFoundException $exception) { + $response = new Response('Not Found', 404); + } catch (Exception $exception) { + $response = new Response('An error occurred', 500); + } + +A route can now be associated with any controller and within a controller, you +can still use the ``render_template()`` to render a template:: + + $routes->add('hello', new Routing\Route('/hello/{name}', [ + 'name' => 'World', + '_controller' => function (Request $request): string { + return render_template($request); + } + ])); + +This is rather flexible as you can change the Response object afterwards and +you can even pass additional arguments to the template:: + + $routes->add('hello', new Routing\Route('/hello/{name}', [ + 'name' => 'World', + '_controller' => function (Request $request): Response { + // $foo will be available in the template + $request->attributes->set('foo', 'bar'); + + $response = render_template($request); + + // change some header + $response->headers->set('Content-Type', 'text/plain'); + + return $response; + } + ])); + +Here is the updated and improved version of our framework:: + + // example.com/web/front.php + require_once __DIR__.'/../vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing; + + function render_template(Request $request): Response + { + extract($request->attributes->all(), EXTR_SKIP); + ob_start(); + include sprintf(__DIR__.'/../src/pages/%s.php', $_route); + + return new Response(ob_get_clean()); + } + + $request = Request::createFromGlobals(); + $routes = include __DIR__.'/../src/app.php'; + + $context = new Routing\RequestContext(); + $context->fromRequest($request); + $matcher = new Routing\Matcher\UrlMatcher($routes, $context); + + try { + $request->attributes->add($matcher->match($request->getPathInfo())); + $response = call_user_func($request->attributes->get('_controller'), $request); + } catch (Routing\Exception\ResourceNotFoundException $exception) { + $response = new Response('Not Found', 404); + } catch (Exception $exception) { + $response = new Response('An error occurred', 500); + } + + $response->send(); + +To celebrate the birth of our new framework, let's create a brand new +application that needs some simple logic. Our application has one page that +says whether a given year is a leap year or not. When calling +``/is_leap_year``, you get the answer for the current year, but you can +also specify a year like in ``/is_leap_year/2009``. Being generic, the +framework does not need to be modified in any way, create a new +``app.php`` file:: + + // example.com/src/app.php + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing; + + function is_leap_year(?int $year = null): bool + { + if (null === $year) { + $year = (int)date('Y'); + } + + return 0 === $year % 400 || (0 === $year % 4 && 0 !== $year % 100); + } + + $routes = new Routing\RouteCollection(); + $routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', [ + 'year' => null, + '_controller' => function (Request $request): Response { + if (is_leap_year($request->attributes->get('year'))) { + return new Response('Yep, this is a leap year!'); + } + + return new Response('Nope, this is not a leap year.'); + } + ])); + + return $routes; + +The ``is_leap_year()`` function returns ``true`` when the given year is a leap +year, ``false`` otherwise. If the year is ``null``, the current year is +tested. The controller does little: it gets the year from the request +attributes, pass it to the ``is_leap_year()`` function, and according to the +return value it creates a new Response object. + +As always, you can decide to stop here and use the framework as is; it's +probably all you need to create simple websites like those fancy one-page +`websites`_ and hopefully a few others. + +.. _`callbacks`: https://www.php.net/manual/en/language.types.callable.php +.. _`websites`: https://kottke.org/08/02/single-serving-sites diff --git a/create_framework/unit_testing.rst b/create_framework/unit_testing.rst new file mode 100644 index 00000000000..32c97a03846 --- /dev/null +++ b/create_framework/unit_testing.rst @@ -0,0 +1,217 @@ +Unit Testing +============ + +You might have noticed some subtle but nonetheless important bugs in the +framework we built in the previous chapter. When creating a framework, you +must be sure that it behaves as advertised. If not, all the applications based +on it will exhibit the same bugs. The good news is that whenever you fix a +bug, you are fixing a bunch of applications too. + +Today's mission is to write unit tests for the framework we have created by +using `PHPUnit`_. At first, install PHPUnit as a development dependency: + +.. code-block:: terminal + + $ composer require --dev phpunit/phpunit:^9.6 + +Then, create a PHPUnit configuration file in ``example.com/phpunit.xml.dist``: + +.. code-block:: xml + + + + + + ./src + + + + + + ./tests + + + + +This configuration defines sensible defaults for most PHPUnit settings; more +interesting, the autoloader is used to bootstrap the tests, and tests will be +stored under the ``example.com/tests/`` directory. + +Now, let's write a test for "not found" resources. To avoid the creation of +all dependencies when writing tests and to really just unit-test what we want, +we are going to use `test doubles`_. Test doubles are easier to create when we +rely on interfaces instead of concrete classes. Fortunately, Symfony provides +such interfaces for core objects like the URL matcher and the controller +resolver. Modify the framework to make use of them:: + + // example.com/src/Simplex/Framework.php + namespace Simplex; + + // ... + + use Calendar\Controller\LeapYearController; + use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; + use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; + use Symfony\Component\Routing\Matcher\UrlMatcherInterface; + + class Framework + { + public function __construct( + private UrlMatcherInterface $matcher, + private ControllerResolverInterface $resolver, + private ArgumentResolverInterface $argumentResolver, + ) { + } + + // ... + } + +We are now ready to write our first test:: + + // example.com/tests/Simplex/Tests/FrameworkTest.php + namespace Simplex\Tests; + + use PHPUnit\Framework\TestCase; + use Simplex\Framework; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; + use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; + use Symfony\Component\Routing; + use Symfony\Component\Routing\Exception\ResourceNotFoundException; + + class FrameworkTest extends TestCase + { + public function testNotFoundHandling(): void + { + $framework = $this->getFrameworkForException(new ResourceNotFoundException()); + + $response = $framework->handle(new Request()); + + $this->assertEquals(404, $response->getStatusCode()); + } + + private function getFrameworkForException($exception): Framework + { + $matcher = $this->createMock(Routing\Matcher\UrlMatcherInterface::class); + + $matcher + ->expects($this->once()) + ->method('match') + ->will($this->throwException($exception)) + ; + $matcher + ->expects($this->once()) + ->method('getContext') + ->will($this->returnValue($this->createMock(Routing\RequestContext::class))) + ; + $controllerResolver = $this->createMock(ControllerResolverInterface::class); + $argumentResolver = $this->createMock(ArgumentResolverInterface::class); + + return new Framework($matcher, $controllerResolver, $argumentResolver); + } + } + +This test simulates a request that does not match any route. As such, the +``match()`` method returns a ``ResourceNotFoundException`` exception and we +are testing that our framework converts this exception to a 404 response. + +Execute this test by running ``phpunit`` in the ``example.com`` directory: + +.. code-block:: terminal + + $ ./vendor/bin/phpunit + +.. note:: + + If you don't understand what the hell is going on in the code, read the + PHPUnit documentation on `test doubles`_. + +After the test ran, you should see a green bar. If not, you have a bug +either in the test or in the framework code! + +Adding a unit test for any exception thrown in a controller:: + + public function testErrorHandling(): void + { + $framework = $this->getFrameworkForException(new \RuntimeException()); + + $response = $framework->handle(new Request()); + + $this->assertEquals(500, $response->getStatusCode()); + } + +Last, but not the least, let's write a test for when we actually have a proper +Response:: + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Controller\ArgumentResolver; + use Symfony\Component\HttpKernel\Controller\ControllerResolver; + // ... + + public function testControllerResponse(): void + { + $matcher = $this->createMock(Routing\Matcher\UrlMatcherInterface::class); + + $matcher + ->expects($this->once()) + ->method('match') + ->will($this->returnValue([ + '_route' => 'is_leap_year/{year}', + 'year' => '2000', + '_controller' => [new LeapYearController(), 'index'], + ])) + ; + $matcher + ->expects($this->once()) + ->method('getContext') + ->will($this->returnValue($this->createMock(Routing\RequestContext::class))) + ; + $controllerResolver = new ControllerResolver(); + $argumentResolver = new ArgumentResolver(); + + $framework = new Framework($matcher, $controllerResolver, $argumentResolver); + + $response = $framework->handle(new Request()); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertStringContainsString('Yep, this is a leap year!', $response->getContent()); + } + +In this test, we simulate a route that matches and returns a simple +controller. We check that the response status is 200 and that its content is +the one we have set in the controller. + +To check that we have covered all possible use cases, run the PHPUnit test +coverage feature (you need to enable `XDebug`_ first): + +.. code-block:: terminal + + $ ./vendor/bin/phpunit --coverage-html=cov/ + +Open ``example.com/cov/src/Simplex/Framework.php.html`` in a browser and check +that all the lines for the Framework class are green (it means that they have +been visited when the tests were executed). + +Alternatively you can output the result directly to the console: + +.. code-block:: terminal + + $ ./vendor/bin/phpunit --coverage-text + +Thanks to the clean object-oriented code that we have written so far, we have +been able to write unit-tests to cover all possible use cases of our +framework; test doubles ensured that we were actually testing our code and not +Symfony code. + +Now that we are confident (again) about the code we have written, we can +safely think about the next batch of features we want to add to our framework. + +.. _`PHPUnit`: https://docs.phpunit.de/en/9.6/ +.. _`test doubles`: https://docs.phpunit.de/en/9.6/test-doubles.html +.. _`XDebug`: https://xdebug.org/ diff --git a/deployment.rst b/deployment.rst new file mode 100644 index 00000000000..07187f53cba --- /dev/null +++ b/deployment.rst @@ -0,0 +1,273 @@ +.. _how-to-deploy-a-symfony2-application: + +How to Deploy a Symfony Application +=================================== + +Deploying a Symfony application can be a complex and varied task depending on +the setup and the requirements of your application. This article is not a +step-by-step guide, but is a general list of the most common requirements and +ideas for deployment. + +.. _symfony2-deployment-basics: + +Symfony Deployment Basics +------------------------- + +The typical steps taken while deploying a Symfony application include: + +#. Upload your code to the production server; +#. Install your vendor dependencies (typically done via Composer and may be done + before uploading); +#. Running database migrations or similar tasks to update any changed data structures; +#. Clearing (and optionally, warming up) your cache. + +A deployment may also include other tasks, such as: + +* Tagging a particular version of your code as a release in your source control + repository; +* Creating a temporary staging area to build your updated setup "offline"; +* Running any tests available to ensure code and/or server stability; +* Removal of any unnecessary files from the ``public/`` directory to keep your + production environment clean; +* Clearing of external cache systems (like `Memcached`_ or `Redis`_). + +How to Deploy a Symfony Application +----------------------------------- + +There are several ways you can deploy a Symfony application. Start with a few +basic deployment strategies and build up from there. + +Basic File Transfer +~~~~~~~~~~~~~~~~~~~ + +The most basic way of deploying an application is copying the files manually +via FTP/SCP (or similar method). This has its disadvantages as you lack control +over the system as the upgrade progresses. This method also requires you +to take some manual steps after transferring the files (see `Common Deployment Tasks`_). + +Using Source Control +~~~~~~~~~~~~~~~~~~~~ + +If you're using source control (e.g. Git or SVN), you can simplify by having +your live installation also be a copy of your repository. When you're ready to +upgrade, fetch the latest updates from your source control +system. When using Git, a common approach is to create a tag for each release +and check out the appropriate tag on deployment (see `Git Tagging`_). + +This makes updating your files *easier*, but you still need to worry about +manually taking other steps (see `Common Deployment Tasks`_). + +Using Platforms as a Service +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using a Platform as a Service (PaaS) can be a great way to deploy your Symfony +app quickly. There are many PaaS, but we recommend `Platform.sh`_ as it +provides a dedicated Symfony integration and helps fund the Symfony development. + +Using Build Scripts and other Tools +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There are also tools to help ease the pain of deployment. Some of them have been +specifically tailored to the requirements of Symfony. + +`Deployer`_ + This is another native PHP rewrite of Capistrano, with some ready recipes for + Symfony. + +`Ansistrano`_ + An Ansible role that allows you to configure a powerful deploy via YAML files. + +`Magallanes`_ + This Capistrano-like deployment tool is built in PHP, and may be easier + for PHP developers to extend for their needs. + +`Fabric`_ + This Python-based library provides a basic suite of operations for executing + local or remote shell commands and uploading/downloading files. + +`Capistrano`_ with `Symfony plugin`_ + `Capistrano`_ is a remote server automation and deployment tool written in Ruby. + `Symfony plugin`_ is a plugin to ease Symfony related tasks, inspired by `Capifony`_ + (which works only with Capistrano 2). + +.. _common-post-deployment-tasks: + +Common Deployment Tasks +----------------------- + +Before and after deploying your actual source code, there are a number of common +things you'll need to do: + +A) Check Requirements +~~~~~~~~~~~~~~~~~~~~~ + +There are some :ref:`technical requirements for running Symfony applications `. +In your development machine, the recommended way to check these requirements is +to use `Symfony CLI`_. However, in your production server you might prefer to +not install the Symfony CLI tool. In those cases, install this other package in +your application: + +.. code-block:: terminal + + $ composer require symfony/requirements-checker + +Then, make sure that the checker is included in your Composer scripts: + +.. code-block:: json + + { + "...": "...", + + "scripts": { + "auto-scripts": { + "vendor/bin/requirements-checker": "php-script", + "...": "..." + }, + + "...": "..." + } + } + +.. _b-configure-your-app-config-parameters-yml-file: + +B) Configure your Environment Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Most Symfony applications read their configuration from environment variables. +While developing locally, you'll usually store these in :ref:`.env files `. +On production, you have two options: + +1. Create "real" environment variables. How you set environment variables, depends + on your setup: they can be set at the command line, in your Nginx configuration, + or via other methods provided by your hosting service; + +2. Or, create a ``.env.prod.local`` file that contains values specific to your + production environment. + +There is no significant advantage to either option: use whichever is most natural +for your hosting environment. + +.. tip:: + + You might not want your application to process the ``.env.*`` files on + every request. You can generate an optimized ``.env.local.php`` which + overrides all other configuration files: + + .. code-block:: terminal + + $ composer dump-env prod + + The generated file will contain all the configuration stored in ``.env``. If you + want to rely only on environment variables, generate one without any values using: + + .. code-block:: terminal + + $ composer dump-env prod --empty + + If you don't have Composer installed on the production server, use instead + :ref:`the dotenv:dump Symfony command `. + +C) Install/Update your Vendors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Your vendors can be updated before transferring your source code (i.e. +update the ``vendor/`` directory, then transfer that with your source +code) or afterwards on the server. Either way, update your vendors +as you normally do: + +.. code-block:: terminal + + $ composer install --no-dev --optimize-autoloader + +.. tip:: + + The ``--optimize-autoloader`` flag improves Composer's autoloader performance + significantly by building a "class map". The ``--no-dev`` flag ensures that + development packages are not installed in the production environment. + +.. warning:: + + 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 :ref:`Symfony Flex `) before running this command so + that the ``post-install-cmd`` scripts run in the ``prod`` environment. + +D) Clear your Symfony Cache +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Make sure you clear and warm-up your Symfony cache: + +.. code-block:: terminal + + $ APP_ENV=prod APP_DEBUG=0 php bin/console cache:clear + +E) Other Things! +~~~~~~~~~~~~~~~~ + +There may be lots of other things that you need to do, depending on your +setup: + +* Running any database migrations +* Clearing your APCu cache +* Add/edit CRON jobs +* Restarting your workers +* :ref:`Building and minifying your assets ` with Webpack Encore +* :ref:`Compile your assets ` if you're using the AssetMapper component +* Pushing assets to a CDN +* On a shared hosting platform using the Apache web server, you may need to + install the `symfony/apache-pack`_ package +* etc. + +Application Lifecycle: Continuous Integration, QA, etc. +------------------------------------------------------- + +While this article covers the technical details of deploying, the full lifecycle +of taking code from development up to production may have more steps: +deploying to staging, QA (Quality Assurance), running tests, etc. + +The use of staging, testing, QA, continuous integration, database migrations +and the capability to roll back in case of failure are all strongly advised. There +are simple and more complex tools and one can make the deployment as easy +(or sophisticated) as your environment requires. + +Don't forget that deploying your application also involves updating any dependency +(typically via Composer), migrating your database, clearing your cache and +other potential things like pushing assets to a CDN (see `Common Deployment Tasks`_). + +Troubleshooting +--------------- + +Deployments not Using the ``composer.json`` File +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :ref:`project root directory ` +(whose value is used via the ``kernel.project_dir`` parameter and the +:method:`Symfony\\Component\\HttpKernel\\Kernel::getProjectDir` method) is +calculated automatically by Symfony as the directory where the main +``composer.json`` file is stored. + +In deployments not using the ``composer.json`` file, you'll need to override the +:method:`Symfony\\Component\\HttpKernel\\Kernel::getProjectDir` method +:ref:`as explained in this section `. + +Learn More +---------- + +.. toctree:: + :maxdepth: 1 + + deployment/proxies + +.. _`Capifony`: https://github.com/everzet/capifony +.. _`Capistrano`: https://capistranorb.com/ +.. _`Fabric`: https://www.fabfile.org/ +.. _`Ansistrano`: https://ansistrano.com/ +.. _`Magallanes`: https://github.com/andres-montanez/Magallanes +.. _`Memcached`: https://memcached.org/ +.. _`Redis`: https://redis.io/ +.. _`Symfony plugin`: https://github.com/capistrano/symfony/ +.. _`Deployer`: https://deployer.org/ +.. _`Git Tagging`: https://git-scm.com/book/en/v2/Git-Basics-Tagging +.. _`Platform.sh`: https://symfony.com/cloud +.. _`Symfony CLI`: https://symfony.com/download +.. _`symfony/apache-pack`: https://packagist.org/packages/symfony/apache-pack diff --git a/deployment/proxies.rst b/deployment/proxies.rst new file mode 100644 index 00000000000..4dad6f95fb1 --- /dev/null +++ b/deployment/proxies.rst @@ -0,0 +1,251 @@ +How to Configure Symfony to Work behind a Load Balancer or a Reverse Proxy +========================================================================== + +When you deploy your application, you may be behind a load balancer (e.g. +an AWS Elastic Load Balancing) or a reverse proxy (e.g. Varnish for +:doc:`caching `). + +For the most part, this doesn't cause any problems with Symfony. But, when +a request passes through a proxy, certain request information is sent using +either the standard ``Forwarded`` header or ``X-Forwarded-*`` headers. For example, +instead of reading the ``REMOTE_ADDR`` header (which will now be the IP address of +your reverse proxy), the user's true IP will be stored in a standard ``Forwarded: for="..."`` +header or a ``X-Forwarded-For`` header. + +If you don't configure Symfony to look for these headers, you'll get incorrect +information about the client's IP address, whether or not the client is connecting +via HTTPS, the client's port and the hostname being requested. + +.. _request-set-trusted-proxies: + +Solution: ``setTrustedProxies()`` +--------------------------------- + +To fix this, you need to tell Symfony which reverse proxy IP addresses to trust +and what headers your reverse proxy uses to send information. + +You can do that by setting the ``SYMFONY_TRUSTED_PROXIES`` and ``SYMFONY_TRUSTED_HEADERS`` +environment variables on your machine. Alternatively, you can configure them +using the following configuration options: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + # the IP address (or range) of your proxy + trusted_proxies: '192.0.0.1,10.0.0.0/8' + # shortcut for private IP address ranges of your proxy + trusted_proxies: 'private_ranges' + # trust *all* "X-Forwarded-*" headers + trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix'] + # or, if your proxy instead uses the "Forwarded" header + trusted_headers: ['forwarded'] + + .. code-block:: xml + + + + + + + + 192.0.0.1,10.0.0.0/8 + + private_ranges + + + x-forwarded-for + x-forwarded-host + x-forwarded-proto + x-forwarded-port + x-forwarded-prefix + + + forwarded + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework + // the IP address (or range) of your proxy + ->trustedProxies('192.0.0.1,10.0.0.0/8') + // shortcut for private IP address ranges of your proxy + ->trustedProxies('private_ranges') + // trust *all* "X-Forwarded-*" headers (the ! prefix means to not trust those headers) + ->trustedHeaders(['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix']) + // or, if your proxy instead uses the "Forwarded" header + ->trustedHeaders(['forwarded']) + ; + }; + +.. versionadded:: 7.1 + + ``private_ranges`` as a shortcut for private IP address ranges for the + ``trusted_proxies`` option was introduced in Symfony 7.1. + +.. versionadded:: 7.2 + + Support for the ``SYMFONY_TRUSTED_PROXIES`` and ``SYMFONY_TRUSTED_HEADERS`` + environment variables was introduced in Symfony 7.2. + +.. danger:: + + Enabling the ``Request::HEADER_X_FORWARDED_HOST`` option exposes the + application to `HTTP Host header attacks`_. Make sure the proxy really + sends an ``x-forwarded-host`` header. + +The Request object has several ``Request::HEADER_*`` constants that control exactly +*which* headers from your reverse proxy are trusted. The argument is a bit field, +so you can also pass your own value (e.g. ``0b00110``). + +.. tip:: + + You can set a ``TRUSTED_PROXIES`` env var to configure proxies on a per-environment basis: + + .. code-block:: bash + + # .env + TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8 + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + trusted_proxies: '%env(TRUSTED_PROXIES)%' + +.. danger:: + + The "trusted proxies" feature does not work as expected when using the + `nginx realip module`_. Disable that module when serving Symfony applications. + +But what if the IP of my Reverse Proxy Changes Constantly! +---------------------------------------------------------- + +Some reverse proxies (like AWS Elastic Load Balancing) don't have a +static IP address or even a range that you can target with the CIDR notation. +In this case, you'll need to - *very carefully* - trust *all* proxies. + +#. Configure your web server(s) to *not* respond to traffic from *any* clients + other than your load balancers. For AWS, this can be done with `security groups`_. + +#. Once you've guaranteed that traffic will only come from your trusted reverse + proxies, configure Symfony to *always* trust incoming request: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + # trust *all* requests (the 'REMOTE_ADDR' string is replaced at + # runtime by $_SERVER['REMOTE_ADDR']) + trusted_proxies: '127.0.0.1,REMOTE_ADDR' + + # you can also use the 'PRIVATE_SUBNETS' string, which is replaced at + # runtime by the IpUtils::PRIVATE_SUBNETS constant + # trusted_proxies: '127.0.0.1,PRIVATE_SUBNETS' + +.. versionadded:: 7.2 + + The support for the ``'PRIVATE_SUBNETS'`` string was introduced in Symfony 7.2. + +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. + +If you are also using a reverse proxy on top of your load balancer (e.g. +`CloudFront`_), calling ``$request->server->get('REMOTE_ADDR')`` won't be +enough, as it will only trust the node sitting directly above your application +(in this case your load balancer). You also need to append the IP addresses or +ranges of any additional proxy (e.g. `CloudFront IP ranges`_) to the array of +trusted proxies. + +Reverse proxy in a subpath / subfolder +-------------------------------------- + +If your Symfony application runs behind a reverse proxy and it's served in a +subpath/subfolder, Symfony might generate incorrect URLs that ignore the +subpath/subfolder of the reverse proxy. + +To fix this, you need to pass the subpath/subfolder route prefix of the reverse +proxy to Symfony by setting the ``X-Forwarded-Prefix`` header. The header can +normally be configured in your reverse proxy configuration. Configure +``X-Forwarded-Prefix`` as trusted header to be able to use this feature. + +The ``X-Forwarded-Prefix`` is used by Symfony to prefix the base URL of request +objects, which is used to generate absolute paths and URLs in Symfony applications. +Without the header, the base URL would be only determined based on the configuration +of the web server running Symfony, which leads to incorrect paths/URLs, when the +application is served under a subpath/subfolder by a reverse proxy. + +For example if your Symfony application is directly served under a URL like +``https://symfony.tld/`` and you would like to use a reverse proxy to serve the +application under ``https://public.tld/app/``, you would need to set the +``X-Forwarded-Prefix`` header to ``/app/`` in your reverse proxy configuration. +Without the header, Symfony would generate URLs based on its server base URL +(e.g. ``/my/route``) instead of the correct ``/app/my/route``, which is +required to access the route via the reverse proxy. + +The header can be different for each reverse proxy, so that access via different +reverse proxies served under different subpaths/subfolders can be handled correctly. + +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['HTTP_X_FORWARDED_PROTO'] = $_SERVER['HTTP_CUSTOM_FORWARDED_PROTO']; + // ... + $response = $kernel->handle($request); + +Overriding Configuration Behind Hidden SSL Termination +------------------------------------------------------ + +Some cloud setups (like running a Docker container with the "Web App for Containers" +in `Microsoft Azure`_) do SSL termination and contact your web server over HTTP, but +do not change the remote address nor set the ``X-Forwarded-*`` headers. This means +the trusted proxy feature of Symfony can't help you. + +Once you made sure your server is only reachable through the cloud proxy over HTTPS +and not through HTTP, you can override the information your web server sends to PHP. +For Nginx, this could look like this: + +.. code-block:: nginx + + location ~ ^/index\.php$ { + fastcgi_pass 127.0.0.1:9000; + include fastcgi.conf; + # Lie to Symfony about the protocol and port so that it generates the correct HTTPS URLs + fastcgi_param SERVER_PORT "443"; + fastcgi_param HTTPS "on"; + } + +.. _`security groups`: https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-groups.html +.. _`CloudFront`: https://en.wikipedia.org/wiki/Amazon_CloudFront +.. _`CloudFront IP ranges`: https://ip-ranges.amazonaws.com/ip-ranges.json +.. _`HTTP Host header attacks`: https://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html +.. _`nginx realip module`: https://nginx.org/en/docs/http/ngx_http_realip_module.html +.. _`Microsoft Azure`: https://en.wikipedia.org/wiki/Microsoft_Azure diff --git a/doctrine.rst b/doctrine.rst new file mode 100644 index 00000000000..171f8a3348a --- /dev/null +++ b/doctrine.rst @@ -0,0 +1,1128 @@ +Databases and the Doctrine ORM +============================== + +.. admonition:: Screencast + :class: screencast + + Do you prefer video tutorials? Check out the `Doctrine screencast series`_. + +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. + +Databases are a broad topic, so the documentation is divided in three articles: + +* 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`` :ref:`Symfony pack `, +as well as the MakerBundle, which will help generate some code: + +.. code-block:: terminal + + $ composer require symfony/orm-pack + $ composer require --dev symfony/maker-bundle + +Configuring the Database +~~~~~~~~~~~~~~~~~~~~~~~~ + +The database connection information is stored as an environment variable called +``DATABASE_URL``. For development, you can find and customize this inside ``.env``: + +.. code-block:: text + + # .env (or override DATABASE_URL in .env.local to avoid committing your changes) + + # customize this line! + DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=8.0.37" + + # to use mariadb: + # Before doctrine/dbal < 3.7 + # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=mariadb-10.5.8" + # Since doctrine/dbal 3.7 + # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=10.5.8-MariaDB" + + # to use sqlite: + # DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db" + + # to use postgresql: + # DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=12.19 (Debian 12.19-1.pgdg120+1)&charset=utf8" + + # to use oracle: + # DATABASE_URL="oci8://db_user:db_password@127.0.0.1:1521/db_name" + +.. warning:: + + If the username, password, host or database name contain any character considered + special in a URI (such as ``: / ? # [ ] @ ! $ & ' ( ) * + , ; =``), + you must encode them. See `RFC 3986`_ for the full list of reserved characters. + You can use the :phpfunction:`urlencode` function to encode them or + the :ref:`urlencode environment variable processor `. + In this case you need to remove the ``resolve:`` prefix in ``config/packages/doctrine.yaml`` + to avoid errors: ``url: '%env(DATABASE_URL)%'`` + +Now that your connection parameters are setup, Doctrine can create the ``db_name`` +database for you: + +.. code-block:: terminal + + $ php bin/console doctrine:database:create + +There are more options in ``config/packages/doctrine.yaml`` that you can configure, +including your ``server_version`` (e.g. 8.0.37 if you're using MySQL 8.0.37), which may +affect how Doctrine functions. + +.. tip:: + + There are many other Doctrine commands. Run ``php bin/console list doctrine`` + to see a full list. + +.. _doctrine-adding-mapping: + +Creating an Entity Class +------------------------ + +Suppose you're building an application where products need to be displayed. +Without even thinking about Doctrine or databases, you already know that +you need a ``Product`` object to represent those products. + +You can use the ``make:entity`` command to create this class and any fields you +need. The command will ask you some questions - answer them like done below: + +.. code-block:: bash + + $ php bin/console make:entity + + Class name of the entity to create or update: + > Product + + New property name (press to stop adding fields): + > name + + Field type (enter ? to see all types) [string]: + > string + + Field length [255]: + > 255 + + Can this field be null in the database (nullable) (yes/no) [no]: + > no + + New property name (press to stop adding fields): + > price + + Field type (enter ? to see all types) [string]: + > integer + + Can this field be null in the database (nullable) (yes/no) [no]: + > no + + New property name (press to stop adding fields): + > + (press enter again to finish) + +Whoa! You now have a new ``src/Entity/Product.php`` file:: + + // src/Entity/Product.php + namespace App\Entity; + + use App\Repository\ProductRepository; + use Doctrine\ORM\Mapping as ORM; + + #[ORM\Entity(repositoryClass: ProductRepository::class)] + class Product + { + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; + + #[ORM\Column(length: 255)] + private ?string $name = null; + + #[ORM\Column] + private ?int $price = null; + + public function getId(): ?int + { + return $this->id; + } + + // ... getter and setter methods + } + +.. tip:: + + Starting in `MakerBundle`_: v1.57.0 - You can pass either ``--with-uuid`` or + ``--with-ulid`` to ``make:entity``. Leveraging Symfony's :doc:`Uid Component `, + this generates an entity with the ``id`` type as :ref:`Uuid ` + or :ref:`Ulid ` instead of ``int``. + +.. note:: + + Starting in v1.44.0 - `MakerBundle`_: only supports entities using PHP attributes. + +.. note:: + + Confused why the price is an integer? Don't worry: this is just an example. + But, storing prices as integers (e.g. 100 = $1 USD) can avoid rounding issues. + +.. warning:: + + There is a `limit of 767 bytes for the index key prefix`_ when using + InnoDB tables in MySQL 5.6 and earlier versions. String columns with 255 + character length and ``utf8mb4`` encoding surpass that limit. This means + that any column of type ``string`` and ``unique=true`` must set its + maximum ``length`` to ``190``. Otherwise, you'll see this error: + *"[PDOException] SQLSTATE[42000]: Syntax error or access violation: + 1071 Specified key was too long; max key length is 767 bytes"*. + +This class is called an "entity". And soon, you'll be able to save and query Product +objects to a ``product`` table in your database. Each property in the ``Product`` +entity can be mapped to a column in that table. This is usually done with attributes: +the ``#[ORM\Column(...)]`` comments that you see above each property: + +.. raw:: html + + + +The ``make:entity`` command is a tool to make life easier. But this is *your* code: +add/remove fields, add/remove methods or update configuration. + +Doctrine supports a wide variety of field types, each with their own options. +Check out the `list of Doctrine mapping types`_ in the Doctrine documentation. +If you want to use XML instead of attributes, add ``type: xml`` and +``dir: '%kernel.project_dir%/config/doctrine'`` to the entity mappings in your +``config/packages/doctrine.yaml`` file. + +.. warning:: + + Be careful not to use reserved SQL keywords as your table or column names + (e.g. ``GROUP`` or ``USER``). See Doctrine's `Reserved SQL keywords documentation`_ + for details on how to escape these. Or, change the table name with + ``#[ORM\Table(name: 'groups')]`` above the class or configure the column name with + the ``name: 'group_name'`` option. + +.. _doctrine-creating-the-database-tables-schema: + +Migrations: Creating the Database Tables/Schema +----------------------------------------------- + +The ``Product`` class is fully-configured and ready to save to a ``product`` table. +If you just defined this class, your database doesn't actually have the ``product`` +table yet. To add it, you can leverage the `DoctrineMigrationsBundle`_, which is +already installed: + +.. code-block:: terminal + + $ php bin/console make:migration + +.. tip:: + + Starting in `MakerBundle`_: v1.56.0 - Passing ``--formatted`` to ``make:migration`` + generates a nice and tidy migration file. + +If everything worked, you should see something like this: + +.. code-block:: text + + SUCCESS! + + Next: Review the new migration "migrations/Version20211116204726.php" + Then: Run the migration with php bin/console doctrine:migrations:migrate + +If you open this file, it contains the SQL needed to update your database! To run +that SQL, execute your migrations: + +.. code-block:: terminal + + $ php bin/console doctrine:migrations:migrate + +This command executes all migration files that have not already been run against +your database. You should run this command on production when you deploy to keep +your production database up-to-date. + +.. _doctrine-add-more-fields: + +Migrations & Adding more Fields +------------------------------- + +But what if you need to add a new field property to ``Product``, like a +``description``? You can edit the class to add the new property. But, you can +also use ``make:entity`` again: + +.. code-block:: bash + + $ php bin/console make:entity + + Class name of the entity to create or update + > Product + + New property name (press to stop adding fields): + > description + + Field type (enter ? to see all types) [string]: + > text + + Can this field be null in the database (nullable) (yes/no) [no]: + > no + + New property name (press to stop adding fields): + > + (press enter again to finish) + +This adds the new ``description`` property and ``getDescription()`` and ``setDescription()`` +methods: + +.. code-block:: diff + + // src/Entity/Product.php + // ... + + use Doctrine\DBAL\Types\Types; + + class Product + { + // ... + + + #[ORM\Column(type: Types::TEXT)] + + private string $description; + + // getDescription() & setDescription() were also added + } + +The new property is mapped, but it doesn't exist yet in the ``product`` table. No +problem! Generate a new migration: + +.. code-block:: terminal + + $ php bin/console make:migration + +This time, the SQL in the generated file will look like this: + +.. code-block:: sql + + ALTER TABLE product ADD description LONGTEXT NOT NULL + +The migration system is *smart*. It compares all of your entities with the current +state of the database and generates the SQL needed to synchronize them! Like +before, execute your migrations: + +.. code-block:: terminal + + $ php bin/console doctrine:migrations:migrate + +.. warning:: + + If you are using an SQLite database, you'll see the following error: + *PDOException: SQLSTATE[HY000]: General error: 1 Cannot add a NOT NULL + column with default value NULL*. Add a ``nullable=true`` option to the + ``description`` property to fix the problem. + +This will only execute the *one* new migration file, because DoctrineMigrationsBundle +knows that the first migration was already executed earlier. Behind the scenes, it +manages a ``migration_versions`` table to track this. + +Each time you make a change to your schema, run these two commands to generate the +migration and then execute it. Be sure to commit the migration files and execute +them when you deploy. + +.. _doctrine-generating-getters-and-setters: + +.. tip:: + + If you prefer to add new properties manually, the ``make:entity`` command can + generate the getter & setter methods for you: + + .. code-block:: terminal + + $ php bin/console make:entity --regenerate + + If you make some changes and want to regenerate *all* getter/setter methods, + also pass ``--overwrite``. + +Persisting Objects to the Database +---------------------------------- + +It's time to save a ``Product`` object to the database! Let's create a new controller +to experiment: + +.. code-block:: terminal + + $ php bin/console make:controller ProductController + +Inside the controller, you can create a new ``Product`` object, set data on it, +and save it:: + + // src/Controller/ProductController.php + namespace App\Controller; + + // ... + use App\Entity\Product; + use Doctrine\ORM\EntityManagerInterface; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + + class ProductController extends AbstractController + { + #[Route('/product', name: 'create_product')] + public function createProduct(EntityManagerInterface $entityManager): Response + { + $product = new Product(); + $product->setName('Keyboard'); + $product->setPrice(1999); + $product->setDescription('Ergonomic and stylish!'); + + // tell Doctrine you want to (eventually) save the Product (no queries yet) + $entityManager->persist($product); + + // actually executes the queries (i.e. the INSERT query) + $entityManager->flush(); + + return new Response('Saved new product with id '.$product->getId()); + } + } + +Try it out! + + http://localhost:8000/product + +Congratulations! You just created your first row in the ``product`` table. To prove it, +you can query the database directly: + +.. code-block:: terminal + + $ php bin/console dbal:run-sql 'SELECT * FROM product' + + # on Windows systems not using Powershell, run this command instead: + # php bin/console dbal:run-sql "SELECT * FROM product" + +Take a look at the previous example in more detail: + +.. _doctrine-entity-manager: + +* **line 13** The ``EntityManagerInterface $entityManager`` argument tells Symfony + to :ref:`inject the Entity Manager service ` into + the controller method. This object is responsible for saving objects to, and + fetching objects from, the database. + +* **lines 15-18** In this section, you instantiate and work with the ``$product`` + object like any other normal PHP object. + +* **line 21** The ``persist($product)`` call tells Doctrine to "manage" the + ``$product`` object. This does **not** cause a query to be made to the database. + +* **line 24** When the ``flush()`` method is called, Doctrine looks through + all of the objects that it's managing to see if they need to be persisted + to the database. In this example, the ``$product`` object's data doesn't + exist in the database, so the entity manager executes an ``INSERT`` query, + creating a new row in the ``product`` table. + +.. note:: + + If the ``flush()`` call fails, a ``Doctrine\ORM\ORMException`` exception + is thrown. See `Transactions and Concurrency`_. + +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 ` can reuse Doctrine metadata to perform +some basic validation tasks. First, add or configure the +:ref:`auto_mapping option ` to define which +entities should be introspected by Symfony to add automatic validation constraints. + +Consider the following controller code:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use App\Entity\Product; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + use Symfony\Component\Validator\Validator\ValidatorInterface; + // ... + + class ProductController extends AbstractController + { + #[Route('/product', name: 'create_product')] + public function createProduct(ValidatorInterface $validator): Response + { + $product = new Product(); + + // ... update the product data somehow (e.g. with a form) ... + + $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 `, if the ``auto_mapping`` option +includes it in the list of entities to introspect, Symfony will infer some +validation rules for it and will apply them. + +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. + +Fetching Objects from the Database +---------------------------------- + +Fetching an object back out of the database is even easier. Suppose you want to +be able to go to ``/product/1`` to see your new product:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use App\Entity\Product; + use Doctrine\ORM\EntityManagerInterface; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + // ... + + class ProductController extends AbstractController + { + #[Route('/product/{id}', name: 'product_show')] + public function show(EntityManagerInterface $entityManager, int $id): Response + { + $product = $entityManager->getRepository(Product::class)->find($id); + + if (!$product) { + throw $this->createNotFoundException( + 'No product found for id '.$id + ); + } + + return new Response('Check out this great product: '.$product->getName()); + + // or render a template + // in the template, print things with {{ product.name }} + // return $this->render('product/show.html.twig', ['product' => $product]); + } + } + +Another possibility is to use the ``ProductRepository`` using Symfony's autowiring +and injected by the dependency injection container:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use App\Entity\Product; + use App\Repository\ProductRepository; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + // ... + + class ProductController extends AbstractController + { + #[Route('/product/{id}', name: 'product_show')] + public function show(ProductRepository $productRepository, int $id): Response + { + $product = $productRepository + ->find($id); + + // ... + } + } + +Try it out! + + http://localhost:8000/product/1 + +When you query for a particular type of object, you always use what's known +as its "repository". You can think of a repository as a PHP class whose only +job is to help you fetch entities of a certain class. + +Once you have a repository object, you have many helper methods:: + + $repository = $entityManager->getRepository(Product::class); + + // look for a single Product by its primary key (usually "id") + $product = $repository->find($id); + + // look for a single Product by name + $product = $repository->findOneBy(['name' => 'Keyboard']); + // or find by name and price + $product = $repository->findOneBy([ + 'name' => 'Keyboard', + 'price' => 1999, + ]); + + // look for multiple Product objects matching the name, ordered by price + $products = $repository->findBy( + ['name' => 'Keyboard'], + ['price' => 'ASC'] + ); + + // look for *all* Product objects + $products = $repository->findAll(); + +You can also add *custom* methods for more complex queries! More on that later in +the :ref:`doctrine-queries` section. + +.. tip:: + + When rendering an HTML page, the web debug toolbar at the bottom of the page + will display the number of queries and the time it took to execute them: + + .. image:: /_images/doctrine/doctrine_web_debug_toolbar.png + :alt: The web dev toolbar showing the Doctrine item. + :class: with-browser + + 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, install the ``profiler`` :ref:`Symfony pack ` + by running this command: ``composer require --dev symfony/profiler-pack``. + + For more information, read the :doc:`Symfony profiler documentation `. + +.. _doctrine-entity-value-resolver: + +Automatically Fetching Objects (EntityValueResolver) +---------------------------------------------------- + +.. versionadded:: 2.7.1 + + Autowiring of the ``EntityValueResolver`` was introduced in DoctrineBundle 2.7.1. + +In many cases, you can use the ``EntityValueResolver`` to do the query for you +automatically! You can simplify the controller to:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use App\Entity\Product; + use App\Repository\ProductRepository; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + // ... + + class ProductController extends AbstractController + { + #[Route('/product/{id}')] + public function show(Product $product): Response + { + // use the Product! + // ... + } + } + +That's it! The bundle uses the ``{id}`` from the route to query for the ``Product`` +by the ``id`` column. If it's not found, a 404 page is generated. + +.. tip:: + + When enabled globally, it's possible to disable the behavior on a specific + controller, by using the ``MapEntity`` set to ``disabled``:: + + public function show( + #[CurrentUser] + #[MapEntity(disabled: true)] + User $user + ): Response { + // User is not resolved by the EntityValueResolver + // ... + } + +Fetch Automatically +~~~~~~~~~~~~~~~~~~~ + +If your route wildcards match properties on your entity, then the resolver +will automatically fetch them:: + + /** + * Fetch via primary key because {id} is in the route. + */ + #[Route('/product/{id}')] + public function showByPk(Product $product): Response + { + } + + /** + * Perform a findOneBy() where the slug property matches {slug}. + */ + #[Route('/product/{slug}')] + public function showBySlug(Product $product): Response + { + } + +Automatic fetching works in these situations: + +* If ``{id}`` is in your route, then this is used to fetch by + primary key via the ``find()`` method. + +* The resolver will attempt to do a ``findOneBy()`` fetch by using + *all* of the wildcards in your route that are actually properties + on your entity (non-properties are ignored). + +This behavior is enabled by default on all controllers. If you prefer, you can +restrict this feature to only work on route wildcards called ``id`` to look for +entities by primary key. To do so, set the option +``doctrine.orm.controller_resolver.auto_mapping`` to ``false``. + +When ``auto_mapping`` is disabled, you can configure the mapping explicitly for +any controller argument with the ``MapEntity`` attribute. You can even control +the ``EntityValueResolver`` behavior by using the `MapEntity options`_ :: + + // src/Controller/ProductController.php + namespace App\Controller; + + use App\Entity\Product; + use Symfony\Bridge\Doctrine\Attribute\MapEntity; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + // ... + + class ProductController extends AbstractController + { + #[Route('/product/{slug}')] + public function show( + #[MapEntity(mapping: ['slug' => 'slug'])] + Product $product + ): Response { + // use the Product! + // ... + } + } + +Fetch via an Expression +~~~~~~~~~~~~~~~~~~~~~~~ + +If automatic fetching doesn't work for your use case, you can write an expression +using the :doc:`ExpressionLanguage component `:: + + #[Route('/product/{product_id}')] + public function show( + #[MapEntity(expr: 'repository.find(product_id)')] + Product $product + ): Response { + } + +In the expression, the ``repository`` variable will be your entity's +Repository class and any route wildcards - like ``{product_id}`` are +available as variables. + +The repository method called in the expression can also return a list of entities. +In that case, update the type of your controller argument:: + + #[Route('/posts_by/{author_id}')] + public function authorPosts( + #[MapEntity(class: Post::class, expr: 'repository.findBy({"author": author_id}, {}, 10)')] + iterable $posts + ): Response { + } + +.. versionadded:: 7.1 + + The mapping of the lists of entities was introduced in Symfony 7.1. + +This can also be used to help resolve multiple arguments:: + + #[Route('/product/{id}/comments/{comment_id}')] + public function show( + Product $product, + #[MapEntity(expr: 'repository.find(comment_id)')] + Comment $comment + ): Response { + } + +In the example above, the ``$product`` argument is handled automatically, +but ``$comment`` is configured with the attribute since they cannot both follow +the default convention. + +If you need to get other information from the request to query the database, you +can also access the request in your expression thanks to the ``request`` +variable. Let's say you want the first or the last comment of a product depending on a query parameter named ``sort``:: + + #[Route('/product/{id}/comments')] + public function show( + Product $product, + #[MapEntity(expr: 'repository.findOneBy({"product": id}, {"createdAt": request.query.get("sort", "DESC")})')] + Comment $comment + ): Response { + } + +MapEntity Options +~~~~~~~~~~~~~~~~~ + +A number of options are available on the ``MapEntity`` attribute to +control behavior: + +``id`` + If an ``id`` option is configured and matches a route parameter, then + the resolver will find by the primary key:: + + #[Route('/product/{product_id}')] + public function show( + #[MapEntity(id: 'product_id')] + Product $product + ): Response { + } + +``mapping`` + Configures the properties and values to use with the ``findOneBy()`` + method: the key is the route placeholder name and the value is the Doctrine + property name:: + + #[Route('/product/{category}/{slug}/comments/{comment_slug}')] + public function show( + #[MapEntity(mapping: ['category' => 'category', 'slug' => 'slug'])] + Product $product, + #[MapEntity(mapping: ['comment_slug' => 'slug'])] + Comment $comment + ): Response { + } + +``exclude`` + Configures the properties that should be used in the ``findOneBy()`` + method by *excluding* one or more properties so that not *all* are used:: + + #[Route('/product/{slug}/{date}')] + public function show( + #[MapEntity(exclude: ['date'])] + Product $product, + \DateTime $date + ): Response { + } + +``stripNull`` + If true, then when ``findOneBy()`` is used, any values that are + ``null`` will not be used for the query. + +``objectManager`` + By default, the ``EntityValueResolver`` uses the *default* + object manager, but you can configure this:: + + #[Route('/product/{id}')] + public function show( + #[MapEntity(objectManager: 'foo')] + Product $product + ): Response { + } + +``evictCache`` + If true, forces Doctrine to always fetch the entity from the database + instead of cache. + +``disabled`` + If true, the ``EntityValueResolver`` will not try to replace the argument. + +``message`` + An optional custom message displayed when there's a :class:`Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException`, + but **only in the development environment** (you won't see this message in production):: + + #[Route('/product/{product_id}')] + public function show( + #[MapEntity(id: 'product_id', message: 'The product does not exist')] + Product $product + ): Response { + } + +.. versionadded:: 7.1 + + The ``message`` option was introduced in Symfony 7.1. + +Updating an Object +------------------ + +Once you've fetched an object from Doctrine, you interact with it the same as +with any PHP model:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use App\Entity\Product; + use App\Repository\ProductRepository; + use Doctrine\ORM\EntityManagerInterface; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + // ... + + class ProductController extends AbstractController + { + #[Route('/product/edit/{id}', name: 'product_edit')] + public function update(EntityManagerInterface $entityManager, int $id): Response + { + $product = $entityManager->getRepository(Product::class)->find($id); + + if (!$product) { + throw $this->createNotFoundException( + 'No product found for id '.$id + ); + } + + $product->setName('New product name!'); + $entityManager->flush(); + + return $this->redirectToRoute('product_show', [ + 'id' => $product->getId() + ]); + } + } + +Using Doctrine to edit an existing product consists of three steps: + +#. fetching the object from Doctrine; +#. modifying the object; +#. calling ``flush()`` on the entity manager. + +You *can* call ``$entityManager->persist($product)``, but it isn't necessary: +Doctrine is already "watching" your object for changes. + +Deleting an Object +------------------ + +Deleting an object is very similar, but requires a call to the ``remove()`` +method of the entity manager:: + + $entityManager->remove($product); + $entityManager->flush(); + +As you might expect, the ``remove()`` method notifies Doctrine that you'd +like to remove the given object from the database. The ``DELETE`` query isn't +actually executed until the ``flush()`` method is called. + +.. _doctrine-queries: + +Querying for Objects: The Repository +------------------------------------ + +You've already seen how the repository object allows you to run basic queries +without any work:: + + // from inside a controller + $repository = $entityManager->getRepository(Product::class); + $product = $repository->find($id); + +But what if you need a more complex query? When you generated your entity with +``make:entity``, the command *also* generated a ``ProductRepository`` class:: + + // src/Repository/ProductRepository.php + namespace App\Repository; + + use App\Entity\Product; + use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; + use Doctrine\Persistence\ManagerRegistry; + + class ProductRepository extends ServiceEntityRepository + { + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Product::class); + } + } + +When you fetch your repository (i.e. ``->getRepository(Product::class)``), it is +*actually* an instance of *this* object! This is because of the ``repositoryClass`` +config that was generated at the top of your ``Product`` entity class. + +Suppose you want to query for all Product objects greater than a certain price. Add +a new method for this to your repository:: + + // src/Repository/ProductRepository.php + + // ... + class ProductRepository extends ServiceEntityRepository + { + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Product::class); + } + + /** + * @return Product[] + */ + public function findAllGreaterThanPrice(int $price): array + { + $entityManager = $this->getEntityManager(); + + $query = $entityManager->createQuery( + 'SELECT p + FROM App\Entity\Product p + WHERE p.price > :price + ORDER BY p.price ASC' + )->setParameter('price', $price); + + // returns an array of Product objects + return $query->getResult(); + } + } + +The string passed to ``createQuery()`` might look like SQL, but it is +`Doctrine Query Language`_. This allows you to type queries using commonly +known query language, but referencing PHP objects instead (i.e. in the ``FROM`` +statement). + +Now, you can call this method on the repository:: + + // from inside a controller + $minPrice = 1000; + + $products = $entityManager->getRepository(Product::class)->findAllGreaterThanPrice($minPrice); + + // ... + +See :ref:`services-constructor-injection` for how to inject the repository into +any service. + +Querying with the Query Builder +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Doctrine also provides a `Query Builder`_, an object-oriented way to write +queries. It is recommended to use this when queries are built dynamically (i.e. +based on PHP conditions):: + + // src/Repository/ProductRepository.php + + // ... + class ProductRepository extends ServiceEntityRepository + { + public function findAllGreaterThanPrice(int $price, bool $includeUnavailableProducts = false): array + { + // automatically knows to select Products + // the "p" is an alias you'll use in the rest of the query + $qb = $this->createQueryBuilder('p') + ->where('p.price > :price') + ->setParameter('price', $price) + ->orderBy('p.price', 'ASC'); + + if (!$includeUnavailableProducts) { + $qb->andWhere('p.available = TRUE'); + } + + $query = $qb->getQuery(); + + return $query->execute(); + + // to get just one result: + // $product = $query->setMaxResults(1)->getOneOrNullResult(); + } + } + +Querying with SQL +~~~~~~~~~~~~~~~~~ + +In addition, you can query directly with SQL if you need to:: + + // src/Repository/ProductRepository.php + + // ... + class ProductRepository extends ServiceEntityRepository + { + public function findAllGreaterThanPrice(int $price): array + { + $conn = $this->getEntityManager()->getConnection(); + + $sql = ' + SELECT * FROM product p + WHERE p.price > :price + ORDER BY p.price ASC + '; + + $resultSet = $conn->executeQuery($sql, ['price' => $price]); + + // returns an array of arrays (i.e. a raw data set) + return $resultSet->fetchAllAssociative(); + } + } + +With SQL, you will get back raw data, not objects (unless you use the `NativeQuery`_ +functionality). + +Configuration +------------- + +See the :doc:`Doctrine config reference `. + +Relationships and Associations +------------------------------ + +Doctrine provides all the functionality you need to manage database relationships +(also known as associations), including ManyToOne, OneToMany, OneToOne and ManyToMany +relationships. + +For info, see :doc:`/doctrine/associations`. + +Database Testing +---------------- + +Read the article about :doc:`testing code that interacts with the database `. + +Doctrine Extensions (Timestampable, Translatable, etc.) +------------------------------------------------------- + +Doctrine community has created some extensions to implement common needs such as +*"set the value of the createdAt property automatically when creating an entity"*. +Read more about the `available Doctrine extensions`_ and use the +`StofDoctrineExtensionsBundle`_ to integrate them in your application. + +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + + doctrine/associations + doctrine/events + doctrine/custom_dql_functions + doctrine/dbal + doctrine/multiple_entity_managers + doctrine/resolve_target_entity + testing/database + +.. _`Doctrine`: https://www.doctrine-project.org/ +.. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt +.. _`list of Doctrine mapping types`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/basic-mapping.html#reference-mapping-types +.. _`Query Builder`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/query-builder.html +.. _`Doctrine Query Language`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/dql-doctrine-query-language.html +.. _`Reserved SQL keywords documentation`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/basic-mapping.html#quoting-reserved-words +.. _`DoctrineMongoDBBundle docs`: https://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html +.. _`Transactions and Concurrency`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/transactions-and-concurrency.html +.. _`DoctrineMigrationsBundle`: https://github.com/doctrine/DoctrineMigrationsBundle +.. _`NativeQuery`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/native-sql.html +.. _`limit of 767 bytes for the index key prefix`: https://dev.mysql.com/doc/refman/5.6/en/innodb-limits.html +.. _`Doctrine screencast series`: https://symfonycasts.com/screencast/symfony-doctrine +.. _`API Platform`: https://api-platform.com/docs/core/validation/ +.. _`PDO`: https://www.php.net/pdo +.. _`available Doctrine extensions`: https://github.com/doctrine-extensions/DoctrineExtensions +.. _`StofDoctrineExtensionsBundle`: https://github.com/stof/StofDoctrineExtensionsBundle +.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html diff --git a/doctrine/associations.rst b/doctrine/associations.rst new file mode 100644 index 00000000000..8dd9aa7f36b --- /dev/null +++ b/doctrine/associations.rst @@ -0,0 +1,636 @@ +How to Work with Doctrine Associations / Relations +================================================== + +.. admonition:: Screencast + :class: screencast + + Do you prefer video tutorials? Check out the `Mastering Doctrine Relations`_ + screencast series. + +There are **two** main relationship/association types: + +``ManyToOne`` / ``OneToMany`` + The most common relationship, mapped in the database with a foreign + key column (e.g. a ``category_id`` column on the ``product`` table). This is + actually only *one* association type, but seen from the two different *sides* + of the relation. + +``ManyToMany`` + Uses a join table and is needed when both sides of the relationship can have + many of the other side (e.g. "students" and "classes": each student is in many + classes, and each class has many students). + +First, you need to determine which relationship to use. If both sides of the relation +will contain many of the other side (e.g. "students" and "classes"), you need a +``ManyToMany`` relation. Otherwise, you likely need a ``ManyToOne``. + +.. tip:: + + There is also a OneToOne relationship (e.g. one User has one Profile and vice + versa). In practice, using this is similar to ``ManyToOne``. + +The ManyToOne / OneToMany Association +------------------------------------- + +Suppose that each product in your application belongs to exactly one category. +In this case, you'll need a ``Category`` class, and a way to relate a +``Product`` object to a ``Category`` object. + +Start by creating a ``Category`` entity with a ``name`` field: + +.. code-block:: bash + + $ php bin/console make:entity Category + + New property name (press to stop adding fields): + > name + + Field type (enter ? to see all types) [string]: + > string + + Field length [255]: + > 255 + + Can this field be null in the database (nullable) (yes/no) [no]: + > no + + New property name (press to stop adding fields): + > + (press enter again to finish) + +This will generate your new entity class:: + + // src/Entity/Category.php + namespace App\Entity; + + // ... + + #[ORM\Entity(repositoryClass: CategoryRepository::class)] + class Category + { + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private $id; + + #[ORM\Column] + private string $name; + + // ... getters and setters + } + +.. tip:: + + Starting in `MakerBundle`_: v1.57.0 - You can pass either ``--with-uuid`` or + ``--with-ulid`` to ``make:entity``. Leveraging Symfony's :doc:`Uid Component `, + this generates an entity with the ``id`` type as :ref:`Uuid ` + or :ref:`Ulid ` instead of ``int``. + +Mapping the ManyToOne Relationship +---------------------------------- + +In this example, each category can be associated with *many* products. But, +each product can be associated with only *one* category. This relationship +can be summarized as: *many* products to *one* category (or equivalently, +*one* category to *many* products). + +From the perspective of the ``Product`` entity, this is a many-to-one relationship. +From the perspective of the ``Category`` entity, this is a one-to-many relationship. + +To map this, first create a ``category`` property on the ``Product`` class with +the ``ManyToOne`` attribute. You can do this by hand, or by using the ``make:entity`` +command, which will ask you several questions about your relationship. If you're +not sure of the answer, don't worry! You can always change the settings later: + +.. code-block:: bash + + $ php bin/console make:entity + + Class name of the entity to create or update (e.g. BraveChef): + > Product + + New property name (press to stop adding fields): + > category + + Field type (enter ? to see all types) [string]: + > relation + + What class should this entity be related to?: + > Category + + Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]: + > ManyToOne + + Is the Product.category property allowed to be null (nullable)? (yes/no) [yes]: + > no + + Do you want to add a new property to Category so that you can access/update + Product objects from it - e.g. $category->getProducts()? (yes/no) [yes]: + > yes + + New field name inside Category [products]: + > products + + Do you want to automatically delete orphaned App\Entity\Product objects + (orphanRemoval)? (yes/no) [no]: + > no + + New property name (press to stop adding fields): + > + (press enter again to finish) + +This made changes to *two* entities. First, it added a new ``category`` property to +the ``Product`` entity (and getter & setter methods): + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Product.php + namespace App\Entity; + + // ... + class Product + { + // ... + + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'products')] + private Category $category; + + public function getCategory(): ?Category + { + return $this->category; + } + + public function setCategory(?Category $category): self + { + $this->category = $category; + + return $this; + } + } + + .. code-block:: yaml + + # src/Resources/config/doctrine/Product.orm.yml + App\Entity\Product: + type: entity + # ... + manyToOne: + category: + targetEntity: App\Entity\Category + inversedBy: products + joinColumn: + nullable: false + + .. code-block:: xml + + + + + + + + + + + + + +This ``ManyToOne`` mapping is required. It tells Doctrine to use the ``category_id`` +column on the ``product`` table to relate each record in that table with +a record in the ``category`` table. + +Next, since *one* ``Category`` object will relate to *many* ``Product`` objects, +the ``make:entity`` command *also* added a ``products`` property to the ``Category`` +class that will hold these objects: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Category.php + namespace App\Entity; + + // ... + use Doctrine\Common\Collections\ArrayCollection; + use Doctrine\Common\Collections\Collection; + + class Category + { + // ... + + #[ORM\OneToMany(targetEntity: Product::class, mappedBy: 'category')] + private Collection $products; + + public function __construct() + { + $this->products = new ArrayCollection(); + } + + /** + * @return Collection + */ + public function getProducts(): Collection + { + return $this->products; + } + + // addProduct() and removeProduct() were also added + } + + .. code-block:: yaml + + # src/Resources/config/doctrine/Category.orm.yml + App\Entity\Category: + type: entity + # ... + oneToMany: + products: + targetEntity: App\Entity\Product + mappedBy: category + # Don't forget to initialize the collection in + # the __construct() method of the entity + + .. code-block:: xml + + + + + + + + + + + + + +The ``ManyToOne`` mapping shown earlier is *required*, But, this ``OneToMany`` +is optional: only add it *if* you want to be able to access the products that are +related to a category (this is one of the questions ``make:entity`` asks you). In +this example, it *will* be useful to be able to call ``$category->getProducts()``. +If you don't want it, then you also don't need the ``inversedBy`` or ``mappedBy`` +config. + +.. sidebar:: What is the ArrayCollection Stuff? + + The code inside ``__construct()`` is important: The ``$products`` property must + be a collection object that implements Doctrine's ``Collection`` interface. + In this case, an `ArrayCollection`_ object is used. This looks and acts almost + *exactly* like an array, but has some added flexibility. Just imagine that + it is an ``array`` and you'll be in good shape. + +Your database is set up! Now, run the migrations like normal: + +.. code-block:: terminal + + $ php bin/console doctrine:migrations:diff + $ php bin/console doctrine:migrations:migrate + +Thanks to the relationship, this creates a ``category_id`` foreign key column on +the ``product`` table. Doctrine is ready to persist our relationship! + +Saving Related Entities +----------------------- + +Now you can see this new code in action! Imagine you're inside a controller:: + + // src/Controller/ProductController.php + namespace App\Controller; + + // ... + use App\Entity\Category; + use App\Entity\Product; + use Doctrine\ORM\EntityManagerInterface; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + + class ProductController extends AbstractController + { + #[Route('/product', name: 'product')] + public function index(EntityManagerInterface $entityManager): Response + { + $category = new Category(); + $category->setName('Computer Peripherals'); + + $product = new Product(); + $product->setName('Keyboard'); + $product->setPrice(19.99); + $product->setDescription('Ergonomic and stylish!'); + + // relates this product to the category + $product->setCategory($category); + + $entityManager->persist($category); + $entityManager->persist($product); + $entityManager->flush(); + + return new Response( + 'Saved new product with id: '.$product->getId() + .' and new category with id: '.$category->getId() + ); + } + } + +When you go to ``/product``, a single row is added to both the ``category`` and +``product`` tables. The ``product.category_id`` column for the new product is set +to whatever the ``id`` is of the new category. Doctrine manages the persistence of this +relationship for you: + +.. raw:: html + + + +If you're new to an ORM, this is the *hardest* concept: you need to stop thinking +about your database, and instead *only* think about your objects. Instead of setting +the category's integer id onto ``Product``, you set the entire ``Category`` *object*. +Doctrine takes care of the rest when saving. + +.. sidebar:: Updating the Relationship from the Inverse Side + + Could you also call ``$category->addProduct()`` to change the relationship? Yes, + but, only because the ``make:entity`` command helped us. For more details, + see: `associations-inverse-side`_. + +Fetching Related Objects +------------------------ + +When you need to fetch associated objects, your workflow looks like it did +before. First, fetch a ``$product`` object and then access its related +``Category`` object:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use App\Entity\Product; + // ... + + class ProductController extends AbstractController + { + public function show(ProductRepository $productRepository, int $id): Response + { + $product = $productRepository->find($id); + // ... + + $categoryName = $product->getCategory()->getName(); + + // ... + } + } + +In this example, you first query for a ``Product`` object based on the product's +``id``. This issues a query to fetch *only* the product data and hydrates the +``$product``. Later, when you call ``$product->getCategory()->getName()``, +Doctrine silently makes a second query to find the ``Category`` that's related +to this ``Product``. It prepares the ``$category`` object and returns it to +you. + +.. raw:: html + + + +What's important is the fact that you have access to the product's related +category, but the category data isn't actually retrieved until you ask for +the category (i.e. it's "lazily loaded"). + +Because we mapped the optional ``OneToMany`` side, you can also query in the other +direction:: + + // src/Controller/ProductController.php + + // ... + class ProductController extends AbstractController + { + public function showProducts(CategoryRepository $categoryRepository, int $id): Response + { + $category = $categoryRepository->find($id); + + $products = $category->getProducts(); + + // ... + } + } + +In this case, the same things occur: you first query for a single ``Category`` +object. Then, only when (and if) you access the products, Doctrine makes a second +query to retrieve the related ``Product`` objects. This extra query can be avoided +by adding JOINs. + +.. sidebar:: Relationships and Proxy Classes + + This "lazy loading" is possible because, when necessary, Doctrine returns + a "proxy" object in place of the true object. Look again at the above + example:: + + $product = $productRepository->find($id); + + $category = $product->getCategory(); + + // prints "Proxies\AppEntityCategoryProxy" + dump(get_class($category)); + die(); + + This proxy object extends the true ``Category`` object, and looks and + acts exactly like it. The difference is that, by using a proxy object, + Doctrine can delay querying for the real ``Category`` data until you + actually need that data (e.g. until you call ``$category->getName()``). + + The proxy classes are generated by Doctrine and stored in the cache directory. + You'll probably never even notice that your ``$category`` object is actually + a proxy object. + + In the next section, when you retrieve the product and category data + all at once (via a *join*), Doctrine will return the *true* ``Category`` + object, since nothing needs to be lazily loaded. + +.. _doctrine-associations-join-query: + +Joining Related Records +----------------------- + +In the examples above, two queries were made - one for the original object +(e.g. a ``Category``) and one for the related object(s) (e.g. the ``Product`` +objects). + +.. tip:: + + Remember that you can see all of the queries made during a request via + the web debug toolbar. + +If you know up front that you'll need to access both objects, you +can avoid the second query by issuing a join in the original query. Add the +following method to the ``ProductRepository`` class:: + + // src/Repository/ProductRepository.php + + // ... + class ProductRepository extends ServiceEntityRepository + { + public function findOneByIdJoinedToCategory(int $productId): ?Product + { + $entityManager = $this->getEntityManager(); + + $query = $entityManager->createQuery( + 'SELECT p, c + FROM App\Entity\Product p + INNER JOIN p.category c + WHERE p.id = :id' + )->setParameter('id', $productId); + + return $query->getOneOrNullResult(); + } + } + +This will *still* return an array of ``Product`` objects. But now, when you call +``$product->getCategory()`` and use that data, no second query is made. + +Now, you can use this method in your controller to query for a ``Product`` +object and its related ``Category`` in one query:: + + // src/Controller/ProductController.php + + // ... + class ProductController extends AbstractController + { + public function show(ProductRepository $productRepository, int $id): Response + { + $product = $productRepository->findOneByIdJoinedToCategory($id); + + $category = $product->getCategory(); + + // ... + } + } + +.. _associations-inverse-side: + +Setting Information from the Inverse Side +----------------------------------------- + +So far, you've updated the relationship by calling ``$product->setCategory($category)``. +This is no accident! Each relationship has two sides: in this example, ``Product.category`` +is the *owning* side and ``Category.products`` is the *inverse* side. + +To update a relationship in the database, you *must* set the relationship on the +*owning* side. The owning side is always where the ``ManyToOne`` mapping is set +(for a ``ManyToMany`` relation, you can choose which side is the owning side). + +Does this mean it's not possible to call ``$category->addProduct()`` or +``$category->removeProduct()`` to update the database? Actually, it *is* possible, +thanks to some clever code that the ``make:entity`` command generated:: + + // src/Entity/Category.php + + // ... + class Category + { + // ... + + public function addProduct(Product $product): self + { + if (!$this->products->contains($product)) { + $this->products[] = $product; + $product->setCategory($this); + } + + return $this; + } + } + +The *key* is ``$product->setCategory($this)``, which sets the *owning* side. Thanks, +to this, when you save, the relationship *will* update in the database. + +What about *removing* a ``Product`` from a ``Category``? The ``make:entity`` command +also generated a ``removeProduct()`` method:: + + // src/Entity/Category.php + namespace App\Entity; + + // ... + class Category + { + // ... + + public function removeProduct(Product $product): self + { + if ($this->products->contains($product)) { + $this->products->removeElement($product); + // set the owning side to null (unless already changed) + if ($product->getCategory() === $this) { + $product->setCategory(null); + } + } + + return $this; + } + } + +Thanks to this, if you call ``$category->removeProduct($product)``, the ``category_id`` +on that ``Product`` will be set to ``null`` in the database. + +.. warning:: + + Please be aware that the inverse side could be associated with a large amount of records. + I.e. there could be a large amount of products with the same category. + In this case ``$this->products->contains($product)`` could lead to unwanted database + requests and very high memory consumption with the risk of hard to debug "Out of memory" errors. + + So make sure if you need an inverse side and check if the generated code could lead to such issues. + +But, instead of setting the ``category_id`` to null, what if you want the ``Product`` +to be *deleted* if it becomes "orphaned" (i.e. without a ``Category``)? To choose +that behavior, use the `orphanRemoval`_ option inside ``Category``: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Category.php + + // ... + + #[ORM\OneToMany(targetEntity: Product::class, mappedBy: 'category', orphanRemoval: true)] + private array $products; + +Thanks to this, if the ``Product`` is removed from the ``Category``, it will be +removed from the database entirely. + +More Information on Associations +-------------------------------- + +This section has been an introduction to one common type of entity relationship, +the one-to-many relationship. For more advanced details and examples of how +to use other types of relations (e.g. one-to-one, many-to-many), see +Doctrine's `Association Mapping Documentation`_. + +.. note:: + + If you're using attributes, you'll need to prepend all attributes with + ``#[ORM\]`` (e.g. ``#[ORM\OneToMany]``), which is not reflected in Doctrine's + documentation. + +.. _`Association Mapping Documentation`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/association-mapping.html +.. _`orphanRemoval`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-associations.html#orphan-removal +.. _`Mastering Doctrine Relations`: https://symfonycasts.com/screencast/doctrine-relations +.. _`ArrayCollection`: https://www.doctrine-project.org/projects/doctrine-collections/en/1.6/index.html +.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html diff --git a/doctrine/custom_dql_functions.rst b/doctrine/custom_dql_functions.rst new file mode 100644 index 00000000000..e5b21819f58 --- /dev/null +++ b/doctrine/custom_dql_functions.rst @@ -0,0 +1,141 @@ +How to Register custom DQL Functions +==================================== + +Doctrine allows you to specify custom DQL functions. For more information +on this topic, read Doctrine's cookbook article `DQL User Defined Functions`_. + +In Symfony, you can register your custom DQL functions as follows: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/doctrine.yaml + doctrine: + orm: + # ... + dql: + string_functions: + test_string: App\DQL\StringFunction + second_string: App\DQL\SecondStringFunction + numeric_functions: + test_numeric: App\DQL\NumericFunction + datetime_functions: + test_datetime: App\DQL\DatetimeFunction + + .. code-block:: xml + + + + + + + + + App\DQL\StringFunction + App\DQL\SecondStringFunction + App\DQL\NumericFunction + App\DQL\DatetimeFunction + + + + + + .. code-block:: php + + // config/packages/doctrine.php + use App\DQL\DatetimeFunction; + use App\DQL\NumericFunction; + use App\DQL\SecondStringFunction; + use App\DQL\StringFunction; + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine): void { + $defaultDql = $doctrine->orm() + ->entityManager('default') + // ... + ->dql(); + + $defaultDql->stringFunction('test_string', StringFunction::class); + $defaultDql->stringFunction('second_string', SecondStringFunction::class); + $defaultDql->numericFunction('test_numeric', NumericFunction::class); + $defaultDql->datetimeFunction('test_datetime', DatetimeFunction::class); + }; + +.. note:: + + In case the ``entity_managers`` were named explicitly, configuring the functions with the + ORM directly will trigger the exception ``Unrecognized option "dql" under "doctrine.orm"``. + The ``dql`` configuration block must be defined under the named entity manager. + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/doctrine.yaml + doctrine: + orm: + # ... + entity_managers: + example_manager: + # Place your functions here + dql: + datetime_functions: + test_datetime: App\DQL\DatetimeFunction + + .. code-block:: xml + + + + + + + + + + + + + + App\DQL\DatetimeFunction + + + + + + + + .. code-block:: php + + // config/packages/doctrine.php + use App\DQL\DatetimeFunction; + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine): void { + $doctrine->orm() + // ... + ->entityManager('example_manager') + // place your functions here + ->dql() + ->datetimeFunction('test_datetime', DatetimeFunction::class); + }; + +.. warning:: + + DQL functions are instantiated by Doctrine outside of the Symfony + :doc:`service container ` so you can't inject services + or parameters into a custom DQL function. + +.. _`DQL User Defined Functions`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/cookbook/dql-user-defined-functions.html diff --git a/doctrine/dbal.rst b/doctrine/dbal.rst new file mode 100644 index 00000000000..4f47b61eb61 --- /dev/null +++ b/doctrine/dbal.rst @@ -0,0 +1,165 @@ +How to Use Doctrine DBAL +======================== + +.. note:: + + This article is about the Doctrine DBAL. Typically, you'll work with + the higher level Doctrine ORM layer, which uses the DBAL behind + the scenes to actually communicate with the database. To read more about + the Doctrine ORM, see ":doc:`/doctrine`". + +The `Doctrine`_ Database Abstraction Layer (DBAL) is an abstraction layer that +sits on top of `PDO`_ and offers an intuitive and flexible API for communicating +with the most popular relational databases. The DBAL library allows you to write +queries independently of your ORM models, e.g. for building reports or direct +data manipulations. + +.. tip:: + + Read the official Doctrine `DBAL Documentation`_ to learn all the details + and capabilities of Doctrine's DBAL library. + +First, install the Doctrine ``orm`` :ref:`Symfony pack `: + +.. code-block:: terminal + + $ composer require symfony/orm-pack + +Then configure the ``DATABASE_URL`` environment variable in ``.env``: + +.. code-block:: text + + # .env (or override DATABASE_URL in .env.local to avoid committing your changes) + + # customize this line! + DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=8.0.37" + +Further things can be configured in ``config/packages/doctrine.yaml`` - see +:ref:`reference-dbal-configuration`. Remove the ``orm`` key in that file +if you *don't* want to use the Doctrine ORM. + +You can then access the Doctrine DBAL connection by autowiring the ``Connection`` +object:: + + // src/Controller/UserController.php + namespace App\Controller; + + use Doctrine\DBAL\Connection; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + + class UserController extends AbstractController + { + public function index(Connection $connection): Response + { + $users = $connection->fetchAllAssociative('SELECT * FROM users'); + + // ... + } + } + +This will pass you the ``database_connection`` service. + +Registering custom Mapping Types +-------------------------------- + +You can register custom mapping types through Symfony's configuration. They +will be added to all configured connections. For more information on custom +mapping types, read Doctrine's `Custom Mapping Types`_ section of their documentation. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/doctrine.yaml + doctrine: + dbal: + types: + custom_first: App\Type\CustomFirst + custom_second: App\Type\CustomSecond + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/doctrine.php + use App\Type\CustomFirst; + use App\Type\CustomSecond; + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine): void { + $dbal = $doctrine->dbal(); + $dbal->type('custom_first')->class(CustomFirst::class); + $dbal->type('custom_second')->class(CustomSecond::class); + }; + +Registering custom Mapping Types in the SchemaTool +-------------------------------------------------- + +The SchemaTool is used to inspect the database to compare the schema. To +achieve this task, it needs to know which mapping type needs to be used +for each database type. Registering new ones can be done through the configuration. + +Now, map the ENUM type (not supported by DBAL by default) to the ``string`` +mapping type: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/doctrine.yaml + doctrine: + dbal: + mapping_types: + enum: string + + .. code-block:: xml + + + + + + + string + + + + + .. code-block:: php + + // config/packages/doctrine.php + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine): void { + $dbalDefault = $doctrine->dbal() + ->connection('default'); + $dbalDefault->mappingType('enum', 'string'); + }; + +.. _`PDO`: https://www.php.net/pdo +.. _`Doctrine`: https://www.doctrine-project.org/ +.. _`DBAL Documentation`: https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/index.html +.. _`Custom Mapping Types`: https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#custom-mapping-types diff --git a/doctrine/events.rst b/doctrine/events.rst new file mode 100644 index 00000000000..929f44b915e --- /dev/null +++ b/doctrine/events.rst @@ -0,0 +1,407 @@ +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 performing tasks such +as *"update the createdAt property automatically right 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 public methods on the entity classes. + They can't use services, so they are intended for **very simple logic** related + to a single entity; +* **Entity listeners**, they are defined as classes with callback methods for the + events you want to respond to. They can use services, but they are only called + for the entities of a certain class, so they are ideal for **complex event logic + related to a single entity**; +* **Lifecycle listeners**, they are similar to entity listeners but their event + methods are called for all entities, not only those of a certain type. They are + ideal to **share event logic between entities**. + +The performance of each type of listener depends on how many entities it applies to: +lifecycle callbacks are faster than entity listeners, which in turn are faster +than lifecycle listeners. + +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 for Doctrine ORM. If you are + using ODM for MongoDB, read the `DoctrineMongoDBBundle documentation`_. + +Doctrine Lifecycle Callbacks +---------------------------- + +Lifecycle callbacks are defined as public 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-attributes + + // src/Entity/Product.php + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + + // When using attributes, 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(): void + { + $this->createdAt = new \DateTimeImmutable(); + } + } + + .. 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 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. + +First, define a PHP class that handles the ``postUpdate`` Doctrine event:: + + // src/EventListener/UserChangedNotifier.php + namespace App\EventListener; + + use App\Entity\User; + use Doctrine\ORM\Event\PostUpdateEventArgs; + + class UserChangedNotifier + { + // the entity listener methods receive two arguments: + // the entity instance and the lifecycle event + public function postUpdate(User $user, PostUpdateEventArgs $event): void + { + // ... do something to notify the changes + } + } + +Then, add the ``#[AsEntityListener]`` attribute to the class to enable it as +a Doctrine entity listener in your application:: + + // src/EventListener/UserChangedNotifier.php + namespace App\EventListener; + + // ... + use App\Entity\User; + use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; + use Doctrine\ORM\Events; + + #[AsEntityListener(event: Events::postUpdate, method: 'postUpdate', entity: User::class)] + class UserChangedNotifier + { + // ... + } + +Alternatively, if you prefer to not use PHP attributes, you must +configure a service for the entity listener and :doc:`tag it ` +with the ``doctrine.orm.entity_listener`` tag as follows: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\EventListener\UserChangedNotifier: + tags: + - + # these are the options required to define the entity listener + name: 'doctrine.orm.entity_listener' + event: 'postUpdate' + entity: 'App\Entity\User' + + # these are other options that you may define if needed + + # set the 'lazy' option to TRUE to only instantiate listeners when they are used + # lazy: true + + # set the 'entity_manager' option if the listener is not associated to the default 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 + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Entity\User; + use App\EventListener\UserChangedNotifier; + + return static function (ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set(UserChangedNotifier::class) + ->tag('doctrine.orm.entity_listener', [ + // These are the options required to define the entity listener: + 'event' => 'postUpdate', + 'entity' => User::class, + + // These are other options that you may define if needed: + + // set the 'lazy' option to TRUE to only instantiate listeners when they are used + // 'lazy' => true, + + // set the 'entity_manager' option if the listener is not associated to the default 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-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\ORM\Event\PostPersistEventArgs; + + 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(PostPersistEventArgs $args): void + { + $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 + } + } + +.. note:: + + In previous Doctrine versions, instead of ``PostPersistEventArgs``, you had + to use ``LifecycleEventArgs``, which was deprecated in Doctrine ORM 2.14. + +Then, add the ``#[AsDoctrineListener]`` attribute to the class to enable it as +a Doctrine listener in your application:: + + // src/EventListener/SearchIndexer.php + namespace App\EventListener; + + use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; + use Doctrine\ORM\Events; + + #[AsDoctrineListener(event: Events::postPersist, priority: 500, connection: 'default')] + class SearchIndexer + { + // ... + } + +Alternatively, if you prefer to not use PHP attributes, you must enable the +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:: php-attributes + + // src/EventListener/SearchIndexer.php + namespace App\EventListener; + + use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; + use Doctrine\ORM\Event\PostPersistEventArgs; + + #[AsDoctrineListener('postPersist'/*, 500, 'default'*/)] + class SearchIndexer + { + public function postPersist(PostPersistEventArgs $event): void + { + // ... + } + } + + .. code-block:: yaml + + # config/services.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 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 + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\EventListener\SearchIndexer; + + return static function (ContainerConfigurator $container): void { + $services = $container->services(); + + // listeners are applied by default to all Doctrine connections + $services->set(SearchIndexer::class) + ->tag('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', + ]) + ; + }; + +.. versionadded:: 2.8.0 + + The `AsDoctrineListener`_ attribute was introduced in DoctrineBundle 2.8.0. + +.. tip:: + + The value of the ``connection`` option can also be a + :ref:`configuration parameter `. + +.. _`Doctrine`: https://www.doctrine-project.org/ +.. _`lifecycle events`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/events.html#lifecycle-events +.. _`official docs about Doctrine events`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/events.html +.. _`DoctrineMongoDBBundle documentation`: https://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html +.. _`AsDoctrineListener`: https://github.com/doctrine/DoctrineBundle/blob/2.12.x/src/Attribute/AsDoctrineListener.php diff --git a/doctrine/multiple_entity_managers.rst b/doctrine/multiple_entity_managers.rst new file mode 100644 index 00000000000..1a56c55ddad --- /dev/null +++ b/doctrine/multiple_entity_managers.rst @@ -0,0 +1,276 @@ +How to Work with Multiple Entity Managers and Connections +========================================================= + +You can use multiple Doctrine entity managers or connections in a Symfony +application. This is necessary if you are using different databases or even +vendors with entirely different sets of entities. In other words, one entity +manager that connects to one database will handle some entities while another +entity manager that connects to another database might handle the rest. +It is also possible to use multiple entity managers to manage a common set of +entities, each with their own database connection strings or separate cache configuration. + +.. note:: + + Using multiple entity managers is not complicated to configure, but more + advanced and not usually required. Be sure you actually need multiple + entity managers before adding in this layer of complexity. + +.. warning:: + + Entities cannot define associations across different entity managers. If you + need that, there are `several alternatives`_ that require some custom setup. + +The following configuration code shows how you can configure two entity managers: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/doctrine.yaml + doctrine: + dbal: + connections: + default: + url: '%env(resolve:DATABASE_URL)%' + customer: + url: '%env(resolve:CUSTOMER_DATABASE_URL)%' + default_connection: default + orm: + default_entity_manager: default + entity_managers: + default: + connection: default + mappings: + Main: + is_bundle: false + dir: '%kernel.project_dir%/src/Entity/Main' + prefix: 'App\Entity\Main' + alias: Main + customer: + connection: customer + mappings: + Customer: + is_bundle: false + dir: '%kernel.project_dir%/src/Entity/Customer' + prefix: 'App\Entity\Customer' + alias: Customer + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/doctrine.php + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine): void { + // Connections: + $doctrine->dbal() + ->connection('default') + ->url(env('DATABASE_URL')->resolve()); + $doctrine->dbal() + ->connection('customer') + ->url(env('CUSTOMER_DATABASE_URL')->resolve()); + $doctrine->dbal()->defaultConnection('default'); + + // Entity Managers: + $doctrine->orm()->defaultEntityManager('default'); + $defaultEntityManager = $doctrine->orm()->entityManager('default'); + $defaultEntityManager->connection('default'); + $defaultEntityManager->mapping('Main') + ->isBundle(false) + ->dir('%kernel.project_dir%/src/Entity/Main') + ->prefix('App\Entity\Main') + ->alias('Main'); + $customerEntityManager = $doctrine->orm()->entityManager('customer'); + $customerEntityManager->connection('customer'); + $customerEntityManager->mapping('Customer') + ->isBundle(false) + ->dir('%kernel.project_dir%/src/Entity/Customer') + ->prefix('App\Entity\Customer') + ->alias('Customer') + ; + }; + +In this case, you've defined two entity managers and called them ``default`` +and ``customer``. The ``default`` entity manager manages entities in the +``src/Entity/Main`` directory, while the ``customer`` entity manager manages +entities in ``src/Entity/Customer``. You've also defined two connections, one +for each entity manager, but you are free to define the same connection for both. + +.. warning:: + + When working with multiple connections and entity managers, you should be + explicit about which configuration you want. If you *do* omit the name of + the connection or entity manager, the default (i.e. ``default``) is used. + + If you use a different name than ``default`` for the default entity manager, + you will need to redefine the default entity manager in the ``prod`` environment + configuration and in the Doctrine migrations configuration (if you use that): + + .. code-block:: yaml + + # config/packages/prod/doctrine.yaml + doctrine: + orm: + default_entity_manager: 'your default entity manager name' + + # ... + + .. code-block:: yaml + + # config/packages/doctrine_migrations.yaml + doctrine_migrations: + # ... + em: 'your default entity manager name' + +When working with multiple connections to create your databases: + +.. code-block:: terminal + + # Play only with "default" connection + $ php bin/console doctrine:database:create + + # Play only with "customer" connection + $ php bin/console doctrine:database:create --connection=customer + +When working with multiple entity managers to generate migrations: + +.. code-block:: terminal + + # Play only with "default" mappings + $ php bin/console doctrine:migrations:diff + $ php bin/console doctrine:migrations:migrate + + # Play only with "customer" mappings + $ php bin/console doctrine:migrations:diff --em=customer + $ php bin/console doctrine:migrations:migrate --em=customer + +If you *do* omit the entity manager's name when asking for it, +the default entity manager (i.e. ``default``) is returned:: + + // src/Controller/UserController.php + namespace App\Controller; + + // ... + use Doctrine\ORM\EntityManagerInterface; + use Doctrine\Persistence\ManagerRegistry; + + class UserController extends AbstractController + { + public function index(ManagerRegistry $doctrine): Response + { + // Both methods return the default entity manager + $entityManager = $doctrine->getManager(); + $entityManager = $doctrine->getManager('default'); + + // This method returns instead the "customer" entity manager + $customerEntityManager = $doctrine->getManager('customer'); + + // ... + } + } + +Entity managers also benefit from :ref:`autowiring aliases ` +when the :doc:`framework bundle ` is used. For +example, to inject the ``customer`` entity manager, type-hint your method with +``EntityManagerInterface $customerEntityManager``. + +You can now use Doctrine like you did before - using the ``default`` entity +manager to persist and fetch entities that it manages and the ``customer`` +entity manager to persist and fetch its entities. + +The same applies to repository calls:: + + // src/Controller/UserController.php + namespace App\Controller; + + use AcmeStoreBundle\Entity\Customer; + use AcmeStoreBundle\Entity\Product; + use Doctrine\Persistence\ManagerRegistry; + // ... + + class UserController extends AbstractController + { + public function index(ManagerRegistry $doctrine): Response + { + // Retrieves a repository managed by the "default" entity manager + $products = $doctrine->getRepository(Product::class)->findAll(); + + // Explicit way to deal with the "default" entity manager + $products = $doctrine->getRepository(Product::class, 'default')->findAll(); + + // Retrieves a repository managed by the "customer" entity manager + $customers = $doctrine->getRepository(Customer::class, 'customer')->findAll(); + + // ... + } + } + +.. warning:: + + One entity can be managed by more than one entity manager. This however + results in unexpected behavior when extending from ``ServiceEntityRepository`` + in your custom repository. The ``ServiceEntityRepository`` always + uses the configured entity manager for that entity. + + In order to fix this situation, extend ``EntityRepository`` instead and + no longer rely on autowiring:: + + // src/Repository/CustomerRepository.php + namespace App\Repository; + + use Doctrine\ORM\EntityRepository; + + class CustomerRepository extends EntityRepository + { + // ... + } + + You should now always fetch this repository using ``ManagerRegistry::getRepository()``. + +.. _`several alternatives`: https://stackoverflow.com/a/11494543 diff --git a/cookbook/doctrine/resolve_target_entity.rst b/doctrine/resolve_target_entity.rst similarity index 56% rename from cookbook/doctrine/resolve_target_entity.rst rename to doctrine/resolve_target_entity.rst index 9fbe7b7176c..5ae6475a957 100644 --- a/cookbook/doctrine/resolve_target_entity.rst +++ b/doctrine/resolve_target_entity.rst @@ -1,15 +1,7 @@ -.. index:: - single: Doctrine; Resolving target entities - single: Doctrine; Define relationships with abstract classes and interfaces - How to Define Relationships with Abstract Classes and Interfaces ================================================================ -.. versionadded:: 2.1 - The ResolveTargetEntityListener is new to Doctrine 2.2, which was first - packaged with Symfony 2.1. - -One of the goals of bundles is to create discreet bundles of functionality +One of the goals of bundles is to create discrete bundles of functionality that do not have many (if any) dependencies, allowing you to use that functionality in other applications without including unnecessary items. @@ -25,8 +17,8 @@ without making them hard dependencies. Background ---------- -Suppose you have an `InvoiceBundle` which provides invoicing functionality -and a `CustomerBundle` that contains customer management tools. You want +Suppose you have an InvoiceBundle which provides invoicing functionality +and a CustomerBundle that contains customer management tools. You want to keep these separated, because they can be used in other systems without each other, but for your application you want to use them together. @@ -38,58 +30,49 @@ with a real object that implements that interface. Set up ------ -Let's use the following basic entities (which are incomplete for brevity) -to explain how to set up and use the RTEL. +This article uses the following two basic entities (which are incomplete for +brevity) to explain how to set up and use the ``ResolveTargetEntityListener``. A Customer entity:: - // src/Acme/AppBundle/Entity/Customer.php - - namespace Acme\AppBundle\Entity; + // src/Entity/Customer.php + namespace App\Entity; + use App\Entity\CustomerInterface as BaseCustomer; + use App\Model\InvoiceSubjectInterface; use Doctrine\ORM\Mapping as ORM; - use Acme\CustomerBundle\Entity\Customer as BaseCustomer; - use Acme\InvoiceBundle\Model\InvoiceSubjectInterface; - /** - * @ORM\Entity - * @ORM\Table(name="customer") - */ + #[ORM\Entity] + #[ORM\Table(name: 'customer')] class Customer extends BaseCustomer implements InvoiceSubjectInterface { - // In our example, any methods defined in the InvoiceSubjectInterface + // In this example, any methods defined in the InvoiceSubjectInterface // are already implemented in the BaseCustomer } An Invoice entity:: - // src/Acme/InvoiceBundle/Entity/Invoice.php + // src/Entity/Invoice.php + namespace App\Entity; - namespace Acme\InvoiceBundle\Entity; - - use Doctrine\ORM\Mapping AS ORM; - use Acme\InvoiceBundle\Model\InvoiceSubjectInterface; + use App\Model\InvoiceSubjectInterface; + use Doctrine\ORM\Mapping as ORM; /** * Represents an Invoice. - * - * @ORM\Entity - * @ORM\Table(name="invoice") */ + #[ORM\Entity] + #[ORM\Table(name: 'invoice')] class Invoice { - /** - * @ORM\ManyToOne(targetEntity="Acme\InvoiceBundle\Model\InvoiceSubjectInterface") - * @var InvoiceSubjectInterface - */ - protected $subject; + #[ORM\ManyToOne(targetEntity: InvoiceSubjectInterface::class)] + protected InvoiceSubjectInterface $subject; } An InvoiceSubjectInterface:: - // src/Acme/InvoiceBundle/Model/InvoiceSubjectInterface.php - - namespace Acme\InvoiceBundle\Model; + // src/Model/InvoiceSubjectInterface.php + namespace App\Model; /** * An interface that the invoice Subject object should implement. @@ -103,10 +86,7 @@ An InvoiceSubjectInterface:: // will need to access on the subject so that you can // be sure that you have access to those methods. - /** - * @return string - */ - public function getName(); + public function getName(): string; } Next, you need to configure the listener, which tells the DoctrineBundle @@ -116,42 +96,46 @@ about the replacement: .. code-block:: yaml - # app/config/config.yml + # config/packages/doctrine.yaml doctrine: - # .... + # ... orm: - # .... + # ... resolve_target_entities: - Acme\InvoiceBundle\Model\InvoiceSubjectInterface: Acme\AppBundle\Entity\Customer + App\Model\InvoiceSubjectInterface: App\Entity\Customer .. code-block:: xml - + + + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/doctrine + https://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"> - Acme\AppBundle\Entity\Customer + App\Entity\Customer .. code-block:: php - // app/config/config.php - $container->loadFromExtension('doctrine', array( - 'orm' => array( - // ... - 'resolve_target_entities' => array( - 'Acme\InvoiceBundle\Model\InvoiceSubjectInterface' => 'Acme\AppBundle\Entity\Customer', - ), - ), - )); + // config/packages/doctrine.php + use App\Entity\Customer; + use App\Model\InvoiceSubjectInterface; + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine): void { + $orm = $doctrine->orm(); + // ... + $orm->resolveTargetEntity(InvoiceSubjectInterface::class, Customer::class); + }; Final Thoughts -------------- diff --git a/emoji.rst b/emoji.rst new file mode 100644 index 00000000000..551497f0c76 --- /dev/null +++ b/emoji.rst @@ -0,0 +1,173 @@ +Working with Emojis +=================== + +.. versionadded:: 7.1 + + The emoji component was introduced in Symfony 7.1. + +Symfony provides several utilities to work with emoji characters and sequences +from the `Unicode CLDR dataset`_. They are available via the Emoji component, +which you must first install in your application: + +.. _installation: + +.. code-block:: terminal + + $ composer require symfony/emoji + +.. include:: /components/require_autoload.rst.inc + +The data needed to store the transliteration of all emojis (~5,000) into all +languages take a considerable disk space. + +If you need to save disk space (e.g. because you deploy to some service with tight +size constraints), run this command (e.g. as an automated script after ``composer install``) +to compress the internal Symfony emoji data files using the PHP ``zlib`` extension: + +.. code-block:: terminal + + # adjust the path to the 'compress' binary based on your application installation + $ php ./vendor/symfony/emoji/Resources/bin/compress + +.. _emoji-transliteration: + +Emoji Transliteration +--------------------- + +The ``EmojiTransliterator`` class offers a way to translate emojis into their +textual representation in all languages based on the `Unicode CLDR dataset`_:: + + use Symfony\Component\Emoji\EmojiTransliterator; + + // Describe emojis in English + $transliterator = EmojiTransliterator::create('en'); + $transliterator->transliterate('Menus with 🍕 or 🍝'); + // => 'Menus with pizza or spaghetti' + + // Describe emojis in Ukrainian + $transliterator = EmojiTransliterator::create('uk'); + $transliterator->transliterate('Menus with 🍕 or 🍝'); + // => 'Menus with піца or спагеті' + +.. tip:: + + When using the :ref:`slugger ` from the String component, + you can combine it with the ``EmojiTransliterator`` to :ref:`slugify emojis `. + +Transliterating Emoji Text Short Codes +-------------------------------------- + +Services like GitHub and Slack allows to include emojis in your messages using +text short codes (e.g. you can add the ``:+1:`` code to render the 👍 emoji). + +Symfony also provides a feature to transliterate emojis into short codes and vice +versa. The short codes are slightly different on each service, so you must pass +the name of the service as an argument when creating the transliterator. + +GitHub Emoji Short Codes Transliteration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Convert emojis to GitHub short codes with the ``emoji-github`` locale:: + + $transliterator = EmojiTransliterator::create('emoji-github'); + $transliterator->transliterate('Teenage 🐢 really love 🍕'); + // => 'Teenage :turtle: really love :pizza:' + +Convert GitHub short codes to emojis with the ``github-emoji`` locale:: + + $transliterator = EmojiTransliterator::create('github-emoji'); + $transliterator->transliterate('Teenage :turtle: really love :pizza:'); + // => 'Teenage 🐢 really love 🍕' + +Gitlab Emoji Short Codes Transliteration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Convert emojis to Gitlab short codes with the ``emoji-gitlab`` locale:: + + $transliterator = EmojiTransliterator::create('emoji-gitlab'); + $transliterator->transliterate('Breakfast with 🥝 or 🥛'); + // => 'Breakfast with :kiwi: or :milk:' + +Convert Gitlab short codes to emojis with the ``gitlab-emoji`` locale:: + + $transliterator = EmojiTransliterator::create('gitlab-emoji'); + $transliterator->transliterate('Breakfast with :kiwi: or :milk:'); + // => 'Breakfast with 🥝 or 🥛' + +Slack Emoji Short Codes Transliteration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Convert emojis to Slack short codes with the ``emoji-slack`` locale:: + + $transliterator = EmojiTransliterator::create('emoji-slack'); + $transliterator->transliterate('Menus with 🥗 or 🧆'); + // => 'Menus with :green_salad: or :falafel:' + +Convert Slack short codes to emojis with the ``slack-emoji`` locale:: + + $transliterator = EmojiTransliterator::create('slack-emoji'); + $transliterator->transliterate('Menus with :green_salad: or :falafel:'); + // => 'Menus with 🥗 or 🧆' + +.. _text-emoji: + +Universal Emoji Short Codes Transliteration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you don't know which service was used to generate the short codes, you can use +the ``text-emoji`` locale, which combines all codes from all services:: + + $transliterator = EmojiTransliterator::create('text-emoji'); + + // Github short codes + $transliterator->transliterate('Breakfast with :kiwi-fruit: or :milk-glass:'); + // Gitlab short codes + $transliterator->transliterate('Breakfast with :kiwi: or :milk:'); + // Slack short codes + $transliterator->transliterate('Breakfast with :kiwifruit: or :glass-of-milk:'); + + // all the above examples produce the same result: + // => 'Breakfast with 🥝 or 🥛' + +You can convert emojis to short codes with the ``emoji-text`` locale:: + + $transliterator = EmojiTransliterator::create('emoji-text'); + $transliterator->transliterate('Breakfast with 🥝 or 🥛'); + // => 'Breakfast with :kiwifruit: or :milk-glass: + +Inverse Emoji Transliteration +----------------------------- + +Given the textual representation of an emoji, you can reverse it back to get the +actual emoji thanks to the :ref:`emojify filter `: + +.. code-block:: twig + + {{ 'I like :kiwi-fruit:'|emojify }} {# renders: I like 🥝 #} + {{ 'I like :kiwi:'|emojify }} {# renders: I like 🥝 #} + {{ 'I like :kiwifruit:'|emojify }} {# renders: I like 🥝 #} + +By default, ``emojify`` uses the :ref:`text catalog `, which +merges the emoji text codes of all services. If you prefer, you can select a +specific catalog to use: + +.. code-block:: twig + + {{ 'I :green-heart: this'|emojify }} {# renders: I 💚 this #} + {{ ':green_salad: is nice'|emojify('slack') }} {# renders: 🥗 is nice #} + {{ 'My :turtle: has no name yet'|emojify('github') }} {# renders: My 🐢 has no name yet #} + {{ ':kiwi: is a great fruit'|emojify('gitlab') }} {# renders: 🥝 is a great fruit #} + +Removing Emojis +--------------- + +The ``EmojiTransliterator`` can also be used to remove all emojis from a string, +via the special ``strip`` locale:: + + use Symfony\Component\Emoji\EmojiTransliterator; + + $transliterator = EmojiTransliterator::create('strip'); + $transliterator->transliterate('🎉Hey!🥳 🎁Happy Birthday!🎁'); + // => 'Hey! Happy Birthday!' + +.. _`Unicode CLDR dataset`: https://github.com/unicode-org/cldr diff --git a/event_dispatcher.rst b/event_dispatcher.rst new file mode 100644 index 00000000000..d9b913ed49f --- /dev/null +++ b/event_dispatcher.rst @@ -0,0 +1,811 @@ +Events and Event Listeners +========================== + +During the execution of a Symfony application, lots of event notifications are +triggered. Your application can listen to these notifications and respond to +them by executing any piece of code. + +Symfony triggers several :doc:`events related to the kernel ` +while processing the HTTP Request. Third-party bundles may also dispatch events, and +you can even dispatch :doc:`custom events ` from your +own code. + +All the examples shown in this article use the same ``KernelEvents::EXCEPTION`` +event for consistency purposes. In your own application, you can use any event +and even mix several of them in the same subscriber. + +Creating an Event Listener +-------------------------- + +The most common way to listen to an event is to register an **event listener**:: + + // src/EventListener/ExceptionListener.php + namespace App\EventListener; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Event\ExceptionEvent; + use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; + + class ExceptionListener + { + public function __invoke(ExceptionEvent $event): void + { + // You get the exception object from the received event + $exception = $event->getThrowable(); + $message = sprintf( + 'My Error says: %s with code: %s', + $exception->getMessage(), + $exception->getCode() + ); + + // Customize your response object to display the exception details + $response = new Response(); + $response->setContent($message); + // the exception message can contain unfiltered user input; + // set the content-type to text to avoid XSS issues + $response->headers->set('Content-Type', 'text/plain; charset=utf-8'); + + // HttpExceptionInterface is a special type of exception that + // holds status code and header details + if ($exception instanceof HttpExceptionInterface) { + $response->setStatusCode($exception->getStatusCode()); + $response->headers->replace($exception->getHeaders()); + } else { + $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR); + } + + // sends the modified response object to the event + $event->setResponse($response); + } + } + +Now that the class is created, you need to register it as a service and +notify Symfony that it is an event listener by using a special "tag": + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + App\EventListener\ExceptionListener: + tags: [kernel.event_listener] + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\EventListener\ExceptionListener; + + return function(ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set(ExceptionListener::class) + ->tag('kernel.event_listener') + ; + }; + +Symfony follows this logic to decide which method to call inside the event +listener class: + +#. If the ``kernel.event_listener`` tag defines the ``method`` attribute, that's + the name of the method to be called; +#. If no ``method`` attribute is defined, try to call the ``__invoke()`` magic + method (which makes event listeners invokable); +#. If the ``__invoke()`` method is not defined either, throw an exception. + +.. note:: + + There is an optional attribute for the ``kernel.event_listener`` tag called + ``priority``, which is a positive or negative integer that defaults to ``0`` + and it controls the order in which listeners are executed (the higher the + number, the earlier a listener is executed). This is useful when you need to + guarantee that one listener is executed before another. The priorities of the + internal Symfony listeners usually range from ``-256`` to ``256`` but your + own listeners can use any positive or negative integer. + +.. note:: + + There is an optional attribute for the ``kernel.event_listener`` tag called + ``event`` which is useful when listener ``$event`` argument is not typed. + If you configure it, it will change type of ``$event`` object. + For 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. + + With this attribute, Symfony follows this logic to decide which method to call + inside the event listener class: + + #. If the ``kernel.event_listener`` tag defines the ``method`` attribute, that's + the name of the method to be called; + #. If no ``method`` attribute is defined, try to call the method whose name + is ``on`` + "PascalCased event name" (e.g. ``onKernelException()`` method for + the ``kernel.exception`` event); + #. If that method is not defined either, try to call the ``__invoke()`` magic + method (which makes event listeners invokable); + #. If the ``__invoke()`` method is not defined either, throw an exception. + +.. _event-dispatcher_event-listener-attributes: + +Defining Event Listeners with PHP Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An alternative way to define an event listener is to use the +:class:`Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener` +PHP attribute. This allows to configure the listener inside its class, without +having to add any configuration in external files:: + + namespace App\EventListener; + + use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + + #[AsEventListener] + final class MyListener + { + public function __invoke(CustomEvent $event): void + { + // ... + } + } + +You can add multiple ``#[AsEventListener]`` attributes to configure different methods. +The ``method`` property is optional, and when not defined, it defaults to +``on`` + uppercased event name. In the example below, the ``'foo'`` event listener +doesn't explicitly define its method, so the ``onFoo()`` method will be called:: + + namespace App\EventListener; + + use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + + #[AsEventListener(event: CustomEvent::class, method: 'onCustomEvent')] + #[AsEventListener(event: 'foo', priority: 42)] + #[AsEventListener(event: 'bar', method: 'onBarEvent')] + final class MyMultiListener + { + public function onCustomEvent(CustomEvent $event): void + { + // ... + } + + public function onFoo(): void + { + // ... + } + + public function onBarEvent(): void + { + // ... + } + } + +:class:`Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener` +can also be applied to methods directly:: + + namespace App\EventListener; + + use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + + final class MyMultiListener + { + #[AsEventListener] + public function onCustomEvent(CustomEvent $event): void + { + // ... + } + + #[AsEventListener(event: 'foo', priority: 42)] + public function onFoo(): void + { + // ... + } + + #[AsEventListener(event: 'bar')] + public function onBarEvent(): void + { + // ... + } + } + +.. note:: + + Note that the attribute doesn't require its ``event`` parameter to be set + if the method already type-hints the expected event. + +.. _events-subscriber: + +Creating an Event Subscriber +---------------------------- + +Another way to listen to events is via an **event subscriber**, which is a class +that defines one or more methods that listen to one or various events. The main +difference with the event listeners is that subscribers always know the events +to which they are listening. + +If different event subscriber methods listen to the same event, their order is +defined by the ``priority`` parameter. This value is a positive or negative +integer which defaults to ``0``. The higher the number, the earlier the method +is called. **Priority is aggregated for all listeners and subscribers**, so your +methods could be called before or after the methods defined in other listeners +and subscribers. To learn more about event subscribers, read :doc:`/components/event_dispatcher`. + +The following example shows an event subscriber that defines several methods which +listen to the same :ref:`kernel.exception event ` +via its ``ExceptionEvent`` class:: + + // src/EventSubscriber/ExceptionSubscriber.php + namespace App\EventSubscriber; + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpKernel\Event\ExceptionEvent; + + class ExceptionSubscriber implements EventSubscriberInterface + { + public static function getSubscribedEvents(): array + { + // return the subscribed events, their methods and priorities + return [ + ExceptionEvent::class => [ + ['processException', 10], + ['logException', 0], + ['notifyException', -10], + ], + ]; + } + + public function processException(ExceptionEvent $event): void + { + // ... + } + + public function logException(ExceptionEvent $event): void + { + // ... + } + + public function notifyException(ExceptionEvent $event): void + { + // ... + } + } + +That's it! Your ``services.yaml`` file should already be setup to load services from +the ``EventSubscriber`` directory. Symfony takes care of the rest. + +.. _ref-event-subscriber-configuration: + +.. tip:: + + If your methods are *not* called when an exception is thrown, double-check that + you're :ref:`loading services ` from + the ``EventSubscriber`` directory and have :ref:`autoconfigure ` + enabled. You can also manually add the ``kernel.event_subscriber`` tag. + +Request Events, Checking Types +------------------------------ + +A single page can make several requests (one main request, and then multiple +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 "main" request or a "sub request":: + + // src/EventListener/RequestListener.php + namespace App\EventListener; + + use Symfony\Component\HttpKernel\Event\RequestEvent; + + class RequestListener + { + public function onKernelRequest(RequestEvent $event): void + { + if (!$event->isMainRequest()) { + // don't do anything if it's not the main request + return; + } + + // ... + } + } + +Certain things, like checking information on the *real* request, may not need to +be done on the sub-request listeners. + +.. _events-or-subscribers: + +Listeners or Subscribers +------------------------ + +Listeners and subscribers can be used in the same application indistinctly. The +decision to use either of them is usually a matter of personal taste. However, +there are some minor advantages for each of them: + +* **Subscribers are easier to reuse** because the knowledge of the events is kept + in the class rather than in the service definition. This is the reason why + Symfony uses subscribers internally; +* **Listeners are more flexible** because bundles can enable or disable each of + them conditionally depending on some configuration value. + +Event Aliases +------------- + +When configuring event listeners and subscribers via dependency injection, +Symfony's core events can also be referred to by the fully qualified class +name (FQCN) of the corresponding event class:: + + // src/EventSubscriber/RequestSubscriber.php + namespace App\EventSubscriber; + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpKernel\Event\RequestEvent; + + class RequestSubscriber implements EventSubscriberInterface + { + public static function getSubscribedEvents(): array + { + return [ + RequestEvent::class => 'onKernelRequest', + ]; + } + + public function onKernelRequest(RequestEvent $event): void + { + // ... + } + } + +Internally, the event FQCN are treated as aliases for the original event names. +Since the mapping already happens when compiling the service container, event +listeners and subscribers using FQCN instead of event names will appear under +the original event name when inspecting the event dispatcher. + +This alias mapping can be extended for custom events by registering the +compiler pass ``AddEventAliasesPass``:: + + // src/Kernel.php + namespace App; + + use App\Event\MyCustomEvent; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass; + use Symfony\Component\HttpKernel\Kernel as BaseKernel; + + class Kernel extends BaseKernel + { + protected function build(ContainerBuilder $container): void + { + $container->addCompilerPass(new AddEventAliasesPass([ + MyCustomEvent::class => 'my_custom_event', + ])); + } + } + +The compiler pass will always extend the existing list of aliases. Because of +that, it is safe to register multiple instances of the pass with different +configurations. + +Debugging Event Listeners +------------------------- + +You can find out what listeners are registered in the event dispatcher +using the console. To show all events and their listeners, run: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher + +You can get registered listeners for a particular event by specifying +its name: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher kernel.exception + +or can get everything which partial matches the event name: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher kernel // matches "kernel.exception", "kernel.response" etc. + $ php bin/console debug:event-dispatcher Security // matches "Symfony\Component\Security\Http\Event\CheckPassportEvent" + +The :doc:`security ` system uses an event dispatcher per +firewall. Use the ``--dispatcher`` option to get the registered listeners +for a particular event dispatcher: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher --dispatcher=security.event_dispatcher.main + +.. _event-dispatcher-before-after-filters: + +How to Set Up Before and After Filters +-------------------------------------- + +It is quite common in web application development to need some logic to be +performed right before or directly after your controller actions acting as +filters or hooks. + +Some web frameworks define methods like ``preExecute()`` and ``postExecute()``, +but there is no such thing in Symfony. The good news is that there is a much +better way to interfere with the Request -> Response process using the +:doc:`EventDispatcher component `. + +Token Validation Example +~~~~~~~~~~~~~~~~~~~~~~~~ + +Imagine that you need to develop an API where some controllers are public +but some others are restricted to one or some clients. For these private features, +you might provide a token to your clients to identify themselves. + +So, before executing your controller action, you need to check if the action +is restricted or not. If it is restricted, you need to validate the provided +token. + +.. note:: + + Please note that for simplicity in this recipe, tokens will be defined + in config and neither database setup nor authentication via the Security + component will be used. + +Before Filters with the ``kernel.controller`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First, define some token configuration as parameters: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + tokens: + client1: pass1 + client2: pass2 + + .. code-block:: xml + + + + + + + + pass1 + pass2 + + + + + .. code-block:: php + + // config/services.php + $container->setParameter('tokens', [ + 'client1' => 'pass1', + 'client2' => 'pass2', + ]); + +Tag Controllers to Be Checked +............................. + +A ``kernel.controller`` (aka ``KernelEvents::CONTROLLER``) listener gets notified +on *every* request, right before the controller is executed. So, first, you need +some way to identify if the controller that matches the request needs token validation. + +A clean and easy way is to create an empty interface and make the controllers +implement it:: + + namespace App\Controller; + + interface TokenAuthenticatedController + { + // ... + } + +A controller that implements this interface looks like this:: + + namespace App\Controller; + + use App\Controller\TokenAuthenticatedController; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + + class FooController extends AbstractController implements TokenAuthenticatedController + { + // An action that needs authentication + public function bar(): Response + { + // ... + } + } + +Creating an Event Subscriber +............................ + +Next, you'll need to create an event subscriber, which will hold the logic +that you want to be executed before your controllers. If you're not familiar with +event subscribers, you can learn more about :ref:`how to use them `:: + + // src/EventSubscriber/TokenSubscriber.php + namespace App\EventSubscriber; + + use App\Controller\TokenAuthenticatedController; + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpKernel\Event\ControllerEvent; + use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; + use Symfony\Component\HttpKernel\KernelEvents; + + class TokenSubscriber implements EventSubscriberInterface + { + public function __construct( + private array $tokens + ) { + } + + public function onKernelController(ControllerEvent $event): void + { + $controller = $event->getController(); + + // when a controller class defines multiple action methods, the controller + // is returned as [$controllerInstance, 'methodName'] + if (is_array($controller)) { + $controller = $controller[0]; + } + + 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!'); + } + } + } + + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::CONTROLLER => 'onKernelController', + ]; + } + } + +That's it! Your ``services.yaml`` file should already be setup to load services from +the ``EventSubscriber`` directory. Symfony takes care of the rest. Your +``TokenSubscriber`` ``onKernelController()`` method will be executed on each request. +If the controller that is about to be executed implements ``TokenAuthenticatedController``, +token authentication is applied. This lets you have a "before" filter on any controller +you want. + +.. tip:: + + If your subscriber is *not* called on each request, double-check that + you're :ref:`loading services ` from + the ``EventSubscriber`` directory and have :ref:`autoconfigure ` + enabled. You can also manually add the ``kernel.event_subscriber`` tag. + +After Filters with the ``kernel.response`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to having a "hook" that's executed *before* your controller, you +can also add a hook that's executed *after* your controller. For this example, +imagine that you want to add a ``sha1`` hash (with a salt using that token) to +all responses that have passed this token authentication. + +Another core Symfony event - called ``kernel.response`` (aka ``KernelEvents::RESPONSE``) - +is notified on every request, but after the controller returns a Response object. +To create an "after" listener, create a listener class and register +it as a service on this event. + +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(ControllerEvent $event): void + { + // ... + + 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!'); + } + + // mark the request as having passed token authentication + $event->getRequest()->attributes->set('auth_token', $token); + } + } + +Now, configure the subscriber to listen to another event and add ``onKernelResponse()``. +This will look for the ``auth_token`` flag on the request object and set a custom +header on the response if it's found:: + + // add the new use statement at the top of your file + use Symfony\Component\HttpKernel\Event\ResponseEvent; + + public function onKernelResponse(ResponseEvent $event): void + { + // check to see if onKernelController marked this as a token "auth'ed" request + if (!$token = $event->getRequest()->attributes->get('auth_token')) { + return; + } + + $response = $event->getResponse(); + + // create a hash and set it as a response header + $hash = sha1($response->getContent().$token); + $response->headers->set('X-CONTENT-HASH', $hash); + } + + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::CONTROLLER => 'onKernelController', + KernelEvents::RESPONSE => 'onKernelResponse', + ]; + } + +That's it! The ``TokenSubscriber`` is now notified before every controller is +executed (``onKernelController()``) and after every controller returns a response +(``onKernelResponse()``). By making specific controllers implement the ``TokenAuthenticatedController`` +interface, your listener knows which controllers it should take action on. +And by storing a value in the request's "attributes" bag, the ``onKernelResponse()`` +method knows to add the extra header. Have fun! + +.. _event-dispatcher-method-behavior: + +How to Customize a Method Behavior without Using Inheritance +------------------------------------------------------------ + +If you want to do something right before, or directly after a method is +called, you can dispatch an event respectively at the beginning or at the +end of the method:: + + class CustomMailer + { + // ... + + public function send(string $subject, string $message): mixed + { + // dispatch an event before the method + $event = new BeforeSendMailEvent($subject, $message); + $this->dispatcher->dispatch($event, 'mailer.pre_send'); + + // get $subject and $message from the event, they may have been modified + $subject = $event->getSubject(); + $message = $event->getMessage(); + + // the real method implementation is here + $returnValue = ...; + + // do something after the method + $event = new AfterSendMailEvent($returnValue); + $this->dispatcher->dispatch($event, 'mailer.post_send'); + + return $event->getReturnValue(); + } + } + +In this example, two events are dispatched: + +#. ``mailer.pre_send``, before the method is called, +#. and ``mailer.post_send`` after the method is called. + +Each uses a custom Event class to communicate information to the listeners +of the two events. For example, ``BeforeSendMailEvent`` might look like +this:: + + // src/Event/BeforeSendMailEvent.php + namespace App\Event; + + use Symfony\Contracts\EventDispatcher\Event; + + class BeforeSendMailEvent extends Event + { + public function __construct( + private string $subject, + private string $message, + ) { + } + + public function getSubject(): string + { + return $this->subject; + } + + public function setSubject(string $subject): string + { + $this->subject = $subject; + } + + public function getMessage(): string + { + return $this->message; + } + + public function setMessage(string $message): void + { + $this->message = $message; + } + } + +And the ``AfterSendMailEvent`` even like this:: + + // src/Event/AfterSendMailEvent.php + namespace App\Event; + + use Symfony\Contracts\EventDispatcher\Event; + + class AfterSendMailEvent extends Event + { + public function __construct( + private mixed $returnValue, + ) { + } + + public function getReturnValue(): mixed + { + return $this->returnValue; + } + + public function setReturnValue(mixed $returnValue): void + { + $this->returnValue = $returnValue; + } + } + +Both events allow you to get some information (e.g. ``getMessage()``) and even change +that information (e.g. ``setMessage()``). + +Now, you can create an event subscriber to hook into this event. For example, you +could listen to the ``mailer.post_send`` event and change the method's return value:: + + // src/EventSubscriber/MailPostSendSubscriber.php + namespace App\EventSubscriber; + + use App\Event\AfterSendMailEvent; + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + + class MailPostSendSubscriber implements EventSubscriberInterface + { + public function onMailerPostSend(AfterSendMailEvent $event): void + { + $returnValue = $event->getReturnValue(); + // modify the original $returnValue value + + $event->setReturnValue($returnValue); + } + + public static function getSubscribedEvents(): array + { + return [ + 'mailer.post_send' => 'onMailerPostSend', + ]; + } + } + +That's it! Your subscriber should be called automatically (or read more about +:ref:`event subscriber configuration `). + +Learn More +---------- + +- :ref:`The Request-Response Lifecycle ` +- :doc:`/reference/events` +- :ref:`Security-related Events ` +- :doc:`/components/event_dispatcher` diff --git a/form/bootstrap4.rst b/form/bootstrap4.rst new file mode 100644 index 00000000000..eef016aa58a --- /dev/null +++ b/form/bootstrap4.rst @@ -0,0 +1,144 @@ +Bootstrap 4 Form Theme +====================== + +Symfony provides several ways of integrating Bootstrap into your application. The +most straightforward way is to add the required ```` and `` + +The major benefit of submitting the whole form to just extract the updated +``position`` field is that no additional server-side code is needed; all the +code from above to generate the submitted form can be reused. diff --git a/form/embedded.rst b/form/embedded.rst new file mode 100644 index 00000000000..9e20164c3a4 --- /dev/null +++ b/form/embedded.rst @@ -0,0 +1,132 @@ +How to Embed Forms +================== + +Often, you'll want to build a form that will include fields from many different +objects. For example, a registration form may contain data belonging to +a ``User`` object as well as many ``Address`` objects. Fortunately this can +be achieved by the Form component. + +.. _forms-embedding-single-object: + +Embedding a Single Object +------------------------- + +Suppose that each ``Task`` belongs to a ``Category`` object. Start by +creating the ``Category`` class:: + + // src/Entity/Category.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Category + { + #[Assert\NotBlank] + public string $name; + } + +Next, add a new ``category`` property to the ``Task`` class:: + + // ... + + class Task + { + // ... + + #[Assert\Type(type: Category::class)] + #[Assert\Valid] + protected ?Category $category = null; + + // ... + + public function getCategory(): ?Category + { + return $this->category; + } + + public function setCategory(?Category $category): void + { + $this->category = $category; + } + } + +.. tip:: + + The ``Valid`` Constraint has been added to the property ``category``. This + cascades the validation to the corresponding entity. If you omit this constraint, + the child entity would not be validated. + +Now that your application has been updated to reflect the new requirements, +create a form class so that a ``Category`` object can be modified by the user:: + + // src/Form/CategoryType.php + namespace App\Form; + + use App\Entity\Category; + use Symfony\Component\Form\AbstractType; + use Symfony\Component\Form\FormBuilderInterface; + use Symfony\Component\OptionsResolver\OptionsResolver; + + class CategoryType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('name'); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Category::class, + ]); + } + } + +The end goal is to allow the ``Category`` of a ``Task`` to be modified right +inside the task form itself. To accomplish this, add a ``category`` field +to the ``TaskType`` object whose type is an instance of the new ``CategoryType`` +class:: + + // src/Form/TaskType.php + use App\Form\CategoryType; + use Symfony\Component\Form\FormBuilderInterface; + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + // ... + + $builder->add('category', CategoryType::class); + } + +The fields from ``CategoryType`` can now be rendered alongside those from +the ``TaskType`` class. + +Render the ``Category`` fields in the same way as the original ``Task`` fields: + +.. code-block:: html+twig + + {# ... #} + +

    Category

    +
    + {{ form_row(form.category.name) }} +
    + + {# ... #} + +When the user submits the form, the submitted data for the ``Category`` fields +are used to construct an instance of ``Category``, which is then set on the +``category`` field of the ``Task`` instance. + +The ``Category`` instance is accessible naturally via ``$task->getCategory()`` +and can be persisted to the database or used however you need. + +Embedding a Collection of Forms +------------------------------- + +You can also embed a collection of forms into one form (imagine a ``Category`` +form with many ``Product`` sub-forms). This is done by using the ``collection`` +field type. + +For more information see the :doc:`/form/form_collections` article and the +:doc:`CollectionType ` reference. diff --git a/form/events.rst b/form/events.rst new file mode 100644 index 00000000000..dad6c242ddd --- /dev/null +++ b/form/events.rst @@ -0,0 +1,417 @@ +Form Events +=========== + +The Form component provides a structured process to let you customize your +forms, by making use of the +:doc:`EventDispatcher component `. +Using form events, you may modify information or fields at different steps +of the workflow: from the population of the form to the submission of the +data from the request. + +For example, if you need to add a field depending on request values, you can +register an event listener to the ``FormEvents::PRE_SUBMIT`` event as follows:: + + // ... + + use Symfony\Component\Form\FormEvent; + use Symfony\Component\Form\FormEvents; + + $listener = function (FormEvent $event): void { + // ... + }; + + $form = $formFactory->createBuilder() + // ... add form fields + ->addEventListener(FormEvents::PRE_SUBMIT, $listener); + + // ... + +The Form Workflow +----------------- + +In the lifecycle of a form, there are two moments where the form data can +be updated: + +1. During **pre-population** (``setData()``) when building the form; +2. When handling **form submission** (``handleRequest()``) to update the + form data based on the values the user entered. + +.. raw:: html + + + +1) Pre-populating the Form (``FormEvents::PRE_SET_DATA`` and ``FormEvents::POST_SET_DATA``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. raw:: html + + + +Two events are dispatched during pre-population of a form, when +:method:`Form::setData() ` +is called: ``FormEvents::PRE_SET_DATA`` and ``FormEvents::POST_SET_DATA``. + +A) The ``FormEvents::PRE_SET_DATA`` Event +......................................... + +The ``FormEvents::PRE_SET_DATA`` event is dispatched at the beginning of the +``Form::setData()`` method. It is used to modify the data given during +pre-population with +:method:`FormEvent::setData() `. +The method :method:`Form::setData() ` +is locked since the event is dispatched from it and will throw an exception +if called from a listener. + +==================== ====================================== +Data Type Value +==================== ====================================== +Event data Model data injected into ``setData()`` +Form model data ``null`` +Form normalized data ``null`` +Form view data ``null`` +==================== ====================================== + +.. seealso:: + + See all form events at a glance in the + :ref:`Form Events Information Table `. + + instead. + +.. sidebar:: ``FormEvents::PRE_SET_DATA`` in the Form component + + The ``Symfony\Component\Form\Extension\Core\Type\CollectionType`` form type relies + on the ``Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener`` + subscriber, listening to the ``FormEvents::PRE_SET_DATA`` event in order + to reorder the form's fields depending on the data from the pre-populated + object, by removing and adding all form rows. + +B) The ``FormEvents::POST_SET_DATA`` Event +.......................................... + +The ``FormEvents::POST_SET_DATA`` event is dispatched at the end of the +:method:`Form::setData() ` +method. This event can be used to modify a form depending on the populated data +(adding or removing fields dynamically). + +==================== ==================================================== +Data Type Value +==================== ==================================================== +Event data Model data injected into ``setData()`` +Form model data Model data injected into ``setData()`` +Form normalized data Model data transformed using a model transformer +Form view data Normalized data transformed using a view transformer +==================== ==================================================== + +.. seealso:: + + See all form events at a glance in the + :ref:`Form Events Information Table `. + +.. sidebar:: ``FormEvents::POST_SET_DATA`` in the Form component + + The ``Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorListener`` + class is subscribed to listen to the ``FormEvents::POST_SET_DATA`` event + in order to collect information about the forms from the denormalized + model and view data. + +2) Submitting a Form (``FormEvents::PRE_SUBMIT``, ``FormEvents::SUBMIT`` and ``FormEvents::POST_SUBMIT``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. raw:: html + + + +Three events are dispatched when +:method:`Form::handleRequest() ` +or :method:`Form::submit() ` are +called: ``FormEvents::PRE_SUBMIT``, ``FormEvents::SUBMIT``, +``FormEvents::POST_SUBMIT``. + +A) The ``FormEvents::PRE_SUBMIT`` Event +....................................... + +The ``FormEvents::PRE_SUBMIT`` event is dispatched at the beginning of the +:method:`Form::submit() ` method. + +It can be used to: + +* Change data from the request, before submitting the data to the form; +* Add or remove form fields, before submitting the data to the form. + +==================== ======================================== +Data Type Value +==================== ======================================== +Event data Data from the request +Form model data Same as in ``FormEvents::POST_SET_DATA`` +Form normalized data Same as in ``FormEvents::POST_SET_DATA`` +Form view data Same as in ``FormEvents::POST_SET_DATA`` +==================== ======================================== + +.. seealso:: + + See all form events at a glance in the + :ref:`Form Events Information Table `. + +.. sidebar:: ``FormEvents::PRE_SUBMIT`` in the Form component + + The ``Symfony\Component\Form\Extension\Core\EventListener\TrimListener`` + subscriber subscribes to the ``FormEvents::PRE_SUBMIT`` event in order to + trim the request's data (for string values). + The ``Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener`` + subscriber subscribes to the ``FormEvents::PRE_SUBMIT`` event in order to + validate the CSRF token. + +B) The ``FormEvents::SUBMIT`` Event +................................... + +The ``FormEvents::SUBMIT`` event is dispatched right before the +:method:`Form::submit() ` method +transforms back the normalized data to the model and view data. + +It can be used to change data from the normalized representation of the data. + +==================== =================================================================================== +Data Type Value +==================== =================================================================================== +Event data Data from the request reverse-transformed from the request using a view transformer +Form model data Same as in ``FormEvents::POST_SET_DATA`` +Form normalized data Same as in ``FormEvents::POST_SET_DATA`` +Form view data Same as in ``FormEvents::POST_SET_DATA`` +==================== =================================================================================== + +.. seealso:: + + See all form events at a glance in the + :ref:`Form Events Information Table `. + +.. warning:: + + At this point, you cannot add or remove fields to the form. + +.. sidebar:: ``FormEvents::SUBMIT`` in the Form component + + The ``Symfony\Component\Form\Extension\Core\EventListener\FixUrlProtocolListener`` + subscribes to the ``FormEvents::SUBMIT`` event in order to prepend a default + protocol to URL fields that were submitted without a protocol. + +C) The ``FormEvents::POST_SUBMIT`` Event +........................................ + +The ``FormEvents::POST_SUBMIT`` event is dispatched after the +:method:`Form::submit() ` once the +model and view data have been denormalized. + +It can be used to fetch data after denormalization. + +==================== =================================================================================== +Data Type Value +==================== =================================================================================== +Event data Normalized data transformed using a view transformer +Form model data Normalized data reverse-transformed using a model transformer +Form normalized data Data from the request reverse-transformed from the request using a view transformer +Form view data Normalized data transformed using a view transformer +==================== =================================================================================== + +.. seealso:: + + See all form events at a glance in the + :ref:`Form Events Information Table `. + +.. warning:: + + At this point, you cannot add or remove fields to the current form and its + children. + +.. sidebar:: ``FormEvents::POST_SUBMIT`` in the Form component + + The ``Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorListener`` + subscribes to the ``FormEvents::POST_SUBMIT`` event in order to collect + information about the forms. + The ``Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener`` + subscribes to the ``FormEvents::POST_SUBMIT`` event in order to + automatically validate the denormalized object. + +Registering Event Listeners or Event Subscribers +------------------------------------------------ + +In order to be able to use Form events, you need to create an event listener +or an event subscriber and register it to an event. + +The name of each of the "form" events is defined as a constant on the +:class:`Symfony\\Component\\Form\\FormEvents` class. +Additionally, each event callback (listener or subscriber method) is passed a +single argument, which is an instance of +:class:`Symfony\\Component\\Form\\FormEvent`. The event object contains a +reference to the current state of the form and the current data being +processed. + +.. _component-form-event-table: + +====================== ============================= =============== +Name ``FormEvents`` Constant Event's Data +====================== ============================= =============== +``form.pre_set_data`` ``FormEvents::PRE_SET_DATA`` Model data +``form.post_set_data`` ``FormEvents::POST_SET_DATA`` Model data +``form.pre_submit`` ``FormEvents::PRE_SUBMIT`` Request data +``form.submit`` ``FormEvents::SUBMIT`` Normalized data +``form.post_submit`` ``FormEvents::POST_SUBMIT`` View data +====================== ============================= =============== + +Event Listeners +~~~~~~~~~~~~~~~ + +An event listener may be any type of valid callable. For example, you can +define an event listener function inline right in the ``addEventListener`` +method of the ``FormFactory``:: + + // ... + + use Symfony\Component\Form\Event\PreSubmitEvent; + use Symfony\Component\Form\Extension\Core\Type\CheckboxType; + use Symfony\Component\Form\Extension\Core\Type\EmailType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\Form\FormEvents; + + $form = $formFactory->createBuilder() + ->add('username', TextType::class) + ->add('showEmail', CheckboxType::class) + ->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event): void { + $user = $event->getData(); + $form = $event->getForm(); + + if (!$user) { + return; + } + + // checks whether the user has chosen to display their email or not. + // If the data was submitted previously, the additional value that is + // included in the request variables needs to be removed. + if (isset($user['showEmail']) && $user['showEmail']) { + $form->add('email', EmailType::class); + } else { + unset($user['email']); + $event->setData($user); + } + }) + ->getForm(); + + // ... + +When you have created a form type class, you can use one of its methods as a +callback for better readability:: + + // src/Form/SubscriptionType.php + namespace App\Form; + + use Symfony\Component\Form\Event\PreSetDataEvent; + use Symfony\Component\Form\Extension\Core\Type\CheckboxType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\Form\FormEvents; + + // ... + class SubscriptionType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('username', TextType::class) + ->add('showEmail', CheckboxType::class) + ->addEventListener( + FormEvents::PRE_SET_DATA, + [$this, 'onPreSetData'] + ) + ; + } + + public function onPreSetData(PreSetDataEvent $event): void + { + // ... + } + } + +Event Subscribers +~~~~~~~~~~~~~~~~~ + +Event subscribers have different uses: + +* Improving readability; +* Listening to multiple events; +* Regrouping multiple listeners inside a single class. + +Consider the following example of a form event subscriber:: + + // src/Form/EventListener/AddEmailFieldListener.php + namespace App\Form\EventListener; + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\Form\Event\PreSetDataEvent; + use Symfony\Component\Form\Event\PreSubmitEvent; + use Symfony\Component\Form\Extension\Core\Type\EmailType; + use Symfony\Component\Form\FormEvents; + + class AddEmailFieldListener implements EventSubscriberInterface + { + public static function getSubscribedEvents(): array + { + return [ + FormEvents::PRE_SET_DATA => 'onPreSetData', + FormEvents::PRE_SUBMIT => 'onPreSubmit', + ]; + } + + public function onPreSetData(PreSetDataEvent $event): void + { + $user = $event->getData(); + $form = $event->getForm(); + + // checks whether the user from the initial data has chosen to + // display their email or not. + if (true === $user->isShowEmail()) { + $form->add('email', EmailType::class); + } + } + + public function onPreSubmit(PreSubmitEvent $event): void + { + $user = $event->getData(); + $form = $event->getForm(); + + if (!$user) { + return; + } + + // checks whether the user has chosen to display their email or not. + // If the data was submitted previously, the additional value that + // is included in the request variables needs to be removed. + if (isset($user['showEmail']) && $user['showEmail']) { + $form->add('email', EmailType::class); + } else { + unset($user['email']); + $event->setData($user); + } + } + } + +To register the event subscriber, use the ``addEventSubscriber()`` method:: + + use App\Form\EventListener\AddEmailFieldListener; + use Symfony\Component\Form\Extension\Core\Type\CheckboxType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + + // ... + + $form = $formFactory->createBuilder() + ->add('username', TextType::class) + ->add('showEmail', CheckboxType::class) + ->addEventSubscriber(new AddEmailFieldListener()) + ->getForm(); + + // ... diff --git a/form/form_collections.rst b/form/form_collections.rst new file mode 100644 index 00000000000..2a0ba99657f --- /dev/null +++ b/form/form_collections.rst @@ -0,0 +1,711 @@ +How to Embed a Collection of Forms +================================== + +Symfony Forms can embed a collection of many other forms, which is useful to +edit related entities in a single form. In this article, you'll create a form to +edit a ``Task`` class and, right inside the same form, you'll be able to edit, +create and remove many ``Tag`` objects related to that Task. + +Let's start by creating a ``Task`` entity:: + + // src/Entity/Task.php + namespace App\Entity; + + use Doctrine\Common\Collections\Collection; + + class Task + { + protected string $description; + protected Collection $tags; + + public function __construct() + { + $this->tags = new ArrayCollection(); + } + + public function getDescription(): string + { + return $this->description; + } + + public function setDescription(string $description): void + { + $this->description = $description; + } + + public function getTags(): Collection + { + return $this->tags; + } + } + +.. note:: + + The `ArrayCollection`_ is specific to Doctrine and is similar to a PHP array + but provides many utility methods. + +Now, create a ``Tag`` class. As you saw above, a ``Task`` can have many ``Tag`` +objects:: + + // src/Entity/Tag.php + namespace App\Entity; + + class Tag + { + private string $name; + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + } + +Then, create a form class so that a ``Tag`` object can be modified by the user:: + + // src/Form/TagType.php + namespace App\Form; + + use App\Entity\Tag; + use Symfony\Component\Form\AbstractType; + use Symfony\Component\Form\FormBuilderInterface; + use Symfony\Component\OptionsResolver\OptionsResolver; + + class TagType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('name'); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Tag::class, + ]); + } + } + +Next, let's create a form for the ``Task`` entity, using a +:doc:`CollectionType ` field of ``TagType`` +forms. This will allow us to modify all the ``Tag`` elements of a ``Task`` right +inside the task form itself:: + + // src/Form/TaskType.php + namespace App\Form; + + use App\Entity\Task; + use Symfony\Component\Form\AbstractType; + use Symfony\Component\Form\Extension\Core\Type\CollectionType; + use Symfony\Component\Form\FormBuilderInterface; + use Symfony\Component\OptionsResolver\OptionsResolver; + + class TaskType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('description'); + + $builder->add('tags', CollectionType::class, [ + 'entry_type' => TagType::class, + 'entry_options' => ['label' => false], + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Task::class, + ]); + } + } + +In your controller, you'll create a new form from the ``TaskType``:: + + // src/Controller/TaskController.php + namespace App\Controller; + + use App\Entity\Tag; + use App\Entity\Task; + use App\Form\TaskType; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + class TaskController extends AbstractController + { + public function new(Request $request): Response + { + $task = new Task(); + + // dummy code - add some example tags to the task + // (otherwise, the template will render an empty list of tags) + $tag1 = new Tag(); + $tag1->setName('tag1'); + $task->getTags()->add($tag1); + $tag2 = new Tag(); + $tag2->setName('tag2'); + $task->getTags()->add($tag2); + // end dummy code + + $form = $this->createForm(TaskType::class, $task); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // ... do your form processing, like saving the Task and Tag entities + } + + return $this->render('task/new.html.twig', [ + 'form' => $form, + ]); + } + } + +In the template, you can now iterate over the existing ``TagType`` forms +to render them: + +.. code-block:: html+twig + + {# templates/task/new.html.twig #} + + {# ... #} + + {{ form_start(form) }} + {{ form_row(form.description) }} + +

    Tags

    +
      + {% for tag in form.tags %} +
    • {{ form_row(tag.name) }}
    • + {% endfor %} +
    + {{ form_end(form) }} + + {# ... #} + +When the user submits the form, the submitted data for the ``tags`` field is +used to construct an ``ArrayCollection`` of ``Tag`` objects. The collection is +then set on the ``tag`` field of the ``Task`` and can be accessed via ``$task->getTags()``. + +So far, this works great, but only to edit *existing* tags. It doesn't allow us +yet to add new tags or delete existing ones. + +.. warning:: + + You can embed nested collections as many levels down as you like. However, + if you use Xdebug, you may receive a ``Maximum function nesting level of '100' + reached, aborting!`` error. To fix this, increase the ``xdebug.max_nesting_level`` + PHP setting, or render each form field by hand using ``form_row()`` instead of + rendering the whole form at once (e.g ``form_widget(form)``). + +.. _form-collections-new-prototype: + +Allowing "new" Tags with the "Prototype" +---------------------------------------- + +Previously you added two tags to your task in the controller. Now let the users +add as many tag forms as they need directly in the browser. This requires a bit +of JavaScript code. + +.. tip:: + + Instead of writing the needed JavaScript code yourself, you can use Symfony + UX to implement this feature with only PHP and Twig code. See the + `Symfony UX Demo of Form Collections`_. + +But first, you need to let the form collection know that instead of exactly two, +it will receive an *unknown* number of tags. Otherwise, you'll see a +*"This form should not contain extra fields"* error. This is done with the +``allow_add`` option:: + + // src/Form/TaskType.php + + // ... + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + // ... + + $builder->add('tags', CollectionType::class, [ + 'entry_type' => TagType::class, + 'entry_options' => ['label' => false], + 'allow_add' => true, + ]); + } + +The ``allow_add`` option also makes a ``prototype`` variable available to you. +This "prototype" is a little "template" that contains all the HTML needed to +dynamically create any new "tag" forms with JavaScript. + +Let's start with plain JavaScript (Vanilla JS) – if you're using Stimulus, see below. + +To render the prototype, add +the following ``data-prototype`` attribute to the existing ``
      `` in your +template: + +.. code-block:: html+twig + + {# the data-index attribute is required for the JavaScript code below #} +
        + +On the rendered page, the result will look something like this: + +.. code-block:: html + +
          + +Now add a button to dynamically add a new tag: + +.. code-block:: html+twig + + + +.. seealso:: + + If you want to customize the HTML code in the prototype, see + :ref:`form-custom-prototype`. + +.. tip:: + + The ``form.tags.vars.prototype`` is a form element that looks and feels just + like the individual ``form_widget(tag.*)`` elements inside your ``for`` loop. + This means that you can call ``form_widget()``, ``form_row()`` or ``form_label()`` + on it. You could even choose to render only one of its fields (e.g. the + ``name`` field): + + .. code-block:: twig + + {{ form_widget(form.tags.vars.prototype.name)|e }} + +.. note:: + + If you render your whole "tags" sub-form at once (e.g. ``form_row(form.tags)``), + the ``data-prototype`` attribute is automatically added to the containing ``div``, + and you need to adjust the following JavaScript accordingly. + +Now add some JavaScript to read this attribute and dynamically add new tag forms +when the user clicks the "Add a tag" link. Add a `` + +Import maps are a native browser feature. When you import ``bootstrap`` from +JavaScript, the browser will look at the ``importmap`` and see that it should +fetch the package from the associated path. + +.. _automatic-import-mapping: + +But where did the ``/assets/duck.js`` import entry come from? That doesn't live +in ``importmap.php``. Great question! + +The ``assets/app.js`` file above imports ``./duck.js``. When you import a file using a +relative path, your browser looks for that file relative to the one importing +it. So, it would look for ``/assets/duck.js``. That URL *would* be correct, +except that the ``duck.js`` file is versioned. Fortunately, the AssetMapper component +sees the import and adds a mapping from ``/assets/duck.js`` to the correct, versioned +filename. The result: importing ``./duck.js`` just works! + +The ``importmap()`` function also outputs an `ES module shim`_ so that +`older browsers `_ understand importmaps +(see the :ref:`polyfill config `). + +.. _app-entrypoint: + +The "app" Entrypoint & Preloading +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An "entrypoint" is the main JavaScript file that the browser loads, +and your app starts with one by default:: + + // importmap.php + return [ + 'app' => [ + 'path' => './assets/app.js', + 'entrypoint' => true, + ], + // ... + ]; + +.. _importmap-app-entry: + +In addition to the importmap, the ``{{ importmap('app') }}`` in +``base.html.twig`` outputs a few other things, including: + +.. code-block:: html + + + +This line tells the browser to load the ``app`` importmap entry, which causes the +code in ``assets/app.js`` to be executed. + +The ``importmap()`` function also outputs a set of "preloads": + +.. code-block:: html + + + + +This is a performance optimization and you can learn more about below +in :ref:`Performance: Add Preloading `. + +Importing Specific Files From a 3rd Party Package +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes you'll need to import a specific file from a package. For example, +suppose you're integrating `highlight.js`_ and want to import just the core +and a specific language: + +.. code-block:: javascript + + import hljs from 'highlight.js/lib/core'; + import javascript from 'highlight.js/lib/languages/javascript'; + + hljs.registerLanguage('javascript', javascript); + hljs.highlightAll(); + +In this case, adding the ``highlight.js`` package to your ``importmap.php`` file +won't work: whatever you import - e.g. ``highlight.js/lib/core`` - needs to +*exactly* match an entry in the ``importmap.php`` file. + +Instead, use ``importmap:require`` and pass it the exact paths you need. This +also shows how you can require multiple packages at once: + +.. code-block:: terminal + + $ php bin/console importmap:require highlight.js/lib/core highlight.js/lib/languages/javascript + +Global Variables like jQuery +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You might be accustomed to relying on global variables - like jQuery's ``$`` +variable: + +.. code-block:: javascript + + // assets/app.js + import 'jquery'; + + // app.js or any other file + $('.something').hide(); // WILL NOT WORK! + +But in a module environment (like with AssetMapper), when you import +a library like ``jquery``, it does *not* create a global variable. Instead, you +should import it and set it to a variable in *every* file you need it: + +.. code-block:: javascript + + import $ from 'jquery'; + $('.something').hide(); + +You can even do this from an inline script tag: + +.. code-block:: html + + + +If you *do* need something to become a global variable, you do it manually +from inside ``app.js``: + +.. code-block:: javascript + + import $ from 'jquery'; + // things on "window" become global variables + window.$ = $; + +.. _asset-mapper-handling-css: + +Handling CSS +------------ + +CSS can be added to your page by importing it from a JavaScript file. The default +``assets/app.js`` already imports ``assets/styles/app.css``: + +.. code-block:: javascript + + // assets/app.js + import '../styles/app.css'; + + // ... + +When you call ``importmap('app')`` in ``base.html.twig``, AssetMapper parses +``assets/app.js`` (and any JavaScript files that it imports) looking for ``import`` +statements for CSS files. The final collection of CSS files is rendered onto +the page as ``link`` tags in the order they were imported. + +.. note:: + + Importing a CSS file is *not* something that is natively supported by + JavaScript modules. AssetMapper makes this work by adding a special importmap + entry for each CSS file. These special entries are valid, but do nothing. + AssetMapper adds a ```` tag for each CSS file, but when JavaScript + executes the ``import`` statement, nothing additional happens. + +.. _asset-mapper-3rd-party-css: + +Handling 3rd-Party CSS +~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes a JavaScript package will contain one or more CSS files. For example, +the ``bootstrap`` package has a `dist/css/bootstrap.min.css file`_. + +You can require CSS files in the same way as JavaScript files: + +.. code-block:: terminal + + $ php bin/console importmap:require bootstrap/dist/css/bootstrap.min.css + +To include it on the page, import it from a JavaScript file: + +.. code-block:: javascript + + // assets/app.js + import 'bootstrap/dist/css/bootstrap.min.css'; + + // ... + +.. tip:: + + Some packages - like ``bootstrap`` - advertise that they contain a CSS + file. In those cases, when you ``importmap:require bootstrap``, the + CSS file is also added to ``importmap.php`` for convenience. If some package + doesn't advertise its CSS file in the ``style`` property of the + `package.json configuration file`_ try to contact the package maintainer to + ask them to add that. + +Paths Inside of CSS Files +~~~~~~~~~~~~~~~~~~~~~~~~~ + +From inside CSS, you can reference other files using the normal CSS ``url()`` +function and a relative path to the target file: + +.. code-block:: css + + /* assets/styles/app.css */ + .quack { + /* file lives at assets/images/duck.png */ + background-image: url('../images/duck.png'); + } + +The path in the final ``app.css`` file will automatically include the versioned URL +for ``duck.png``: + +.. code-block:: css + + /* public/assets/styles/app-3c16d92m.css */ + .quack { + background-image: url('../images/duck-3c16d92m.png'); + } + +.. _asset-mapper-tailwind: + +Using Tailwind CSS +~~~~~~~~~~~~~~~~~~ + +To use the `Tailwind`_ CSS framework with the AssetMapper component, check out +`symfonycasts/tailwind-bundle`_. + +.. _asset-mapper-sass: + +Using Sass +~~~~~~~~~~ + +To use Sass with AssetMapper component, check out `symfonycasts/sass-bundle`_. + +Lazily Importing CSS from a JavaScript File +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you have some CSS that you want to load lazily, you can do that via +the normal, "dynamic" import syntax: + +.. code-block:: javascript + + // assets/any-file.js + import('./lazy.css'); + + // ... + +In this case, ``lazy.css`` will be downloaded asynchronously and then added to +the page. If you use a dynamic import to lazily-load a JavaScript file and that +file imports a CSS file (using the non-dynamic ``import`` syntax), that CSS file +will also be downloaded asynchronously. + +Issues and Debugging +-------------------- + +There are a few common errors and problems you might run into. + +Missing importmap Entry +~~~~~~~~~~~~~~~~~~~~~~~ + +One of the most common errors will come from your browser's console, and +will look something like this: + + Failed to resolve module specifier " bootstrap". Relative references must start + with either "/", "./", or "../". + +Or: + + The specifier "bootstrap" was a bare specifier, but was not remapped to anything. + Relative module specifiers must start with "./", "../" or "/". + +This means that, somewhere in your JavaScript, you're importing a 3rd party +package - e.g. ``import 'bootstrap'``. The browser tries to find this +package in your ``importmap`` file, but it's not there. + +The fix is almost always to add it to your ``importmap``: + +.. code-block:: terminal + + $ php bin/console importmap:require bootstrap + +.. note:: + + Some browsers, like Firefox, show *where* this "import" code lives, while + others like Chrome currently do not. + +404 Not Found for a JavaScript, CSS or Image File +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes a JavaScript file you're importing (e.g. ``import './duck.js'``), +or a CSS/image file you're referencing won't be found, and you'll see a 404 +error in your browser's console. You'll also notice that the 404 URL is missing +the version hash in the filename (e.g. a 404 to ``/assets/duck.js`` instead of +a path like ``/assets/duck-1b7a64b3.js``). + +This is usually because the path is wrong. If you're referencing the file +directly in a Twig template: + +.. code-block:: html+twig + + + +Then the path that you pass ``asset()`` should be the "logical path" to the +file. Use the ``debug:asset-map`` command to see all valid logical paths +in your app. + +More likely, you're importing the failing asset from a CSS file (e.g. +``@import url('other.css')``) or a JavaScript file: + +.. code-block:: javascript + + // assets/controllers/farm-controller.js + import '../farm/chicken.js'; + +When doing this, the path should be *relative* to the file that's importing it +(and, in JavaScript files, should start with ``./`` or ``../``). In this case, +``../farm/chicken.js`` would point to ``assets/farm/chicken.js``. To +see a list of *all* invalid imports in your app, run: + +.. code-block:: terminal + + $ php bin/console cache:clear + $ php bin/console debug:asset-map + +Any invalid imports will show up as warnings on top of the screen (make sure +you have ``symfony/monolog-bundle`` installed): + +.. code-block:: text + + WARNING [asset_mapper] Unable to find asset "../images/ducks.png" referenced in "assets/styles/app.css". + WARNING [asset_mapper] Unable to find asset "./ducks.js" imported from "assets/app.js". + +Missing Asset Warnings on Commented-out Code +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The AssetMapper component looks in your JavaScript files for ``import`` lines so +that it can :ref:`automatically add them to your importmap `. +This is done via regex and works very well, though it isn't perfect. If you +comment-out an import, it will still be found and added to your importmap. That +doesn't harm anything, but could be surprising. + +If the imported path cannot be found, you'll see warning log when that asset +is being built, which you can ignore. + +.. _asset-mapper-deployment: + +Deploying with the AssetMapper Component +---------------------------------------- + +When you're ready to deploy, "compile" your assets by running this command: + +.. code-block:: terminal + + $ php bin/console asset-map:compile + +This will write all your versioned asset files into the ``public/assets/`` directory, +along with a few JSON files (``manifest.json``, ``importmap.json``, etc.) so that +the ``importmap`` can be rendered lightning fast. + +.. _optimization: + +Optimizing Performance +---------------------- + +To make your AssetMapper-powered site fly, there are a few things you need to +do. If you want to take a shortcut, you can use a service like `Cloudflare`_, +which will automatically do most of these things for you: + +- **Use HTTP/2**: Your web server should be running HTTP/2 or HTTP/3 so the + browser can download assets in parallel. HTTP/2 is automatically enabled in Caddy + and can be activated in Nginx and Apache. Or, proxy your site through a + service like Cloudflare, which will automatically enable HTTP/2 for you. + +- **Compress your assets**: Your web server should compress (e.g. using gzip) + your assets (JavaScript, CSS, images) before sending them to the browser. This + is automatically enabled in Caddy and can be activated in Nginx and Apache. + In Cloudflare, assets are compressed by default. AssetMapper also supports + :ref:`precompressing your web assets ` to further + improve performance. + +- **Set long-lived cache expiry**: Your web server should set a long-lived + ``Cache-Control`` HTTP header on your assets. Because the AssetMapper component includes a version + hash in the filename of each asset, you can safely set ``max-age`` + to a very long time (e.g. 1 year). This isn't automatic in + any web server, but can be easily enabled. + +Once you've done these things, you can use a tool like `Lighthouse`_ to +check the performance of your site. + +.. _performance-preloading: + +Performance: Understanding Preloading +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +One issue that Lighthouse may report is: + + Avoid Chaining Critical Requests + +To understand the problem, imagine this theoretical setup: + +- ``assets/app.js`` imports ``./duck.js`` +- ``assets/duck.js`` imports ``bootstrap`` + +Without preloading, when the browser downloads the page, the following would happen: + +1. The browser downloads ``assets/app.js``; +2. It *then* sees the ``./duck.js`` import and downloads ``assets/duck.js``; +3. It *then* sees the ``bootstrap`` import and downloads ``assets/bootstrap.js``. + +Instead of downloading all 3 files in parallel, the browser would be forced to +download them one-by-one as it discovers them. That would hurt performance. + +AssetMapper avoids this problem by outputting "preload" ``link`` tags. +The logic works like this: + +**A) When you call ``importmap('app')`` in your template**, the AssetMapper component +looks at the ``assets/app.js`` file and finds all of the JavaScript files +that it imports or files that those files import, etc. + +**B) It then outputs a ``link`` tag** for each of those files with a ``rel="preload"`` +attribute. This tells the browser to start downloading those files immediately, +even though it hasn't yet seen the ``import`` statement for them. + +Additionally, if the :doc:`WebLink Component ` is available in your application, +Symfony will add a ``Link`` header in the response to preload the CSS files. + +.. _performance-precompressing: + +Pre-Compressing Assets +---------------------- + +Although most servers (Caddy, Nginx, Apache, FrankenPHP) and services like Cloudflare +provide asset compression features, AssetMapper also allows you to compress all +your assets before serving them. + +This improves performance because you can compress assets using the highest (and +slowest) compression ratios beforehand and provide those compressed assets to the +server, which then returns them to the client without wasting CPU resources on +compression. + +AssetMapper supports `Brotli`_, `Zstandard`_ and `gzip`_ compression formats. +Before using any of them, the server that pre-compresses assets must have +installed the following PHP extensions or CLI commands: + +* Brotli: ``brotli`` CLI command; `brotli PHP extension`_; +* Zstandard: ``zstd`` CLI command; `zstd PHP extension`_; +* gzip: ``zopfli`` (better) or ``gzip`` CLI command; `zlib PHP extension`_. + +Then, update your AssetMapper configuration to define which compression to use +and which file extensions should be compressed: + +.. code-block:: yaml + + # config/packages/asset_mapper.yaml + framework: + asset_mapper: + # ... + + precompress: + format: 'zstandard' + # if you don't define the following option, AssetMapper will compress all + # the extensions considered safe (css, js, json, svg, xml, ttf, otf, wasm, etc.) + extensions: ['css', 'js', 'json', 'svg', 'xml'] + +Now, when running the ``asset-map:compile`` command, all matching files will be +compressed in the configured format and at the highest compression level. The +compressed files are created with the same name as the original but with the +``.br``, ``.zst``, or ``.gz`` extension appended. + +Then, you need to configure your web server to serve the precompressed assets +instead of the original ones: + +.. configuration-block:: + + .. code-block:: caddy + + file_server { + precompressed br zstd gzip + } + + .. code-block:: nginx + + gzip_static on; + + # Requires https://github.com/google/ngx_brotli + brotli_static on; + + # Requires https://github.com/tokers/zstd-nginx-module + zstd_static on; + +.. tip:: + + AssetMapper provides an ``assets:compress`` CLI command and a service called + ``asset_mapper.compressor`` that you can use anywhere in your application to + compress any kind of files (e.g. files uploaded by users to your application). + +Frequently Asked Questions +-------------------------- + +Does the AssetMapper Component Combine Assets? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Nope! But that's because this is no longer necessary! + +In the past, it was common to combine assets to reduce the number of HTTP +requests that were made. Thanks to advances in web servers like +HTTP/2, it's typically not a problem to keep your assets separate and let the +browser download them in parallel. In fact, by keeping them separate, when +you update one asset, the browser can continue to use the cached version of +all of your other assets. + +See :ref:`Optimization ` for more details. + +Does the AssetMapper Component Minify Assets? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Nope! In most cases, this is perfectly fine. The web asset compression performed +by web servers before sending them is usually sufficient. However, if you think +you could benefit from minifying assets (in addition to later compressing them), +you can use the `SensioLabs Minify Bundle`_. + +This bundle integrates seamlessly with AssetMapper and minifies all web assets +automatically when running the ``asset-map:compile`` command (as explained in +the :ref:`serving assets in production ` section). + +See :ref:`Optimization ` for more details. + +Is the AssetMapper Component Production Ready? Is it Performant? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Yes! Very! The AssetMapper component leverages advances in browser technology (like +importmaps and native ``import`` support) and web servers (like HTTP/2, which allows +assets to be downloaded in parallel). See the other questions about minimization +and combination and :ref:`Optimization ` for more details. + +The https://ux.symfony.com site runs on the AssetMapper component and has a 99% +Google Lighthouse score. + +Does the AssetMapper Component work in All Browsers? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Yes! Features like importmaps and the ``import`` statement are supported +in all modern browsers, but the AssetMapper component ships with an `ES module shim`_ +to support ``importmap`` in old browsers. So, it works everywhere (see note +below). + +Inside your own code, if you're relying on modern `ES6`_ JavaScript features +like the `class syntax`_, this is supported in all but the oldest browsers. +If you *do* need to support very old browsers, you should use a tool like +:ref:`Encore ` instead of the AssetMapper component. + +.. note:: + + The `import statement`_ can't be polyfilled or shimmed to work on *every* + browser. However, only the **oldest** browsers don't support it - basically + IE 11 (which is no longer supported by Microsoft and has less than .4% + of global usage). + + The ``importmap`` feature **is** shimmed to work in **all** browsers by the + AssetMapper component. However, the shim doesn't work with "dynamic" imports: + + .. code-block:: javascript + + // this works + import { add } from './math.js'; + + // this will not work in the oldest browsers + import('./math.js').then(({ add }) => { + // ... + }); + + If you want to use dynamic imports and need to support certain older browsers + (https://caniuse.com/import-maps), you can use an ``importShim()`` function + from the shim: https://www.npmjs.com/package/es-module-shims#user-content-polyfill-edge-case-dynamic-import + +Can I Use it with Sass or Tailwind? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sure! See :ref:`Using Tailwind CSS ` or :ref:`Using Sass `. + +Can I Use it with TypeScript? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sure! See :ref:`Using TypeScript `. + +Can I Use it with JSX or Vue? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Probably not. And if you're writing an application in React, Svelte or another +frontend framework, you'll probably be better off using *their* tools directly. + +JSX *can* be compiled directly to a native JavaScript file but if you're using a lot of JSX, +you'll probably want to use a tool like :ref:`Encore `. +See the `UX React Documentation`_ for more details about using it with the AssetMapper +component. + +Vue files *can* be written in native JavaScript, and those *will* work with +the AssetMapper component. But you cannot write single-file components (i.e. ``.vue`` +files) with component, as those must be used in a build system. See the +`UX Vue.js Documentation`_ for more details about using with the AssetMapper +component. + +Can I Lint and Format My Code? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Not with AssetMapper, but you can install `kocal/biome-js-bundle`_ in your project +to lint and format your front-end assets. It's much faster than alternatives like +Prettier and requires no configuration to handle your JavaScript, TypeScript and CSS files. + +.. _asset-mapper-ts: + +Using TypeScript +---------------- + +To use TypeScript with the AssetMapper component, check out `sensiolabs/typescript-bundle`_. + +Third-Party Bundles & Custom Asset Paths +---------------------------------------- + +All bundles that have a ``Resources/public/`` or ``public/`` directory will +automatically have that directory added as an "asset path", using the namespace: +``bundles/``. For example, if you're using `BabdevPagerfantaBundle`_ +and you run the ``debug:asset-map`` command, you'll see an asset whose logical +path is ``bundles/babdevpagerfanta/css/pagerfanta.css``. + +This means you can render these assets in your templates using the +``asset()`` function: + +.. code-block:: html+twig + + + +Actually, this path - ``bundles/babdevpagerfanta/css/pagerfanta.css`` - already +works in applications *without* the AssetMapper component, because the ``assets:install`` +command copies the assets from bundles into ``public/bundles/``. However, when +the AssetMapper component is enabled, the ``pagerfanta.css`` file will automatically +be versioned! It will output something like: + +.. code-block:: html+twig + + + +Overriding 3rd-Party Assets +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to override a 3rd-party asset, you can do that by creating a +file in your ``assets/`` directory with the same name. For example, if you +want to override the ``pagerfanta.css`` file, create a file at +``assets/bundles/babdevpagerfanta/css/pagerfanta.css``. This file will be +used instead of the original file. + +.. note:: + + If a bundle renders their *own* assets, but they use a non-default + :ref:`asset package `, then the AssetMapper component will + not be used. This happens, for example, with `EasyAdminBundle`_. + +Importing Assets Outside of the ``assets/`` Directory +----------------------------------------------------- + +You *can* import assets that live outside of your asset path +(i.e. the ``assets/`` directory). For example: + +.. code-block:: css + + /* assets/styles/app.css */ + + /* you can reach above assets/ */ + @import url('../../vendor/babdev/pagerfanta-bundle/Resources/public/css/pagerfanta.css'); + +However, if you get an error like this: + + The "app" importmap entry contains the path "vendor/some/package/assets/foo.js" + but it does not appear to be in any of your asset paths. + +It means that you're pointing to a valid file, but that file isn't in any of +your asset paths. You can fix this by adding the path to your ``asset_mapper.yaml`` +file: + +.. code-block:: yaml + + # config/packages/asset_mapper.yaml + framework: + asset_mapper: + paths: + - assets/ + - vendor/some/package/assets + +Then try the command again. + +Configuration Options +--------------------- + +You can see every available configuration options and some info by running: + +.. code-block:: terminal + + $ php bin/console config:dump framework asset_mapper + +Some of the more important options are described below. + +``framework.asset_mapper.paths`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This config holds all of the directories that will be scanned for assets. This +can be a simple list: + +.. code-block:: yaml + + framework: + asset_mapper: + paths: + - assets/ + - vendor/some/package/assets + +Or you can give each path a "namespace" that will be used in the asset map: + +.. code-block:: yaml + + framework: + asset_mapper: + paths: + assets/: '' + vendor/some/package/assets/: 'some-package' + +In this case, the "logical path" to all of the files in the ``vendor/some/package/assets/`` +directory will be prefixed with ``some-package`` - e.g. ``some-package/foo.js``. + +.. _excluded_patterns: + +``framework.asset_mapper.excluded_patterns`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is a list of glob patterns that will be excluded from the asset map: + +.. code-block:: yaml + + framework: + asset_mapper: + excluded_patterns: + - '*/*.scss' + +You can use the ``debug:asset-map`` command to double-check that the files +you expect are being included in the asset map. + +``framework.asset_mapper.exclude_dotfiles`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Whether to exclude any file starting with a ``.`` from the asset mapper. This +is useful if you want to avoid leaking sensitive files like ``.env`` or +``.gitignore`` in the files published by the asset mapper. + +.. code-block:: yaml + + framework: + asset_mapper: + exclude_dotfiles: true + +This option is enabled by default. + +.. _config-importmap-polyfill: + +``framework.asset_mapper.importmap_polyfill`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Configure the polyfill for older browsers. By default, the `ES module shim`_ is loaded +via a CDN (i.e. the default value for this setting is ``es-module-shims``): + +.. code-block:: yaml + + framework: + asset_mapper: + # set this option to false to disable the shim entirely + # (your website/web app won't work in old browsers) + importmap_polyfill: false + + # you can also use a custom polyfill by adding it to your importmap.php file + # and setting this option to the key of that file in the importmap.php file + # importmap_polyfill: 'custom_polyfill' + +.. tip:: + + You can tell the AssetMapper to load the `ES module shim`_ locally by + using the following command, without changing your configuration: + + .. code-block:: terminal + + $ php bin/console importmap:require es-module-shims + +``framework.asset_mapper.importmap_script_attributes`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is a list of attributes that will be added to the `` + + + #} + {{ encore_entry_script_tags('homepage') }} + +Controlling how Assets are Split +-------------------------------- + +The logic for *when* and *how* to split the files is controlled by the +`SplitChunksPlugin from Webpack`_. You can control the configuration passed to +this plugin with the ``configureSplitChunks()`` function: + +.. code-block:: diff + + // webpack.config.js + Encore + // ... + + .splitEntryChunks() + + .configureSplitChunks(function(splitChunks) { + + // change the configuration + + splitChunks.minSize = 0; + + }) + +.. _`SplitChunksPlugin from Webpack`: https://webpack.js.org/plugins/split-chunks-plugin/ diff --git a/frontend/encore/typescript.rst b/frontend/encore/typescript.rst new file mode 100644 index 00000000000..c9cd7487d39 --- /dev/null +++ b/frontend/encore/typescript.rst @@ -0,0 +1,63 @@ +Enabling TypeScript (ts-loader) with Webpack Encore +=================================================== + +Want to use `TypeScript`_? No problem! First, enable it: + +.. code-block:: diff + + // webpack.config.js + + // ... + Encore + // ... + + .addEntry('main', './assets/main.ts') + + + .enableTypeScriptLoader() + + // optionally enable forked type script for faster builds + // https://www.npmjs.com/package/fork-ts-checker-webpack-plugin + // requires that you have a tsconfig.json file that is setup correctly. + + //.enableForkedTypeScriptTypesChecking() + ; + +Then create an empty ``tsconfig.json`` file with the contents ``{}`` in the project +root folder (or in the folder where your TypeScript files are located; e.g. ``assets/``). +In ``tsconfig.json`` you can define more options, as shown in `tsconfig.json reference`_. + +Then restart Encore. When you do, it will give you a command you can run to +install any missing dependencies. After running that command and restarting +Encore, you're done! + +Any ``.ts`` files that you require will be processed correctly. You can +also configure the `ts-loader options`_ via the ``enableTypeScriptLoader()`` +method. + +.. code-block:: diff + + // webpack.config.js + Encore + // ... + .addEntry('main', './assets/main.ts') + + - .enableTypeScriptLoader() + + .enableTypeScriptLoader(function(tsConfig) { + + // You can use this callback function to adjust ts-loader settings + + // https://github.com/TypeStrong/ts-loader/blob/master/README.md#loader-options + + // For example: + + // tsConfig.silent = false + + }) + + // ... + ; + +See the `Encore's index.js file`_ for detailed documentation and check +out the `tsconfig.json reference`_ and the `Webpack guide about Typescript`_. + +If React is enabled (``.enableReactPreset()``), any ``.tsx`` file will also be +processed by ``ts-loader``. + +.. _`TypeScript`: https://www.typescriptlang.org/ +.. _`ts-loader options`: https://github.com/TypeStrong/ts-loader#options +.. _`Encore's index.js file`: https://github.com/symfony/webpack-encore/blob/master/index.js +.. _`tsconfig.json reference`: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html +.. _`Webpack guide about Typescript`: https://webpack.js.org/guides/typescript/ diff --git a/frontend/encore/url-loader.rst b/frontend/encore/url-loader.rst new file mode 100644 index 00000000000..f63fa01cc8d --- /dev/null +++ b/frontend/encore/url-loader.rst @@ -0,0 +1,32 @@ +Inlining Images & Fonts in CSS with Webpack Encore +================================================== + +A simple technique to improve the performance of web applications is to reduce +the number of HTTP requests inlining small files as base64 encoded URLs in the +generated CSS files. + +You can enable this in ``webpack.config.js`` for images, fonts or both: + +.. code-block:: javascript + + // webpack.config.js + // ... + + Encore + // ... + .configureImageRule({ + // tell Webpack it should consider inlining + type: 'asset', + //maxSize: 4 * 1024, // 4 kb - the default is 8kb + }) + + .configureFontRule({ + type: 'asset', + //maxSize: 4 * 1024 + }) + ; + +This leverages Webpack `Asset Modules`_. You can read more about this and the +configuration there. + +.. _`Asset Modules`: https://webpack.js.org/guides/asset-modules/ diff --git a/frontend/encore/versioning.rst b/frontend/encore/versioning.rst new file mode 100644 index 00000000000..5b848c17b04 --- /dev/null +++ b/frontend/encore/versioning.rst @@ -0,0 +1,92 @@ +Asset Versioning with Webpack Encore +==================================== + +.. _encore-long-term-caching: + +Tired of deploying and having browser's cache the old version of your assets? +By calling ``enableVersioning()``, each filename will now include a hash that +changes whenever the *contents* of that file change (e.g. ``app.123abc.js`` +instead of ``app.js``). This allows you to use aggressive caching strategies +(e.g. a far future ``Expires``) because, whenever a file changes, its hash will change, +ignoring any existing cache: + +.. code-block:: diff + + // webpack.config.js + + // ... + Encore + .setOutputPath('public/build/') + // ... + + .enableVersioning() + +To link to these assets, Encore creates two files ``entrypoints.json`` and +``manifest.json``. + +.. _load-manifest-files: + +Loading Assets from ``entrypoints.json`` & ``manifest.json`` +------------------------------------------------------------ + +Whenever you run Encore, two configuration files are generated in your +output folder (default location: ``public/build/``): ``entrypoints.json`` +and ``manifest.json``. Each file is similar, and contains a map to the final, versioned +filenames. + +The first file – ``entrypoints.json`` – is used by the ``encore_entry_script_tags()`` +and ``encore_entry_link_tags()`` Twig helpers. If you're using these, then your +CSS and JavaScript files will render with the new, versioned filename. If you're +not using Symfony, your app will need to read this file in a similar way. + +The ``manifest.json`` file is only needed to get the versioned filename of *other* +files, like font files or image files (though it also contains information about +the CSS and JavaScript files): + +.. code-block:: json + + { + "build/app.js": "/build/app.123abc.js", + "build/dashboard.css": "/build/dashboard.a4bf2d.css", + "build/images/logo.png": "/build/images/logo.3eed42.png" + } + +In your app, you need to read this file if you want to be able to link (e.g. via +an ``img`` tag) to certain assets. If you're using Symfony, just activate the +``json_manifest_file`` versioning strategy: + +.. code-block:: yaml + + # this file is added automatically when installing Encore with Symfony Flex + # config/packages/assets.yaml + framework: + assets: + json_manifest_path: '%kernel.project_dir%/public/build/manifest.json' + +That's it! Be sure to wrap each path in the Twig ``asset()`` function +like normal: + +.. code-block:: html+twig + + ACME logo + +Troubleshooting +--------------- + +Asset Versioning and Deployment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When deploying a new version of your application, versioned assets will include +a new hash, making the previous assets no longer available. This is usually not +a problem when deploying applications using a rolling update, blue/green or +symlink strategies. + +However, even when applying those techniques, there could be a lapse of time +when some publicly/privately cached response requests the previous version of +the assets. If your application can't afford to serve any broken asset, the best +solution is to use a CDN (or custom made service) that keeps all the old assets +cached for some time. + +Learn more +---------- + +* :doc:`/components/asset` diff --git a/frontend/encore/virtual-machine.rst b/frontend/encore/virtual-machine.rst new file mode 100644 index 00000000000..34587173b93 --- /dev/null +++ b/frontend/encore/virtual-machine.rst @@ -0,0 +1,122 @@ +Using Encore in a Virtual Machine +================================= + +Encore is compatible with virtual machines such as `VirtualBox`_ and `VMWare`_ +but you may need to make some changes to your configuration to make it work. + +File Watching Issues +-------------------- + +When using a virtual machine, your project root directory is shared with the +virtual machine using `NFS`_. This introduces issues with files watching, so +you must enable the `polling`_ option to make it work: + +.. code-block:: javascript + + // webpack.config.js + + // ... + + // will be applied for `encore dev --watch` and `encore dev-server` commands + Encore.configureWatchOptions(watchOptions => { + watchOptions.poll = 250; // check for changes every 250 milliseconds + }); + +Development Server Issues +------------------------- + +Configure the Public Path +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + + You can skip this section if your application is running on + ``http://localhost`` instead a custom local domain-name like + ``http://app.vm``. + +When running the development server, you will probably see the following errors +in the web console: + +.. code-block:: text + + GET http://localhost:8080/build/vendors~app.css net::ERR_CONNECTION_REFUSED + GET http://localhost:8080/build/runtime.js net::ERR_CONNECTION_REFUSED + ... + +If your Symfony application is running on a custom domain (e.g. +``http://app.vm``), you must configure the public path explicitly in your +``package.json``: + +.. code-block:: diff + + { + ... + "scripts": { + - "dev-server": "encore dev-server", + + "dev-server": "encore dev-server --public http://app.vm:8080", + ... + } + } + +After restarting Encore and reloading your web page, you will probably see +different issues in the web console: + +.. code-block:: text + + GET http://app.vm:8080/build/vendors~app.css net::ERR_CONNECTION_REFUSED + GET http://app.vm:8080/build/runtime.js net::ERR_CONNECTION_REFUSED + +You still need to make other configuration changes, as explained in the +following sections. + +Allow External Access +~~~~~~~~~~~~~~~~~~~~~ + +Add the ``--host 0.0.0.0`` argument to the ``dev-server`` configuration in your +``package.json`` file to make the development server accept all incoming +connections: + +.. code-block:: diff + + { + ... + "scripts": { + - "dev-server": "encore dev-server --public http://app.vm:8080", + + "dev-server": "encore dev-server --public http://app.vm:8080 --host 0.0.0.0", + ... + } + } + +.. danger:: + + Make sure to run the development server inside your virtual machine only; + otherwise other computers can have access to it. + +Fix "Invalid Host header" Issue +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Webpack will respond ``Invalid Host header`` when trying to access files from +the dev-server. To fix this, set the ``allowedHosts`` option: + +.. code-block:: javascript + + // webpack.config.js + // ... + + Encore + // ... + + .configureDevServerOptions(options => { + options.allowedHosts = 'all'; + }) + +.. warning:: + + Beware that `it's not recommended to set allowedHosts to all`_ in general, but + here it's required to solve the issue when using Encore in a virtual machine. + +.. _`VirtualBox`: https://www.virtualbox.org/ +.. _`VMWare`: https://www.vmware.com +.. _`NFS`: https://en.wikipedia.org/wiki/Network_File_System +.. _`polling`: https://webpack.js.org/configuration/watch/#watchoptionspoll +.. _`it's not recommended to set allowedHosts to all`: https://webpack.js.org/configuration/dev-server/#devserverallowedhosts diff --git a/frontend/encore/vuejs.rst b/frontend/encore/vuejs.rst new file mode 100644 index 00000000000..354e6c590aa --- /dev/null +++ b/frontend/encore/vuejs.rst @@ -0,0 +1,215 @@ +Enabling Vue.js (``vue-loader``) with Webpack Encore +==================================================== + +.. admonition:: Screencast + :class: screencast + + Do you prefer video tutorials? Check out the `Vue screencast series`_. + +.. tip:: + + Check out live demos of Symfony UX Vue.js component at `https://ux.symfony.com/vue`_! + +Want to use `Vue.js`_? No problem! First enable it in ``webpack.config.js``: + +.. code-block:: diff + + // webpack.config.js + // ... + + Encore + // ... + .addEntry('main', './assets/main.js') + + + .enableVueLoader() + ; + +Then restart Encore. When you do, it will give you a command you can run to +install any missing dependencies. After running that command and restarting +Encore, you're done! + +Any ``.vue`` files that you require will be processed correctly. You can also +configure the `vue-loader options`_ by passing an options callback to +``enableVueLoader()``. See the `Encore's index.js file`_ for detailed documentation. + +Runtime Compiler Build +---------------------- + +By default, Encore uses a Vue "build" that allows you to compile templates at +runtime. This means that you *can* do either of these: + +.. code-block:: javascript + + new Vue({ + template: '
          {{ hi }}
          ' + }) + + new Vue({ + el: '#app', // where
          in your DOM contains the Vue template + }); + +If you do *not* need this functionality (e.g. you use single file components), +then you can tell Encore to create a *smaller* build following Content Security Policy: + +.. code-block:: javascript + + // webpack.config.js + // ... + + Encore + // ... + + .enableVueLoader(() => {}, { runtimeCompilerBuild: false }) + ; + +You can also silence the recommendation by passing ``runtimeCompilerBuild: true``. + +Hot Module Replacement (HMR) +---------------------------- + +The ``vue-loader`` supports hot module replacement: just update your code and watch +your Vue.js app update *without* a browser refresh! To activate it, use the +``dev-server``: + +.. code-block:: terminal + + $ npm run dev-server + +That's it! Change one of your ``.vue`` files and watch your browser update. But +note: this does *not* currently work for *style* changes in a ``.vue`` file. Seeing +updated styles still requires a page refresh. + +See :doc:`/frontend/encore/dev-server` for more details. + +JSX Support +----------- + +You can enable `JSX with Vue.js`_ by configuring the second parameter of the +``.enableVueLoader()`` method: + +.. code-block:: diff + + // webpack.config.js + // ... + + Encore + // ... + .addEntry('main', './assets/main.js') + + - .enableVueLoader() + + .enableVueLoader(() => {}, { + + useJsx: true + + }) + ; + +Next, run or restart Encore. When you do, you will see an error message helping +you install any missing dependencies. After running that command and restarting +Encore, you're done! + +Your ``.jsx`` files will now be transformed through ``@vue/babel-preset-jsx``. + +Using styles +~~~~~~~~~~~~ + +You can't use ```` sections and you must **inline +all the CSS styles**. + +CSS inlining means that every HTML tag must define a ``style`` attribute with +all its CSS styles. This can make organizing your CSS a mess. That's why Twig +provides a ``CssInlinerExtension`` that automates everything for you. Install +it with: + +.. code-block:: terminal + + $ composer require twig/extra-bundle twig/cssinliner-extra + +The extension is enabled automatically. To use it, wrap the entire template +with the ``inline_css`` filter: + +.. code-block:: html+twig + + {% apply inline_css %} + + +

          Welcome {{ email.toName }}!

          + {# ... #} + {% endapply %} + +Using External CSS Files +........................ + +You can also define CSS styles in external files and pass them as +arguments to the filter: + +.. code-block:: html+twig + + {% apply inline_css(source('@styles/email.css')) %} +

          Welcome {{ username }}!

          + {# ... #} + {% endapply %} + +You can pass unlimited number of arguments to ``inline_css()`` to load multiple +CSS files. For this example to work, you also need to define a new Twig namespace +called ``styles`` that points to the directory where ``email.css`` lives: + +.. _mailer-css-namespace: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/twig.yaml + twig: + # ... + + paths: + # point this wherever your css files live + '%kernel.project_dir%/assets/styles': styles + + .. code-block:: xml + + + + + + + + + %kernel.project_dir%/assets/styles + + + + .. code-block:: php + + // config/packages/twig.php + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig): void { + // ... + + // point this wherever your css files live + $twig->path('%kernel.project_dir%/assets/styles', 'styles'); + }; + +.. _mailer-markdown: + +Rendering Markdown Content +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Twig provides another extension called ``MarkdownExtension`` that lets you +define the email contents using `Markdown syntax`_. To use this, install the +extension and a Markdown conversion library (the extension is compatible with +several popular libraries): + +.. code-block:: terminal + + # instead of league/commonmark, you can also use erusev/parsedown or michelf/php-markdown + $ composer require twig/extra-bundle twig/markdown-extra league/commonmark + +The extension adds a ``markdown_to_html`` filter, which you can use to convert parts or +the entire email contents from Markdown to HTML: + +.. code-block:: twig + + {% apply markdown_to_html %} + Welcome {{ email.toName }}! + =========================== + + You signed up to our site using the following email: + `{{ email.to[0].address }}` + + [Activate your account]({{ url('...') }}) + {% endapply %} + +.. _mailer-inky: + +Inky Email Templating Language +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Creating beautifully designed emails that work on every email client is so +complex that there are HTML/CSS frameworks dedicated to that. One of the most +popular frameworks is called `Inky`_. It defines a syntax based on some HTML-like +tags which are later transformed into the real HTML code sent to users: + +.. code-block:: html + + + + + This is a column. + + + +Twig provides integration with Inky via the ``InkyExtension``. First, install +the extension in your application: + +.. code-block:: terminal + + $ composer require twig/extra-bundle twig/inky-extra + +The extension adds an ``inky_to_html`` filter, which can be used to convert +parts or the entire email contents from Inky to HTML: + +.. code-block:: html+twig + + {% apply inky_to_html %} + + + + +

          Welcome {{ email.toName }}!

          +
          + + {# ... #} +
          +
          + {% endapply %} + +You can combine all filters to create complex email messages: + +.. code-block:: twig + + {% apply inky_to_html|inline_css(source('@styles/foundation-emails.css')) %} + {# ... #} + {% endapply %} + +This makes use of the :ref:`styles Twig namespace ` we created +earlier. You could, for example, `download the foundation-emails.css file`_ +directly from GitHub and save it in ``assets/styles``. + +.. _signing-and-encrypting-messages: + +Signing and Encrypting Messages +------------------------------- + +It's possible to sign and/or encrypt email messages to increase their +integrity/security. Both options can be combined to encrypt a signed message +and/or to sign an encrypted message. + +Before signing/encrypting messages, make sure to have: + +* The `OpenSSL PHP extension`_ properly installed and configured; +* A valid `S/MIME`_ security certificate. + +.. tip:: + + When using OpenSSL to generate certificates, make sure to add the + ``-addtrust emailProtection`` command option. + +.. warning:: + + Signing and encrypting messages require their contents to be fully rendered. + For example, the content of :ref:`templated emails ` is rendered + by a :class:`Symfony\\Component\\Mailer\\EventListener\\MessageListener`. + So, if you want to sign and/or encrypt such a message, you need to do it in + a :ref:`MessageEvent ` listener run after it (you need to set + a negative priority to your listener). + +Signing Messages +~~~~~~~~~~~~~~~~ + +When signing a message, a cryptographic hash is generated for the entire content +of the message (including attachments). This hash is added as an attachment so +the recipient can validate the integrity of the received message. However, the +contents of the original message are still readable for mailing agents not +supporting signed messages, so you must also encrypt the message if you want to +hide its contents. + +You can sign messages using either ``S/MIME`` or ``DKIM``. In both cases, the +certificate and private key must be `PEM encoded`_, and can be either created +using for example OpenSSL or obtained at an official Certificate Authority (CA). +The email recipient must have the CA certificate in the list of trusted issuers +in order to verify the signature. + +.. warning:: + + If you use message signature, sending to ``Bcc`` will be removed from the + message. If you need to send a message to multiple recipients, you need + to compute a new signature for each recipient. + +S/MIME Signer +............. + +`S/MIME`_ is a standard for public key encryption and signing of MIME data. It +requires using both a certificate and a private key:: + + use Symfony\Component\Mime\Crypto\SMimeSigner; + use Symfony\Component\Mime\Email; + + $email = (new Email()) + ->from('hello@example.com') + // ... + ->html('...'); + + $signer = new SMimeSigner('/path/to/certificate.crt', '/path/to/certificate-private-key.key'); + // if the private key has a passphrase, pass it as the third argument + // new SMimeSigner('/path/to/certificate.crt', '/path/to/certificate-private-key.key', 'the-passphrase'); + + $signedEmail = $signer->sign($email); + // now use the Mailer component to send this $signedEmail instead of the original email + +.. tip:: + + The ``SMimeSigner`` class defines other optional arguments to pass + intermediate certificates and to configure the signing process using a + bitwise operator options for :phpfunction:`openssl_pkcs7_sign` PHP function. + +DKIM Signer +........... + +`DKIM`_ is an email authentication method that affixes a digital signature, +linked to a domain name, to each outgoing email messages. It requires a private +key but not a certificate:: + + use Symfony\Component\Mime\Crypto\DkimSigner; + use Symfony\Component\Mime\Email; + + $email = (new Email()) + ->from('hello@example.com') + // ... + ->html('...'); + + // first argument: same as openssl_pkey_get_private(), either a string with the + // contents of the private key or the absolute path to it (prefixed with 'file://') + // second and third arguments: the domain name and "selector" used to perform a DNS lookup + // (the selector is a string used to point to a specific DKIM public key record in your DNS) + $signer = new DkimSigner('file:///path/to/private-key.key', 'example.com', 'sf'); + // if the private key has a passphrase, pass it as the fifth argument + // new DkimSigner('file:///path/to/private-key.key', 'example.com', 'sf', [], 'the-passphrase'); + + $signedEmail = $signer->sign($email); + // now use the Mailer component to send this $signedEmail instead of the original email + + // DKIM signer provides many config options and a helper object to configure them + use Symfony\Component\Mime\Crypto\DkimOptions; + + $signedEmail = $signer->sign($email, (new DkimOptions()) + ->bodyCanon('relaxed') + ->headerCanon('relaxed') + ->headersToIgnore(['Message-ID']) + ->toArray() + ); + +Signing Messages Globally +......................... + +Instead of creating a signer instance for each email, you can configure a global +signer that automatically applies to all outgoing messages. This approach +minimizes repetition and centralizes your configuration for DKIM and S/MIME signing. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mailer.yaml + framework: + mailer: + dkim_signer: + key: 'file://%kernel.project_dir%/var/certificates/dkim.pem' + domain: 'symfony.com' + select: 's1' + smime_signer: + key: '%kernel.project_dir%/var/certificates/smime.key' + certificate: '%kernel.project_dir%/var/certificates/smime.crt' + passphrase: '' + + .. code-block:: xml + + + + + + + + + + file://%kernel.project_dir%/var/certificates/dkim.pem + symfony.com + s1 + + + %kernel.project_dir%/var/certificates/smime.pem + %kernel.project_dir%/var/certificates/smime.crt + + + + + + + .. code-block:: php + + // config/packages/mailer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $mailer = $framework->mailer(); + $mailer->dsn('%env(MAILER_DSN)%'); + $mailer->dkimSigner() + ->key('file://%kernel.project_dir%/var/certificates/dkim.pem') + ->domain('symfony.com') + ->select('s1'); + + $mailer->smimeSigner() + ->key('%kernel.project_dir%/var/certificates/smime.key') + ->certificate('%kernel.project_dir%/var/certificates/smime.crt') + ->passphrase('') + ; + }; + +.. versionadded:: 7.3 + + Global message signing was introduced in Symfony 7.3. + +Encrypting Messages +~~~~~~~~~~~~~~~~~~~ + +When encrypting a message, the entire message (including attachments) is +encrypted using a certificate. Therefore, only the recipients that have the +corresponding private key can read the original message contents:: + + use Symfony\Component\Mime\Crypto\SMimeEncrypter; + use Symfony\Component\Mime\Email; + + $email = (new Email()) + ->from('hello@example.com') + // ... + ->html('...'); + + $encrypter = new SMimeEncrypter('/path/to/certificate.crt'); + $encryptedEmail = $encrypter->encrypt($email); + // now use the Mailer component to send this $encryptedEmail instead of the original email + +You can pass more than one certificate to the ``SMimeEncrypter`` constructor +and it will select the appropriate certificate depending on the ``To`` option:: + + $firstEmail = (new Email()) + // ... + ->to('jane@example.com'); + + $secondEmail = (new Email()) + // ... + ->to('john@example.com'); + + // the second optional argument of SMimeEncrypter defines which encryption algorithm is used + // (it must be one of these constants: https://www.php.net/manual/en/openssl.ciphers.php) + $encrypter = new SMimeEncrypter([ + // key = email recipient; value = path to the certificate file + 'jane@example.com' => '/path/to/first-certificate.crt', + 'john@example.com' => '/path/to/second-certificate.crt', + ]); + + $firstEncryptedEmail = $encrypter->encrypt($firstEmail); + $secondEncryptedEmail = $encrypter->encrypt($secondEmail); + +Encrypting Messages Globally +............................ + +Instead of creating a new encrypter for each email, you can configure a global S/MIME +encrypter that automatically applies to all outgoing messages: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mailer.yaml + framework: + mailer: + smime_encrypter: + certificate: '%kernel.project_dir%/var/certificates/smime.crt' + + .. code-block:: xml + + + + + + + + + + %kernel.project_dir%/var/certificates/smime.crt + + + + + + .. code-block:: php + + // config/packages/mailer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $mailer = $framework->mailer(); + $mailer->smimeEncrypter() + ->certificate('%kernel.project_dir%/var/certificates/smime.crt') + ; + }; + +.. versionadded:: 7.3 + + Global message encryption configuration was introduced in Symfony 7.3. + +.. _multiple-email-transports: + +Multiple Email Transports +------------------------- + +You may want to use more than one mailer transport for delivery of your messages. +This can be configured by replacing the ``dsn`` configuration entry with a +``transports`` entry, like: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mailer.yaml + framework: + mailer: + transports: + main: '%env(MAILER_DSN)%' + alternative: '%env(MAILER_DSN_IMPORTANT)%' + + .. code-block:: xml + + + + + + + + + %env(MAILER_DSN)% + %env(MAILER_DSN_IMPORTANT)% + + + + + .. code-block:: php + + // config/packages/mailer.php + use function Symfony\Component\DependencyInjection\Loader\Configurator\env; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->mailer() + ->transport('main', env('MAILER_DSN')) + ->transport('alternative', env('MAILER_DSN_IMPORTANT')) + ; + }; + +By default the first transport is used. The other transports can be selected by +adding an ``X-Transport`` header (which Mailer will remove automatically from +the final email):: + + // Send using first transport ("main"): + $mailer->send($email); + + // ... or use the transport "alternative": + $email->getHeaders()->addTextHeader('X-Transport', 'alternative'); + $mailer->send($email); + +.. _mailer-sending-messages-async: + +Sending Messages Async +---------------------- + +When you call ``$mailer->send($email)``, the email is sent to the transport immediately. +To improve performance, you can leverage :doc:`Messenger ` to send +the messages later via a Messenger transport. + +Start by following the :doc:`Messenger ` documentation and configuring +a transport. Once everything is set up, when you call ``$mailer->send()``, a +:class:`Symfony\\Component\\Mailer\\Messenger\\SendEmailMessage` message will +be dispatched through the default message bus (``messenger.default_bus``). Assuming +you have a transport called ``async``, you can route the message there: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + async: "%env(MESSENGER_TRANSPORT_DSN)%" + + routing: + 'Symfony\Component\Mailer\Messenger\SendEmailMessage': async + + .. code-block:: xml + + + + + + + + %env(MESSENGER_TRANSPORT_DSN)% + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->messenger() + ->transport('async')->dsn(env('MESSENGER_TRANSPORT_DSN')); + + $framework->messenger() + ->routing('Symfony\Component\Mailer\Messenger\SendEmailMessage') + ->senders(['async']); + }; + +Thanks to this, instead of being delivered immediately, messages will be sent +to the transport to be handled later (see :ref:`messenger-worker`). Note that +the "rendering" of the email (computed headers, body rendering, ...) is also +deferred and will only happen just before the email is sent by the Messenger +handler. + +When sending an email asynchronously, its instance must be serializable. +This is always the case for :class:`Symfony\\Component\\Mailer\\Mailer` +instances, but when sending a +:class:`Symfony\\Bridge\\Twig\\Mime\\TemplatedEmail`, you must ensure that +the ``context`` is serializable. If you have non-serializable variables, +like Doctrine entities, either replace them with more specific variables or +render the email before calling ``$mailer->send($email)``:: + + use Symfony\Component\Mailer\MailerInterface; + use Symfony\Component\Mime\BodyRendererInterface; + + public function action(MailerInterface $mailer, BodyRendererInterface $bodyRenderer): void + { + $email = (new TemplatedEmail()) + ->htmlTemplate($template) + ->context($context) + ; + $bodyRenderer->render($email); + + $mailer->send($email); + } + +You can configure which bus is used to dispatch the message using the ``message_bus`` option. +You can also set this to ``false`` to call the Mailer transport directly and +disable asynchronous delivery. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mailer.yaml + framework: + mailer: + message_bus: app.another_bus + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // config/packages/mailer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->mailer() + ->messageBus('app.another_bus'); + }; + +.. note:: + + In cases of long-running scripts, and when Mailer uses the + :class:`Symfony\\Component\\Mailer\\Transport\\Smtp\\SmtpTransport` + you may manually disconnect from the SMTP server to avoid keeping + an open connection to the SMTP server in between sending emails. + You can do so by using the ``stop()`` method. + +You can also select the transport by adding an ``X-Bus-Transport`` header (which +will be removed automatically from the final message):: + + // Use the bus transport "app.another_bus": + $email->getHeaders()->addTextHeader('X-Bus-Transport', 'app.another_bus'); + $mailer->send($email); + +Adding Tags and Metadata to Emails +---------------------------------- + +Certain 3rd party transports support email *tags* and *metadata*, which can be used +for grouping, tracking and workflows. You can add those by using the +:class:`Symfony\\Component\\Mailer\\Header\\TagHeader` and +:class:`Symfony\\Component\\Mailer\\Header\\MetadataHeader` classes. If your transport +supports headers, it will convert them to their appropriate format:: + + use Symfony\Component\Mailer\Header\MetadataHeader; + use Symfony\Component\Mailer\Header\TagHeader; + + $email->getHeaders()->add(new TagHeader('password-reset')); + $email->getHeaders()->add(new MetadataHeader('Color', 'blue')); + $email->getHeaders()->add(new MetadataHeader('Client-ID', '12345')); + +If your transport does not support tags and metadata, they will be added as custom headers: + +.. code-block:: text + + X-Tag: password-reset + X-Metadata-Color: blue + X-Metadata-Client-ID: 12345 + +The following transports currently support tags and metadata: + +* Brevo +* Mailgun +* Mailtrap +* Mandrill +* Postmark +* Sendgrid + +The following transports only support tags: + +* MailPace +* Resend + +The following transports only support metadata: + +* Amazon SES (note that Amazon refers to this feature as "tags", but Symfony + calls it "metadata" because it contains a key and a value) + +Draft Emails +------------ + +:class:`Symfony\\Component\\Mime\\DraftEmail` is a special instance of +:class:`Symfony\\Component\\Mime\\Email`. Its purpose is to build up an email +(with body, attachments, etc) and make available to download as an ``.eml`` with +the ``X-Unsent`` header. Many email clients can open these files and interpret +them as *draft emails*. You can use these to create advanced ``mailto:`` links. + +Here's an example of making one available to download:: + + // src/Controller/DownloadEmailController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpFoundation\ResponseHeaderBag; + use Symfony\Component\Mime\DraftEmail; + use Symfony\Component\Routing\Attribute\Route; + + class DownloadEmailController extends AbstractController + { + #[Route('/download-email')] + public function __invoke(): Response + { + $message = (new DraftEmail()) + ->html($this->renderView(/* ... */)) + ->addPart(/* ... */) + ; + + $response = new Response($message->toString()); + $contentDisposition = $response->headers->makeDisposition( + ResponseHeaderBag::DISPOSITION_ATTACHMENT, + 'download.eml' + ); + $response->headers->set('Content-Type', 'message/rfc822'); + $response->headers->set('Content-Disposition', $contentDisposition); + + return $response; + } + } + +.. note:: + + As it's possible for :class:`Symfony\\Component\\Mime\\DraftEmail`'s to be created + without a To/From they cannot be sent with the mailer. + +Mailer Events +------------- + +MessageEvent +~~~~~~~~~~~~ + +**Event Class**: :class:`Symfony\\Component\\Mailer\\Event\\MessageEvent` + +``MessageEvent`` allows to change the Mailer message and the envelope before +the email is sent:: + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\Mailer\Event\MessageEvent; + use Symfony\Component\Mime\Email; + + public function onMessage(MessageEvent $event): void + { + $message = $event->getMessage(); + if (!$message instanceof Email) { + return; + } + // do something with the message (logging, ...) + + // and/or add some Messenger stamps + $event->addStamp(new SomeMessengerStamp()); + } + +If you want to stop the Message from being sent, call ``reject()`` (it will +also stop the event propagation):: + + use Symfony\Component\Mailer\Event\MessageEvent; + + public function onMessage(MessageEvent $event): void + { + $event->reject(); + } + +Execute this command to find out which listeners are registered for this event +and their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\MessageEvent" + +.. _mailer-sent-message-event: + +SentMessageEvent +~~~~~~~~~~~~~~~~ + +**Event Class**: :class:`Symfony\\Component\\Mailer\\Event\\SentMessageEvent` + +``SentMessageEvent`` allows you to act on the :class:`Symfony\\Component\\\Mailer\\\SentMessage` +class to access the original message (``getOriginalMessage()``) and some +:ref:`debugging information ` (``getDebug()``) such as +the HTTP calls made by the HTTP transports, which is useful for debugging errors:: + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\Mailer\Event\SentMessageEvent; + + public function onMessage(SentMessageEvent $event): void + { + $message $event->getMessage(); + + // do something with the message (e.g. get its id) + } + +Execute this command to find out which listeners are registered for this event +and their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\SentMessageEvent" + +.. _mailer-failed-message-event: + +FailedMessageEvent +~~~~~~~~~~~~~~~~~~ + +**Event Class**: :class:`Symfony\\Component\\Mailer\\Event\\FailedMessageEvent` + +``FailedMessageEvent`` allows acting on the initial message in case of a failure +and some :ref:`debugging information ` (``getDebug()``) +such as the HTTP calls made by the HTTP transports, which is useful for debugging errors:: + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\Mailer\Event\FailedMessageEvent; + use Symfony\Component\Mailer\Exception\TransportExceptionInterface; + + public function onMessage(FailedMessageEvent $event): void + { + // e.g you can get more information on this error when sending an email + $error = $event->getError(); + if ($error instanceof TransportExceptionInterface) { + $error->getDebug(); + } + + // do something with the message + } + +Execute this command to find out which listeners are registered for this event +and their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\FailedMessageEvent" + +Development & Debugging +----------------------- + +.. _mail-catcher: + +Enabling an Email Catcher +~~~~~~~~~~~~~~~~~~~~~~~~~ + +When developing locally, it is recommended to use an email catcher. If you have +enabled Docker support via Symfony recipes, an email catcher is automatically +configured. In addition, if you are using the :doc:`Symfony local web server +`, the mailer DSN is automatically exposed via the +:ref:`symfony binary Docker integration `. + +Sending Test Emails +~~~~~~~~~~~~~~~~~~~ + +Symfony provides a command to send emails, which is useful during development +to test if sending emails works correctly: + +.. code-block:: terminal + + # the only mandatory argument is the recipient address + # (check the command help to learn about its options) + $ php bin/console mailer:test someone@example.com + +This command bypasses the :doc:`Messenger bus `, if configured, to +ease testing emails even when the Messenger consumer is not running. + +Disabling Delivery +~~~~~~~~~~~~~~~~~~ + +While developing (or testing), you may want to disable delivery of messages +entirely. You can do this by using ``null://null`` as the mailer DSN, either in +your :ref:`.env configuration files ` or in +the mailer configuration file (e.g. in the ``dev`` or ``test`` environments): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mailer.yaml + when@dev: + framework: + mailer: + dsn: 'null://null' + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // config/packages/mailer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + $framework->mailer() + ->dsn('null://null'); + }; + +.. note:: + + If you're using Messenger and routing to a transport, the message will *still* + be sent to that transport. + +Always Send to the same Address +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Instead of disabling delivery entirely, you might want to *always* send emails to +a specific address, instead of the *real* address: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mailer.yaml + when@dev: + framework: + mailer: + envelope: + recipients: ['youremail@example.com'] + + .. code-block:: xml + + + + + + + + + + youremail@example.com + + + + + + .. code-block:: php + + // config/packages/mailer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + $framework->mailer() + ->envelope() + ->recipients(['youremail@example.com']) + ; + }; + +Use the ``allowed_recipients`` option to specify exceptions to the behavior defined +in the ``recipients`` option; allowing emails directed to these specific recipients +to maintain their original destination: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mailer.yaml + when@dev: + framework: + mailer: + envelope: + recipients: ['youremail@example.com'] + allowed_recipients: + - 'internal@example.com' + # you can also use regular expression to define allowed recipients + - 'internal-.*@example.(com|fr)' + + .. code-block:: xml + + + + + + + + + + youremail@example.com + internal@example.com + + internal-.*@example.(com|fr) + + + + + + .. code-block:: php + + // config/packages/mailer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + $framework->mailer() + ->envelope() + ->recipients(['youremail@example.com']) + ->allowedRecipients([ + 'internal@example.com', + // you can also use regular expression to define allowed recipients + 'internal-.*@example.(com|fr)', + ]) + ; + }; + +With this configuration, all emails will be sent to ``youremail@example.com``, +except for those sent to ``internal@example.com``, ``internal-monitoring@example.fr``, +etc., which will receive emails as usual. + +.. versionadded:: 7.1 + + The ``allowed_recipients`` option was introduced in Symfony 7.1. + +Write a Functional Test +~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides lots of :ref:`built-in mailer assertions ` +to functionally test that an email was sent, its contents or headers, etc. +They are available in test classes extending +:class:`Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase` or when using +the :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\MailerAssertionsTrait`:: + + // tests/Controller/MailControllerTest.php + namespace App\Tests\Controller; + + use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + + class MailControllerTest extends WebTestCase + { + public function testMailIsSentAndContentIsOk(): void + { + $client = static::createClient(); + $client->request('GET', '/mail/send'); + $this->assertResponseIsSuccessful(); + + $this->assertEmailCount(1); // use assertQueuedEmailCount() when using Messenger + + $email = $this->getMailerMessage(); + + $this->assertEmailHtmlBodyContains($email, 'Welcome'); + $this->assertEmailTextBodyContains($email, 'Welcome'); + } + } + +.. tip:: + + If your controller returns a redirect response after sending the email, make + sure to have your client *not* follow redirects. The kernel is rebooted after + following the redirection and the message will be lost from the mailer event + handler. + +.. _`AhaSend`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/AhaSend/README.md +.. _`Amazon SES`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Amazon/README.md +.. _`Azure`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Azure/README.md +.. _`App Password`: https://support.google.com/accounts/answer/185833 +.. _`Brevo`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Brevo/README.md +.. _`default_socket_timeout`: https://www.php.net/manual/en/filesystem.configuration.php#ini.default-socket-timeout +.. _`DKIM`: https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail +.. _`download the foundation-emails.css file`: https://github.com/foundation/foundation-emails/blob/develop/dist/foundation-emails.css +.. _`Google Gmail`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Google/README.md +.. _`high availability`: https://en.wikipedia.org/wiki/High_availability +.. _`Infobip`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Infobip/README.md +.. _`Inky`: https://get.foundation/emails/docs/inky.html +.. _`league/html-to-markdown`: https://github.com/thephpleague/html-to-markdown +.. _`load balancing`: https://en.wikipedia.org/wiki/Load_balancing_(computing) +.. _`MailerSend`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/MailerSend/README.md +.. _`Mandrill`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailchimp/README.md +.. _`Mailgun`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailgun/README.md +.. _`Mailjet`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailjet/README.md +.. _`Markdown syntax`: https://commonmark.org/ +.. _`Mailomat`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailomat/README.md +.. _`MailPace`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/MailPace/README.md +.. _`OpenSSL PHP extension`: https://www.php.net/manual/en/book.openssl.php +.. _`PEM encoded`: https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail +.. _`Postal`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Postal/README.md +.. _`Postmark`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Postmark/README.md +.. _`Mailtrap`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailtrap/README.md +.. _`Resend`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Resend/README.md +.. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt +.. _`S/MIME`: https://en.wikipedia.org/wiki/S/MIME +.. _`Scaleway`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Scaleway/README.md +.. _`SendGrid`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md +.. _`Sweego`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Sweego/README.md diff --git a/mercure.rst b/mercure.rst new file mode 100644 index 00000000000..c2aa068167d --- /dev/null +++ b/mercure.rst @@ -0,0 +1,784 @@ +Pushing Data to Clients Using the Mercure Protocol +================================================== + +Being able to broadcast data in real-time from servers to clients is a +requirement for many modern web and mobile applications. + +Creating a UI reacting in live to changes made by other users +(e.g. a user changes the data currently browsed by several other users, +all UIs are instantly updated), +notifying the user when :doc:`an asynchronous job ` has been +completed or creating chat applications are among the typical use cases +requiring "push" capabilities. + +Symfony provides a straightforward component, built on top of +`the Mercure protocol`_, specifically designed for this class of use cases. + +Mercure is an open protocol designed from the ground up to publish updates from +server to clients. It is a modern and efficient alternative to timer-based +polling and to WebSocket. + +Because it is built on top `Server-Sent Events (SSE)`_, Mercure is supported +out of the box in modern browsers (old versions of Edge and IE require +`a polyfill`_) and has `high-level implementations`_ in many programming +languages. + +Mercure comes with an authorization mechanism, +automatic reconnection in case of network issues +with retrieving of lost updates, a presence API, +"connection-less" push for smartphones and auto-discoverability (a supported +client can automatically discover and subscribe to updates of a given resource +thanks to a specific HTTP header). + +All these features are supported in the Symfony integration. + +`In this recording`_ you can see how a Symfony web API leverages Mercure +and API Platform to update in live a React app and a mobile app (React Native) +generated using the API Platform client generator. + +Installation +------------ + +Installing the Symfony Bundle +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Run this command to install the Mercure support: + +.. code-block:: terminal + + $ composer require mercure + +Running a Mercure Hub +~~~~~~~~~~~~~~~~~~~~~ + +To manage persistent connections, Mercure relies on a Hub: a dedicated server +that handles persistent SSE connections with the clients. +The Symfony app publishes the updates to the hub, that will broadcast them to +clients. + +.. raw:: html + + + +In production, you have to install a Mercure hub by yourself. +An official and open source (AGPL) hub based on the Caddy web server +can be downloaded as a static binary from `Mercure.rocks`_. +A Docker image, a Helm chart for Kubernetes +and a managed, High Availability Hub are also provided. + +Thanks to :doc:`the Docker integration of Symfony `, +:ref:`Flex ` proposes to install a Mercure hub for development. +Run ``docker-compose up`` to start the hub if you have chosen this option. + +If you use the :doc:`Symfony Local Web Server `, +you must start it with the ``--no-tls`` option. + +.. code-block:: terminal + + $ symfony server:start --no-tls -d + +If you use the Docker integration, a hub is already up and running. + +Configuration +------------- + +The preferred way to configure MercureBundle is using +:doc:`environment variables `. + +When MercureBundle has been installed, the ``.env`` file of your project +has been updated by the Flex recipe to include the available env vars. + +Also, if you are using the Docker integration with the Symfony Local Web Server, +`Symfony Docker`_ or the `API Platform distribution`_, +the proper environment variables have been automatically set. +Skip straight to the next section. + +Otherwise, set the URL of your hub as the value of the ``MERCURE_URL`` +and ``MERCURE_PUBLIC_URL`` env vars. +Sometimes a different URL must be called by the Symfony app (usually to publish), +and the JavaScript client (usually to subscribe). It's especially common when +the Symfony app must use a local URL and the client-side JavaScript code a public one. +In this case, ``MERCURE_URL`` must contain the local URL used by the +Symfony app (e.g. ``https://mercure/.well-known/mercure``), and ``MERCURE_PUBLIC_URL`` +the publicly available URL (e.g. ``https://example.com/.well-known/mercure``). + +The clients must also bear a `JSON Web Token`_ (JWT) +to the Mercure Hub to be authorized to publish updates and, sometimes, to subscribe. + +This token must be signed with the same secret key as the one used by the Hub to verify the JWT (``!ChangeThisMercureHubJWTSecretKey!`` if you use the Docker integration). +This secret key must be stored in the ``MERCURE_JWT_SECRET`` environment variable. +MercureBundle will use it to automatically generate and sign the needed JWTs. + +In addition to these environment variables, +MercureBundle provides a more advanced configuration: + +* ``secret``: the key to use to sign the JWT - A key of the same size as the hash output (for instance, 256 bits for "HS256") or larger MUST be used. (all other options, beside ``algorithm``, ``subscribe``, and ``publish`` will be ignored) +* ``publish``: a list of topics to allow publishing to when generating the JWT (only usable when ``secret``, or ``factory`` are provided) +* ``subscribe``: a list of topics to allow subscribing to when generating the JWT (only usable when ``secret``, or ``factory`` are provided) +* ``algorithm``: The algorithm to use to sign the JWT (only usable when ``secret`` is provided) +* ``provider``: The ID of a service to call to provide the JWT (all other options will be ignored) +* ``factory``: The ID of a service to call to create the JWT (all other options, beside ``subscribe``, and ``publish`` will be ignored) +* ``value``: the raw JWT to use (all other options will be ignored) + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mercure.yaml + mercure: + hubs: + default: + url: '%env(string:MERCURE_URL)%' + public_url: '%env(string:MERCURE_PUBLIC_URL)%' + jwt: + secret: '%env(string:MERCURE_JWT_SECRET)%' + publish: ['https://example.com/foo1', 'https://example.com/foo2'] + subscribe: ['https://example.com/bar1', 'https://example.com/bar2'] + algorithm: 'hmac.sha256' + provider: 'My\Provider' + factory: 'My\Factory' + value: 'my.jwt' + + .. code-block:: xml + + + + + + + https://example.com/foo1 + https://example.com/foo2 + https://example.com/bar1 + https://example.com/bar2 + + + + + .. code-block:: php + + // config/packages/mercure.php + $container->loadFromExtension('mercure', [ + 'hubs' => [ + 'default' => [ + 'url' => '%env(string:MERCURE_URL)%', + 'public_url' => '%env(string:MERCURE_PUBLIC_URL)%', + 'jwt' => [ + 'secret' => '%env(string:MERCURE_JWT_SECRET)%', + 'publish' => ['https://example.com/foo1', 'https://example.com/foo2'], + 'subscribe' => ['https://example.com/bar1', 'https://example.com/bar2'], + 'algorithm' => 'hmac.sha256', + 'provider' => 'My\Provider', + 'factory' => 'My\Factory', + 'value' => 'my.jwt', + ], + ], + ], + ]); + +.. tip:: + + The JWT payload must contain at least the following structure for the client to be allowed to + publish: + + .. code-block:: json + + { + "mercure": { + "publish": ["*"] + } + } + + The jwt.io website is a convenient way to create and sign JWTs, checkout this `example JWT`_. + Don't forget to set your secret key properly in the bottom of the right panel of the form! + +Basic Usage +----------- + +Publishing +~~~~~~~~~~ + +The Mercure Component provides an ``Update`` value object representing +the update to publish. It also provides a ``Publisher`` service to dispatch +updates to the Hub. + +The ``Publisher`` service can be injected using the +:doc:`autowiring ` in any other +service, including controllers:: + + // src/Controller/PublishController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Mercure\HubInterface; + use Symfony\Component\Mercure\Update; + + class PublishController extends AbstractController + { + public function publish(HubInterface $hub): Response + { + $update = new Update( + 'https://example.com/books/1', + json_encode(['status' => 'OutOfStock']) + ); + + $hub->publish($update); + + return new Response('published!'); + } + } + +The first parameter to pass to the ``Update`` constructor is +the **topic** being updated. This topic should be an `IRI`_ +(Internationalized Resource Identifier, RFC 3987): a unique identifier +of the resource being dispatched. + +Usually, this parameter contains the original URL of the resource +transmitted to the client, but it can be any string or `IRI`_, +and it doesn't have to be a URL that exists (similarly to XML namespaces). + +The second parameter of the constructor is the content of the update. +It can be anything, stored in any format. +However, serializing the resource in a hypermedia format such as JSON-LD, +Atom, HTML or XML is recommended. + +Subscribing +~~~~~~~~~~~ + +Subscribing to updates in JavaScript from a Twig template is straightforward: + +.. code-block:: html+twig + + + +The ``mercure()`` Twig function generates the URL of the Mercure hub +according to the configuration. The URL includes the ``topic`` query +parameters corresponding to the topics passed as first argument. + +If you want to access to this URL from an external JavaScript file, generate the +URL in a dedicated HTML element: + +.. code-block:: html+twig + + + + +
          + +Then retrieve it from your JS file: + +.. code-block:: javascript + + const url = JSON.parse(document.getElementById("mercure-url").textContent); + const eventSource = new EventSource(url); + // ... + + // with Stimulus + this.eventSource = new EventSource(this.mercureUrlValue); + +Mercure also allows subscribing to several topics, +and to use URI Templates or the special value ``*`` (matched by all topics) +as patterns: + +.. code-block:: html+twig + + + +However, on the client side (i.e. in JavaScript's ``EventSource``), there is no +built-in way to know which topic a certain message originates from. If this (or +any other meta information) is important to you, you need to include it in the +message's data (e.g. by adding a key to the JSON, or a ``data-*`` attribute to +the HTML). + +.. tip:: + + Test if a URI Template matches a URL using `the online debugger`_ + +.. tip:: + + Google Chrome features a practical UI to display the received events: + + .. image:: /_images/mercure/chrome.png + :alt: The Chrome DevTools showing the EventStream tab containing information about each SSE event. + + In DevTools, select the "Network" tab, then click on the request to the Mercure hub, then on the "EventStream" sub-tab. + +Discovery +--------- + +The Mercure protocol comes with a discovery mechanism. +To leverage it, the Symfony application must expose the URL of the Mercure Hub +in a ``Link`` HTTP header. + +.. raw:: html + + + +You can create ``Link`` headers with the ``Discovery`` helper class +(under the hood, it uses the :doc:`WebLink Component `):: + + // src/Controller/DiscoverController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\JsonResponse; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Mercure\Discovery; + + class DiscoverController extends AbstractController + { + public function discover(Request $request, Discovery $discovery): JsonResponse + { + // Link: ; rel="mercure" + $discovery->addLink($request); + + return $this->json([ + '@id' => '/books/1', + 'availability' => 'https://schema.org/InStock', + ]); + } + } + +Then, this header can be parsed client-side to find the URL of the Hub, +and to subscribe to it: + +.. code-block:: javascript + + // Fetch the original resource served by the Symfony web API + fetch('/books/1') // Has Link: ; rel="mercure" + .then(response => { + // Extract the hub URL from the Link header + const hubUrl = response.headers.get('Link').match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1]; + + // Append the topic(s) to subscribe as query parameter + const hub = new URL(hubUrl, window.origin); + hub.searchParams.append('topic', 'https://example.com/books/{id}'); + + // Subscribe to updates + const eventSource = new EventSource(hub); + eventSource.onmessage = event => console.log(event.data); + }); + +Authorization +------------- + +Mercure also allows dispatching updates only to authorized clients. +To do so, mark the update as **private** by setting the third parameter +of the ``Update`` constructor to ``true``:: + + // src/Controller/Publish.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Mercure\Update; + + class PublishController extends AbstractController + { + public function publish(HubInterface $hub): Response + { + $update = new Update( + 'https://example.com/books/1', + json_encode(['status' => 'OutOfStock']), + true // private + ); + + // Publisher's JWT must contain this topic, a URI template it matches or * in mercure.publish or you'll get a 401 + // Subscriber's JWT must contain this topic, a URI template it matches or * in mercure.subscribe to receive the update + $hub->publish($update); + + return new Response('private update published!'); + } + } + +To subscribe to private updates, subscribers must provide to the Hub +a JWT containing a topic selector matching by the topic of the update. + +To provide this JWT, the subscriber can use a cookie, +or an ``Authorization`` HTTP header. + +Cookies can be set automatically by Symfony by passing the appropriate options +to the ``mercure()`` Twig function. Cookies set by Symfony are automatically +passed by the browsers to the Mercure hub if the ``withCredentials`` attribute +of the ``EventSource`` class is set to ``true``. Then, the Hub verifies the +validity of the provided JWT, and extract the topic selectors from it. + +.. code-block:: html+twig + + + +The supported options are: + +* ``subscribe``: the list of topic selectors to include in the ``mercure.subscribe`` claim of the JWT +* ``publish``: the list of topic selectors to include in the ``mercure.publish`` claim of the JWT +* ``additionalClaims``: extra claims to include in the JWT (expiration date, token ID...) + +Using cookies is the most secure and preferred way when the client is a web +browser. If the client is not a web browser, then using an authorization header +is the way to go. + +.. warning:: + + To use the cookie authentication method, the Symfony app and the Hub + must be served from the same domain (can be different sub-domains). + +.. tip:: + + The native implementation of EventSource doesn't allow specifying headers. + For example, authorization using a Bearer token. In order to achieve that, use `a polyfill`_ + + .. code-block:: html+twig + + + +Programmatically Setting The Cookie +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes, it can be convenient to set the authorization cookie from your code +instead of using the Twig function. MercureBundle provides a convenient service, +``Authorization``, to do so. + +In the following example controller, the added cookie contains a JWT, itself +containing the appropriate topic selector. + +And here is the controller:: + + // src/Controller/DiscoverController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\JsonResponse; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Mercure\Authorization; + use Symfony\Component\Mercure\Discovery; + + class DiscoverController extends AbstractController + { + public function publish(Request $request, Discovery $discovery, Authorization $authorization): JsonResponse + { + $discovery->addLink($request); + $authorization->setCookie($request, ['https://example.com/books/1']); + + return $this->json([ + '@id' => '/demo/books/1', + 'availability' => 'https://schema.org/InStock' + ]); + } + } + +.. tip:: + + You cannot use the ``mercure()`` helper and the ``setCookie()`` + method at the same time (it would set the cookie twice on a single request). Choose + either one method or the other. + +Programmatically Generating The JWT Used to Publish +--------------------------------------------------- + +Instead of directly storing a JWT in the configuration, +you can create a token provider that will return the token used by +the ``HubInterface`` object:: + + // src/Mercure/MyTokenProvider.php + namespace App\Mercure; + + use Symfony\Component\Mercure\Jwt\TokenProviderInterface; + + final class MyTokenProvider implements TokenProviderInterface + { + public function getJwt(): string + { + return 'the-JWT'; + } + } + +Then, reference this service in the bundle configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mercure.yaml + mercure: + hubs: + default: + url: https://mercure-hub.example.com/.well-known/mercure + jwt: + provider: App\Mercure\MyTokenProvider + + .. code-block:: xml + + + + + + + + + + .. code-block:: php + + // config/packages/mercure.php + use App\Mercure\MyJwtProvider; + + $container->loadFromExtension('mercure', [ + 'hubs' => [ + 'default' => [ + 'url' => 'https://mercure-hub.example.com/.well-known/mercure', + 'jwt' => [ + 'provider' => MyJwtProvider::class, + ], + ], + ], + ]); + +This method is especially convenient when using tokens having an expiration +date, that can be refreshed programmatically. + +Web APIs +-------- + +When creating a web API, it's convenient to be able to instantly push +new versions of the resources to all connected devices, and to update +their views. + +API Platform can use the Mercure Component to dispatch updates automatically, +every time an API resource is created, modified or deleted. + +Start by installing the library using its official recipe: + +.. code-block:: terminal + + $ composer require api + +Then, creating the following entity is enough to get a fully-featured +hypermedia API, and automatic update broadcasting through the Mercure hub:: + + // src/Entity/Book.php + namespace App\Entity; + + use ApiPlatform\Core\Annotation\ApiResource; + use Doctrine\ORM\Mapping as ORM; + + #[ApiResource(mercure: true)] + #[ORM\Entity] + class Book + { + #[ORM\Id] + #[ORM\Column] + public string $name = ''; + + #[ORM\Column] + public string $status = ''; + } + +As showcased `in this recording`_, the API Platform Client Generator also +allows to scaffold complete React and React Native applications from this API. +These applications will render the content of Mercure updates in real-time. + +Checkout `the dedicated API Platform documentation`_ to learn more about +its Mercure support. + +Testing +------- + +During unit testing it's usually not needed to send updates to Mercure. + +You can instead make use of the ``MockHub`` class:: + + // tests/FunctionalTest.php + namespace App\Tests\Unit\Controller; + + use App\Controller\MessageController; + use Symfony\Component\Mercure\HubInterface; + use Symfony\Component\Mercure\JWT\StaticTokenProvider; + use Symfony\Component\Mercure\MockHub; + use Symfony\Component\Mercure\Update; + + class MessageControllerTest extends TestCase + { + public function testPublishing(): void + { + $hub = new MockHub('https://internal/.well-known/mercure', new StaticTokenProvider('foo'), function(Update $update): string { + // $this->assertTrue($update->isPrivate()); + + return 'id'; + }); + + $controller = new MessageController($hub); + + // ... + } + } + +For functional testing, you can instead create a stub of the Hub:: + + // tests/Functional/Stub/HubStub.php + namespace App\Tests\Functional\Stub; + + use Symfony\Component\Mercure\HubInterface; + use Symfony\Component\Mercure\Update; + + class HubStub implements HubInterface + { + public function publish(Update $update): string + { + return 'id'; + } + + // implement rest of HubInterface methods here + } + +Use ``HubStub`` to replace the default hub service so no updates are actually +sent: + +.. code-block:: yaml + + # config/services_test.yaml + services: + mercure.hub.default: + class: App\Tests\Functional\Stub\HubStub + +As MercureBundle supports multiple hubs, you may have to replace +the other service definitions accordingly. + +.. tip:: + + Symfony Panther has `a feature to test applications using Mercure`_. + +Debugging +--------- + +.. versionadded:: 0.2 + + The WebProfiler panel was introduced in MercureBundle 0.2. + +MercureBundle is shipped with a debug panel. Install the Debug pack to +enable it:: + +.. code-block:: terminal + + $ composer require --dev symfony/debug-pack + +.. image:: /_images/mercure/panel.png + :alt: The Mercure panel of the Symfony Profiler, showing information like time, memory, topics and data of each message sent by Mercure. + :class: with-browser + +The Mercure hub itself provides a debug tool that can be enabled and it's +available on ``/.well-known/mercure/ui/`` + +Async dispatching +----------------- + +.. tip:: + + Async dispatching is discouraged. Most Mercure hubs already + handle publications asynchronously and using Messenger is + usually not necessary. + +Instead of calling the ``Publisher`` service directly, you can also let Symfony +dispatching the updates asynchronously thanks to the provided integration with +the Messenger component. + +First, be sure :doc:`to install the Messenger component ` +and to configure properly a transport (if you don't, the handler will +be called synchronously). + +Then, dispatch the Mercure ``Update`` to the Messenger's Message Bus, +it will be handled automatically:: + + // src/Controller/PublishController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Mercure\Update; + use Symfony\Component\Messenger\MessageBusInterface; + + class PublishController extends AbstractController + { + public function publish(MessageBusInterface $bus): Response + { + $update = new Update( + 'https://example.com/books/1', + json_encode(['status' => 'OutOfStock']) + ); + + // Sync, or async (Doctrine, RabbitMQ, Kafka...) + $bus->dispatch($update); + + return new Response('published!'); + } + } + +Going further +------------- + +* The Mercure protocol is also supported by :doc:`the Notifier component `. + Use it to send push notifications to web browsers. +* `Symfony UX Turbo`_ is a library using Mercure to provide the same experience + as with Single Page Applications but without having to write a single line of JavaScript! + +.. _`the Mercure protocol`: https://mercure.rocks/spec +.. _`Server-Sent Events (SSE)`: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events +.. _`a polyfill`: https://github.com/Yaffle/EventSource +.. _`high-level implementations`: https://mercure.rocks/docs/ecosystem/awesome +.. _`In this recording`: https://www.youtube.com/watch?v=UI1l0JOjLeI +.. _`Mercure.rocks`: https://mercure.rocks +.. _`Symfony Docker`: https://github.com/dunglas/symfony-docker/ +.. _`API Platform distribution`: https://api-platform.com/docs/distribution/ +.. _`JSON Web Token`: https://tools.ietf.org/html/rfc7519 +.. _`example JWT`: https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.iHLdpAEjX4BqCsHJEegxRmO-Y6sMxXwNATrQyRNt3GY +.. _`IRI`: https://tools.ietf.org/html/rfc3987 +.. _`the dedicated API Platform documentation`: https://api-platform.com/docs/core/mercure/ +.. _`the online debugger`: https://uri-template-tester.mercure.rocks +.. _`a feature to test applications using Mercure`: https://github.com/symfony/panther#creating-isolated-browsers-to-test-apps-using-mercure-or-websocket +.. _`Symfony UX Turbo`: https://github.com/symfony/ux-turbo diff --git a/messenger.rst b/messenger.rst new file mode 100644 index 00000000000..9078a500d11 --- /dev/null +++ b/messenger.rst @@ -0,0 +1,3763 @@ +Messenger: Sync & Queued Message Handling +========================================= + +Messenger provides a message bus with the ability to send messages and then +handle them immediately in your application or send them through transports +(e.g. queues) to be handled later. To learn more deeply about it, read the +:doc:`Messenger component docs `. + +Installation +------------ + +In applications using :ref:`Symfony Flex `, run this command to +install messenger: + +.. code-block:: terminal + + $ composer require symfony/messenger + +Creating a Message & Handler +---------------------------- + +Messenger centers around two different classes that you'll create: (1) a message +class that holds data and (2) a handler(s) class that will be called when that +message is dispatched. The handler class will read the message class and perform +one or more tasks. + +There are no specific requirements for a message class, except that it can be +serialized:: + + // src/Message/SmsNotification.php + namespace App\Message; + + class SmsNotification + { + public function __construct( + private string $content, + ) { + } + + public function getContent(): string + { + return $this->content; + } + } + +.. _messenger-handler: + +A message handler is a PHP callable, the recommended way to create it is to +create a class that has the :class:`Symfony\\Component\\Messenger\\Attribute\\AsMessageHandler` +attribute and has an ``__invoke()`` method that's type-hinted with the +message class (or a message interface):: + + // src/MessageHandler/SmsNotificationHandler.php + namespace App\MessageHandler; + + use App\Message\SmsNotification; + use Symfony\Component\Messenger\Attribute\AsMessageHandler; + + #[AsMessageHandler] + class SmsNotificationHandler + { + public function __invoke(SmsNotification $message) + { + // ... do some work - like sending an SMS message! + } + } + +.. tip:: + + You can also use the ``#[AsMessageHandler]`` attribute on individual class + methods. You may use the attribute on as many methods in a single class as you + like, allowing you to group the handling of multiple related types of messages. + +Thanks to :ref:`autoconfiguration ` and the ``SmsNotification`` +type-hint, Symfony knows that this handler should be called when an ``SmsNotification`` +message is dispatched. Most of the time, this is all you need to do. But you can +also :ref:`manually configure message handlers `. To +see all the configured handlers, run: + +.. code-block:: terminal + + $ php bin/console debug:messenger + +Dispatching the Message +----------------------- + +You're ready! To dispatch the message (and call the handler), inject the +``messenger.default_bus`` service (via the ``MessageBusInterface``), like in a controller:: + + // src/Controller/DefaultController.php + namespace App\Controller; + + use App\Message\SmsNotification; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Messenger\MessageBusInterface; + + class DefaultController extends AbstractController + { + public function index(MessageBusInterface $bus): Response + { + // will cause the SmsNotificationHandler to be called + $bus->dispatch(new SmsNotification('Look! I created a message!')); + + // ... + } + } + +Transports: Async/Queued Messages +--------------------------------- + +By default, messages are handled as soon as they are dispatched. If you want +to handle a message asynchronously, you can configure a transport. A transport +is capable of sending messages (e.g. to a queueing system) and then +:ref:`receiving them via a worker `. Messenger supports +:ref:`multiple transports `. + +.. note:: + + If you want to use a transport that's not supported, check out the + `Enqueue's transport`_, which backs services like Kafka and Google + Pub/Sub. + +A transport is registered using a "DSN". Thanks to Messenger's Flex recipe, your +``.env`` file already has a few examples. + +.. code-block:: env + + # MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages + # MESSENGER_TRANSPORT_DSN=doctrine://default + # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages + +Uncomment whichever transport you want (or set it in ``.env.local``). See +:ref:`messenger-transports-config` for more details. + +Next, in ``config/packages/messenger.yaml``, let's define a transport called ``async`` +that uses this configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + async: "%env(MESSENGER_TRANSPORT_DSN)%" + + # or expanded to configure more options + #async: + # dsn: "%env(MESSENGER_TRANSPORT_DSN)%" + # options: [] + + .. code-block:: xml + + + + + + + + %env(MESSENGER_TRANSPORT_DSN)% + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->messenger() + ->transport('async') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + ; + + $framework->messenger() + ->transport('async') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + ->options([]) + ; + }; + +.. _messenger-routing: + +Routing Messages to a Transport +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now that you have a transport configured, instead of handling a message immediately, +you can configure them to be sent to a transport: + +.. _messenger-message-attribute: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Message/SmsNotification.php + namespace App\Message; + + use Symfony\Component\Messenger\Attribute\AsMessage; + + #[AsMessage('async')] + class SmsNotification + { + // ... + } + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + async: "%env(MESSENGER_TRANSPORT_DSN)%" + + routing: + # async is whatever name you gave your transport above + 'App\Message\SmsNotification': async + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->messenger() + // async is whatever name you gave your transport above + ->routing('App\Message\SmsNotification')->senders(['async']) + ; + }; + +.. versionadded:: 7.2 + + The ``#[AsMessage]`` attribute was introduced in Symfony 7.2. + +Thanks to this, the ``App\Message\SmsNotification`` will be sent to the ``async`` +transport and its handler(s) will *not* be called immediately. Any messages not +matched under ``routing`` will still be handled immediately, i.e. synchronously. + +.. note:: + + If you configure routing with both YAML/XML/PHP configuration files and + PHP attributes, the configuration always takes precedence over the class + attribute. This behavior allows you to override routing on a per-environment basis. + +.. note:: + + When configuring the routing in separate YAML/XML/PHP files, you can use a partial + PHP namespace like ``'App\Message\*'`` to match all the messages within the + matching namespace. The only requirement is that the ``'*'`` wildcard has to + be placed at the end of the namespace. + + You may use ``'*'`` as the message class. This will act as a default routing + rule for any message not matched under ``routing``. This is useful to ensure + no message is handled synchronously by default. + + The only drawback is that ``'*'`` will also apply to the emails sent with the + Symfony Mailer (which uses ``SendEmailMessage`` when Messenger is available). + This could cause issues if your emails are not serializable (e.g. if they include + file attachments as PHP resources/streams). + +You can also route classes by their parent class or interface. Or send messages +to multiple transports: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Message/SmsNotification.php + namespace App\Message; + + use Symfony\Component\Messenger\Attribute\AsMessage; + + #[AsMessage(['async', 'audit'])] + class SmsNotification + { + // ... + } + + // if you prefer, you can also apply multiple attributes to the message class + #[AsMessage('async')] + #[AsMessage('audit')] + class SmsNotification + { + // ... + } + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + routing: + # route all messages that extend this example base class or interface + 'App\Message\AbstractAsyncMessage': async + 'App\Message\AsyncMessageInterface': async + + 'My\Message\ToBeSentToTwoSenders': [async, audit] + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + // route all messages that extend this example base class or interface + $messenger->routing('App\Message\AbstractAsyncMessage')->senders(['async']); + $messenger->routing('App\Message\AsyncMessageInterface')->senders(['async']); + $messenger->routing('My\Message\ToBeSentToTwoSenders')->senders(['async', 'audit']); + }; + +.. note:: + + If you configure routing for both a child and parent class, both rules + are used. E.g. if you have an ``SmsNotification`` object that extends + from ``Notification``, both the routing for ``Notification`` and + ``SmsNotification`` will be used. + +.. tip:: + + You can define and override the transport that a message is using at + runtime by using the + :class:`Symfony\\Component\\Messenger\\Stamp\\TransportNamesStamp` on + the envelope of the message. This stamp takes an array of transport + name as its only argument. For more information about stamps, see + `Envelopes & Stamps`_. + +Doctrine Entities in Messages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to pass a Doctrine entity in a message, it's better to pass the entity's +primary key (or whatever relevant information the handler actually needs, like ``email``, +etc.) instead of the object (otherwise you might see errors related to the Entity Manager):: + + // src/Message/NewUserWelcomeEmail.php + namespace App\Message; + + class NewUserWelcomeEmail + { + public function __construct( + private int $userId, + ) { + } + + public function getUserId(): int + { + return $this->userId; + } + } + +Then, in your handler, you can query for a fresh object:: + + // src/MessageHandler/NewUserWelcomeEmailHandler.php + namespace App\MessageHandler; + + use App\Message\NewUserWelcomeEmail; + use App\Repository\UserRepository; + use Symfony\Component\Messenger\Attribute\AsMessageHandler; + + #[AsMessageHandler] + class NewUserWelcomeEmailHandler + { + public function __construct( + private UserRepository $userRepository, + ) { + } + + public function __invoke(NewUserWelcomeEmail $welcomeEmail): void + { + $user = $this->userRepository->find($welcomeEmail->getUserId()); + + // ... send an email! + } + } + +This guarantees the entity contains fresh data. + +.. _messenger-handling-messages-synchronously: + +Handling Messages Synchronously +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a message doesn't :ref:`match any routing rules `, it won't +be sent to any transport and will be handled immediately. In some cases (like +when `binding handlers to different transports`_), +it's easier or more flexible to handle this explicitly: by creating a ``sync`` +transport and "sending" messages there to be handled immediately: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + # ... other transports + + sync: 'sync://' + + routing: + App\Message\SmsNotification: sync + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + // ... other transports + + $messenger->transport('sync')->dsn('sync://'); + $messenger->routing('App\Message\SmsNotification')->senders(['sync']); + }; + +Creating your Own Transport +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can also create your own transport if you need to send or receive messages +from something that is not supported. See :doc:`/messenger/custom-transport`. + +.. _messenger-worker: + +Consuming Messages (Running the Worker) +--------------------------------------- + +Once your messages have been routed, in most cases, you'll need to "consume" them. +You can do this with the ``messenger:consume`` command: + +.. code-block:: terminal + + $ php bin/console messenger:consume async + + # use -vv to see details about what's happening + $ php bin/console messenger:consume async -vv + +The first argument is the receiver's name (or service id if you routed to a +custom service). By default, the command will run forever: looking for new messages +on your transport and handling them. This command is called your "worker". + +If you want to consume messages from all available receivers, you can use the +command with the ``--all`` option: + +.. code-block:: terminal + + $ php bin/console messenger:consume --all + +.. versionadded:: 7.1 + + The ``--all`` option was introduced in Symfony 7.1. + +Messages that take a long time to process may be redelivered prematurely because +some transports assume that an unacknowledged message is lost. To prevent this +issue, use the ``--keepalive`` command option to specify an interval (in seconds; +default value = ``5``) at which the message is marked as "in progress". This prevents +the message from being redelivered until the worker completes processing it: + +.. code-block:: terminal + + $ php bin/console messenger:consume --keepalive + +.. note:: + + This option is only available for the following transports: Beanstalkd, AmazonSQS, Doctrine and Redis. + +.. versionadded:: 7.2 + + The ``--keepalive`` option was introduced in Symfony 7.2. + +.. tip:: + + In a development environment and if you're using the Symfony CLI tool, + you can configure workers to be automatically run along with the webserver. + You can find more information in the + :ref:`Symfony CLI Workers ` documentation. + +.. tip:: + + To properly stop a worker, throw an instance of + :class:`Symfony\\Component\\Messenger\\Exception\\StopWorkerException`. + +Deploying to Production +~~~~~~~~~~~~~~~~~~~~~~~ + +On production, there are a few important things to think about: + +**Use a Process Manager like Supervisor or systemd to keep your worker(s) running** + You'll want one or more "workers" running at all times. To do that, use a + process control system like :ref:`Supervisor ` + or :ref:`systemd `. + +**Don't Let Workers Run Forever** + Some services (like Doctrine's ``EntityManager``) will consume more memory + over time. So, instead of allowing your worker to run forever, use a flag + like ``messenger:consume --limit=10`` to tell your worker to only handle 10 + messages before exiting (then the process manager will create a new process). There + are also other options like ``--memory-limit=128M`` and ``--time-limit=3600``. + +**Stopping Workers That Encounter Errors** + If a worker dependency like your database server is down, or timeout is reached, + you can try to add :ref:`reconnect logic `, or just quit + the worker if it receives too many errors with the ``--failure-limit`` option of + the ``messenger:consume`` command. + +**Restart Workers on Deploy** + Each time you deploy, you'll need to restart all your worker processes so + that they see the newly deployed code. To do this, run ``messenger:stop-workers`` + on deployment. This will signal to each worker that it should finish the message + it's currently handling and should shut down gracefully. Then, the process manager + will create new worker processes. The command uses the :ref:`app ` + cache internally - so make sure this is configured to use an adapter you like. + +**Use the Same Cache Between Deploys** + If your deploy strategy involves the creation of new target directories, you + should set a value for the :ref:`cache.prefix_seed ` + configuration option in order to use the same cache namespace between deployments. + Otherwise, the ``cache.app`` pool will use the value of the ``kernel.project_dir`` + parameter as base for the namespace, which will lead to different namespaces + each time a new deployment is made. + +Prioritized Transports +~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes certain types of messages should have a higher priority and be handled +before others. To make this possible, you can create multiple transports and route +different messages to them. For example: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + async_priority_high: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + options: + # queue_name is specific to the doctrine transport + queue_name: high + + # for AMQP send to a separate exchange then queue + #exchange: + # name: high + #queues: + # messages_high: ~ + # for redis try "group" + async_priority_low: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + options: + queue_name: low + + routing: + 'App\Message\SmsNotification': async_priority_low + 'App\Message\NewUserWelcomeEmail': async_priority_high + + .. code-block:: xml + + + + + + + + + + + Queue + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $messenger->transport('async_priority_high') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + ->options(['queue_name' => 'high']); + + $messenger->transport('async_priority_low') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + ->options(['queue_name' => 'low']); + + $messenger->routing('App\Message\SmsNotification')->senders(['async_priority_low']); + $messenger->routing('App\Message\NewUserWelcomeEmail')->senders(['async_priority_high']); + }; + +You can then run individual workers for each transport or instruct one worker +to handle messages in a priority order: + +.. code-block:: terminal + + $ php bin/console messenger:consume async_priority_high async_priority_low + +The worker will always first look for messages waiting on ``async_priority_high``. If +there are none, *then* it will consume messages from ``async_priority_low``. + +.. _messenger-limit-queues: + +Limit Consuming to Specific Queues +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some transports (notably AMQP) have the concept of exchanges and queues. A Symfony +transport is always bound to an exchange. By default, the worker consumes from all +queues attached to the exchange of the specified transport. However, there are use +cases to want a worker to only consume from specific queues. + +You can limit the worker to only process messages from specific queue(s): + +.. code-block:: terminal + + $ php bin/console messenger:consume my_transport --queues=fasttrack + + # you can pass the --queues option more than once to process multiple queues + $ php bin/console messenger:consume my_transport --queues=fasttrack1 --queues=fasttrack2 + +.. note:: + + To allow using the ``queues`` option, the receiver must implement the + :class:`Symfony\\Component\\Messenger\\Transport\\Receiver\\QueueReceiverInterface`. + +.. _messenger-message-count: + +Checking the Number of Queued Messages Per Transport +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Run the ``messenger:stats`` command to know how many messages are in the "queues" +of some or all transports: + +.. code-block:: terminal + + # displays the number of queued messages in all transports + $ php bin/console messenger:stats + + # shows stats only for some transports + $ php bin/console messenger:stats my_transport_name other_transport_name + + # you can also output the stats in JSON format + $ php bin/console messenger:stats --format=json + $ php bin/console messenger:stats my_transport_name other_transport_name --format=json + +.. versionadded:: 7.2 + + The ``format`` option was introduced in Symfony 7.2. + +.. note:: + + In order for this command to work, the configured transport's receiver must implement + :class:`Symfony\\Component\\Messenger\\Transport\\Receiver\\MessageCountAwareInterface`. + +.. _messenger-supervisor: + +Supervisor Configuration +~~~~~~~~~~~~~~~~~~~~~~~~ + +Supervisor is a great tool to guarantee that your worker process(es) is +*always* running (even if it closes due to failure, hitting a message limit +or thanks to ``messenger:stop-workers``). You can install it on Ubuntu, for +example, via: + +.. code-block:: terminal + + $ sudo apt-get install supervisor + +Supervisor configuration files typically live in a ``/etc/supervisor/conf.d`` +directory. For example, you can create a new ``messenger-worker.conf`` file +there to make sure that 2 instances of ``messenger:consume`` are running at all +times: + +.. code-block:: ini + + ;/etc/supervisor/conf.d/messenger-worker.conf + [program:messenger-consume] + command=php /path/to/your/app/bin/console messenger:consume async --time-limit=3600 + user=ubuntu + numprocs=2 + startsecs=0 + autostart=true + autorestart=true + startretries=10 + process_name=%(program_name)s_%(process_num)02d + +Change the ``async`` argument to use the name of your transport (or transports) +and ``user`` to the Unix user on your server. + +.. warning:: + + During a deployment, something might be unavailable (e.g. the + database) causing the consumer to fail to start. In this situation, + Supervisor will try ``startretries`` number of times to restart the + command. Make sure to change this setting to avoid getting the command + in a FATAL state, which will never restart again. + + Each restart, Supervisor increases the delay by 1 second. For instance, if + the value is ``10``, it will wait 1 sec, 2 sec, 3 sec, etc. This gives the + service a total of 55 seconds to become available again. Increase the + ``startretries`` setting to cover the maximum expected downtime. + +If you use the Redis Transport, note that each worker needs a unique consumer +name to avoid the same message being handled by multiple workers. One way to +achieve this is to set an environment variable in the Supervisor configuration +file, which you can then refer to in ``messenger.yaml`` +(see the :ref:`Redis section ` below): + +.. code-block:: ini + + environment=MESSENGER_CONSUMER_NAME=%(program_name)s_%(process_num)02d + +Next, tell Supervisor to read your config and start your workers: + +.. code-block:: terminal + + $ sudo supervisorctl reread + + $ sudo supervisorctl update + + $ sudo supervisorctl start messenger-consume:* + + # If you deploy an update of your code, don't forget to restart your workers + # to run the new code + $ sudo supervisorctl restart messenger-consume:* + +See the `Supervisor docs`_ for more details. + +Graceful Shutdown +................. + +If you install the `PCNTL`_ PHP extension in your project, workers will handle +the ``SIGTERM`` or ``SIGINT`` POSIX signals to finish processing their current +message before terminating. + +However, you might prefer to use different POSIX signals for graceful shutdown. +You can override default ones by setting the ``framework.messenger.stop_worker_on_signals`` +configuration option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + stop_worker_on_signals: + - SIGTERM + - SIGINT + - SIGUSR1 + + .. code-block:: xml + + + + + + + + + SIGTERM + SIGINT + SIGUSR1 + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->messenger() + ->stopWorkerOnSignals(['SIGTERM', 'SIGINT', 'SIGUSR1']); + }; + +.. versionadded:: 7.3 + + Support for signals plain names in configuration was introduced in Symfony 7.3. + Previously, you had to use the numeric values of signals as defined by the + ``pcntl`` extension's `predefined constants`_. + +In some cases the ``SIGTERM`` signal is sent by Supervisor itself (e.g. stopping +a Docker container having Supervisor as its entrypoint). In these cases you +need to add a ``stopwaitsecs`` key to the program configuration (with a value +of the desired grace period in seconds) in order to perform a graceful shutdown: + +.. code-block:: ini + + [program:x] + stopwaitsecs=20 + +.. _messenger-systemd: + +Systemd Configuration +~~~~~~~~~~~~~~~~~~~~~ + +While Supervisor is a great tool, it has the disadvantage that you need system +access to run it. Systemd has become the standard on most Linux distributions, +and has a good alternative called *user services*. + +Systemd user service configuration files typically live in a ``~/.config/systemd/user`` +directory. For example, you can create a new ``messenger-worker.service`` file. Or a +``messenger-worker@.service`` file if you want more instances running at the same time: + +.. code-block:: ini + + [Unit] + Description=Symfony messenger-consume %i + + [Service] + ExecStart=php /path/to/your/app/bin/console messenger:consume async --time-limit=3600 + # for Redis, set a custom consumer name for each instance + Environment="MESSENGER_CONSUMER_NAME=symfony-%n-%i" + Restart=always + RestartSec=30 + + [Install] + WantedBy=default.target + +Now, tell systemd to enable and start one worker: + +.. code-block:: terminal + + $ systemctl --user enable messenger-worker@1.service + $ systemctl --user start messenger-worker@1.service + + # to enable and start 20 workers + $ systemctl --user enable messenger-worker@{1..20}.service + $ systemctl --user start messenger-worker@{1..20}.service + +If you change your service config file, you need to reload the daemon: + +.. code-block:: terminal + + $ systemctl --user daemon-reload + +To restart all your consumers: + +.. code-block:: terminal + + $ systemctl --user restart messenger-consume@*.service + +The systemd user instance is only started after the first login of the +particular user. Consumer often need to start on system boot instead. +Enable lingering on the user to activate that behavior: + +.. code-block:: terminal + + $ loginctl enable-linger + +Logs are managed by journald and can be worked with using the journalctl +command: + +.. code-block:: terminal + + # follow logs of consumer nr 11 + $ journalctl -f --user-unit messenger-consume@11.service + + # follow logs of all consumers + $ journalctl -f --user-unit messenger-consume@* + + # follow all logs from your user services + $ journalctl -f _UID=$UID + +See the `systemd docs`_ for more details. + +.. note:: + + You either need elevated privileges for the ``journalctl`` command, or add + your user to the systemd-journal group: + + .. code-block:: terminal + + $ sudo usermod -a -G systemd-journal + +Stateless Worker +~~~~~~~~~~~~~~~~ + +PHP is designed to be stateless, there are no shared resources across different +requests. In HTTP context PHP cleans everything after sending the response, so +you can decide to not take care of services that may leak memory. + +On the other hand, it's common for workers to process messages sequentially in +long-running CLI processes which don't finish after processing a single message. +Beware about service states to prevent information and/or memory leakage as +Symfony will inject the same instance of a service in all messages, preserving +the internal state of the services. + +However, certain Symfony services, such as the Monolog +:ref:`fingers crossed handler `, leak by design. +Symfony provides a **service reset** feature to solve this problem. When resetting +the container automatically between two messages, Symfony looks for any services +implementing :class:`Symfony\\Contracts\\Service\\ResetInterface` (including your +own services) and calls their ``reset()`` method so they can clean their internal state. + +If a service is not stateless and you want to reset its properties after each message, then +the service must implement :class:`Symfony\\Contracts\\Service\\ResetInterface` where you can reset the +properties in the ``reset()`` method. + +If you don't want to reset the container, add the ``--no-reset`` option when +running the ``messenger:consume`` command. + +.. _messenger-retries-failures: + +Rate Limited Transport +~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes you might need to rate limit your message worker. You can configure a +rate limiter on a transport (requires the :doc:`RateLimiter component `) +by setting its ``rate_limiter`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + async: + rate_limiter: your_rate_limiter_name + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->messenger() + ->transport('async') + ->options(['rate_limiter' => 'your_rate_limiter_name']) + ; + }; + +.. warning:: + + When a rate limiter is configured on a transport, it will block the whole + worker when the limit is hit. You should make sure you configure a dedicated + worker for a rate limited transport to avoid other transports to be blocked. + +Retries & Failures +------------------ + +If an exception is thrown while consuming a message from a transport it will +automatically be re-sent to the transport to be tried again. By default, a message +will be retried 3 times before being discarded or +:ref:`sent to the failure transport `. Each retry +will also be delayed, in case the failure was due to a temporary issue. All of +this is configurable for each transport: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + async_priority_high: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + + # default configuration + retry_strategy: + max_retries: 3 + # milliseconds delay + delay: 1000 + # causes the delay to be higher before each retry + # e.g. 1 second delay, 2 seconds, 4 seconds + multiplier: 2 + max_delay: 0 + # applies randomness to the delay that can prevent the thundering herd effect + # the value (between 0 and 1.0) is the percentage of 'delay' that will be added/subtracted + jitter: 0.1 + # override all of this with a service that + # implements Symfony\Component\Messenger\Retry\RetryStrategyInterface + # service: null + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $messenger->transport('async_priority_high') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + // default configuration + ->retryStrategy() + ->maxRetries(3) + // milliseconds delay + ->delay(1000) + // causes the delay to be higher before each retry + // e.g. 1 second delay, 2 seconds, 4 seconds + ->multiplier(2) + ->maxDelay(0) + // applies randomness to the delay that can prevent the thundering herd effect + // the value (between 0 and 1.0) is the percentage of 'delay' that will be added/subtracted + ->jitter(0.1) + // override all of this with a service that + // implements Symfony\Component\Messenger\Retry\RetryStrategyInterface + ->service(null) + ; + }; + +.. versionadded:: 7.1 + + The ``jitter`` option was introduced in Symfony 7.1. + +.. tip:: + + Symfony triggers a :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageRetriedEvent` + when a message is retried so you can run your own logic. + +.. note:: + + Thanks to :class:`Symfony\\Component\\Messenger\\Stamp\\SerializedMessageStamp`, + the serialized form of the message is saved, which prevents to serialize it + again if the message is later retried. + +Avoiding Retrying +~~~~~~~~~~~~~~~~~ + +Sometimes handling a message might fail in a way that you *know* is permanent +and should not be retried. If you throw +:class:`Symfony\\Component\\Messenger\\Exception\\UnrecoverableMessageHandlingException`, +the message will not be retried. + +.. note:: + + Messages that will not be retried, will still show up in the configured failure transport. + If you want to avoid that, consider handling the error yourself and let the handler + successfully end. + +Forcing Retrying +~~~~~~~~~~~~~~~~ + +Sometimes handling a message must fail in a way that you *know* is temporary +and must be retried. If you throw +:class:`Symfony\\Component\\Messenger\\Exception\\RecoverableMessageHandlingException`, +the message will always be retried infinitely and ``max_retries`` setting will be ignored. + +You can define a custom retry delay (e.g., to use the value from the ``Retry-After`` +header in an HTTP response) by setting the ``retryDelay`` argument in the +constructor of the ``RecoverableMessageHandlingException``. + +.. versionadded:: 7.2 + + The ``retryDelay`` argument and the ``getRetryDelay()`` method were introduced + in Symfony 7.2. + +.. _messenger-failure-transport: + +Saving & Retrying Failed Messages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a message fails it is retried multiple times (``max_retries``) and then will +be discarded. To avoid this happening, you can instead configure a ``failure_transport``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + # after retrying, messages will be sent to the "failed" transport + failure_transport: failed + + transports: + # ... other transports + + failed: 'doctrine://default?queue_name=failed' + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + // after retrying, messages will be sent to the "failed" transport + $messenger->failureTransport('failed'); + + // ... other transports + + $messenger->transport('failed') + ->dsn('doctrine://default?queue_name=failed'); + }; + +In this example, if handling a message fails 3 times (default ``max_retries``), +it will then be sent to the ``failed`` transport. While you *can* use +``messenger:consume failed`` to consume this like a normal transport, you'll +usually want to manually view the messages in the failure transport and choose +to retry them: + +.. code-block:: terminal + + # see all messages in the failure transport with a default limit of 50 + $ php bin/console messenger:failed:show + + # see the 10 first messages + $ php bin/console messenger:failed:show --max=10 + + # see only App\Message\MyMessage messages + $ php bin/console messenger:failed:show --class-filter='App\Message\MyMessage' + + # see the number of messages by message class + $ php bin/console messenger:failed:show --stats + + # see details about a specific failure + $ php bin/console messenger:failed:show 20 -vv + + # for each message, this command asks whether to retry, skip, or delete + $ php bin/console messenger:failed:retry -vv + + # retry specific messages + $ php bin/console messenger:failed:retry 20 30 --force + + # remove a message without retrying it + $ php bin/console messenger:failed:remove 20 + + # remove messages without retrying them and show each message before removing it + $ php bin/console messenger:failed:remove 20 30 --show-messages + + # remove all messages in the failure transport + $ php bin/console messenger:failed:remove --all + + # remove only App\Message\MyMessage messages + $ php bin/console messenger:failed:remove --class-filter='App\Message\MyMessage' + +If the message fails again, it will be re-sent back to the failure transport +due to the normal :ref:`retry rules `. Once the max +retry has been hit, the message will be discarded permanently. + +.. versionadded:: 7.2 + + The option to skip a message in the ``messenger:failed:retry`` command was + introduced in Symfony 7.2 + +.. versionadded:: 7.3 + + The option to filter by a message class in the ``messenger:failed:remove`` command was + introduced in Symfony 7.3 + +Multiple Failed Transports +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes it is not enough to have a single, global ``failed transport`` configured +because some messages are more important than others. In those cases, you can +override the failure transport for only specific transports: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + # after retrying, messages will be sent to the "failed" transport + # by default if no "failed_transport" is configured inside a transport + failure_transport: failed_default + + transports: + async_priority_high: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + failure_transport: failed_high_priority + + # since no failed transport is configured, the one used will be + # the global "failure_transport" set + async_priority_low: + dsn: 'doctrine://default?queue_name=async_priority_low' + + failed_default: 'doctrine://default?queue_name=failed_default' + failed_high_priority: 'doctrine://default?queue_name=failed_high_priority' + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + // after retrying, messages will be sent to the "failed" transport + // by default if no "failure_transport" is configured inside a transport + $messenger->failureTransport('failed_default'); + + $messenger->transport('async_priority_high') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + ->failureTransport('failed_high_priority'); + + // since no failed transport is configured, the one used will be + // the global failure_transport set + $messenger->transport('async_priority_low') + ->dsn('doctrine://default?queue_name=async_priority_low'); + + $messenger->transport('failed_default') + ->dsn('doctrine://default?queue_name=failed_default'); + + $messenger->transport('failed_high_priority') + ->dsn('doctrine://default?queue_name=failed_high_priority'); + }; + +If there is no ``failure_transport`` defined globally or on the transport level, +the messages will be discarded after the number of retries. + +The failed commands have an optional option ``--transport`` to specify +the ``failure_transport`` configured at the transport level. + +.. code-block:: terminal + + # see all messages in "failure_transport" transport + $ php bin/console messenger:failed:show --transport=failure_transport + + # retry specific messages from "failure_transport" + $ php bin/console messenger:failed:retry 20 30 --transport=failure_transport --force + + # remove a message without retrying it from "failure_transport" + $ php bin/console messenger:failed:remove 20 --transport=failure_transport + +.. _messenger-transports-config: + +Transport Configuration +----------------------- + +Messenger supports a number of different transport types, each with their own +options. Options can be passed to the transport via a DSN string or configuration. + +.. code-block:: env + + # .env + MESSENGER_TRANSPORT_DSN=amqp://localhost/%2f/messages?auto_setup=false + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + my_transport: + dsn: "%env(MESSENGER_TRANSPORT_DSN)%" + options: + auto_setup: false + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $messenger->transport('my_transport') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + ->options(['auto_setup' => false]); + }; + +Options defined under ``options`` take precedence over ones defined in the DSN. + +AMQP Transport +~~~~~~~~~~~~~~ + +The AMQP transport uses the AMQP PHP extension to send messages to queues like +RabbitMQ. Install it by running: + +.. code-block:: terminal + + $ composer require symfony/amqp-messenger + +The AMQP transport DSN may looks like this: + +.. code-block:: env + + # .env + MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages + + # or use the AMQPS protocol + MESSENGER_TRANSPORT_DSN=amqps://guest:guest@localhost/%2f/messages + +If you want to use TLS/SSL encrypted AMQP, you must also provide a CA certificate. +Define the certificate path in the ``amqp.cacert`` PHP.ini setting +(e.g. ``amqp.cacert = /etc/ssl/certs``) or in the ``cacert`` parameter of the +DSN (e.g ``amqps://localhost?cacert=/etc/ssl/certs/``). + +The default port used by TLS/SSL encrypted AMQP is 5671, but you can overwrite +it in the ``port`` parameter of the DSN (e.g. ``amqps://localhost?cacert=/etc/ssl/certs/&port=12345``). + +.. note:: + + By default, the transport will automatically create any exchanges, queues and + binding keys that are needed. That can be disabled, but some functionality + may not work correctly (like delayed queues). + To not autocreate any queues, you can configure a transport with ``queues: []``. + +.. note:: + + You can limit the consumer of an AMQP transport to only process messages + from some queues of an exchange. See :ref:`messenger-limit-queues`. + +The transport has a number of other options, including ways to configure +the exchange, queues binding keys and more. See the documentation on +:class:`Symfony\\Component\\Messenger\\Bridge\\Amqp\\Transport\\Connection`. + +The transport has a number of options: + +``auto_setup`` (default: ``true``) + Whether the exchanges and queues should be created automatically during + send / get. + +``cacert`` + Path to the CA cert file in PEM format. + +``cert`` + Path to the client certificate in PEM format. + +``channel_max`` + Specifies highest channel number that the server permits. 0 means standard + extension limit + +``confirm_timeout`` + Timeout in seconds for confirmation; if none specified, transport will not + wait for message confirmation. Note: 0 or greater seconds. May be + fractional. + +``connect_timeout`` + Connection timeout. Note: 0 or greater seconds. May be fractional. + +``frame_max`` + The largest frame size that the server proposes for the connection, + including frame header and end-byte. 0 means standard extension limit + (depends on librabbimq default frame size limit) + +``heartbeat`` + The delay, in seconds, of the connection heartbeat that the server wants. 0 + means the server does not want a heartbeat. Note, librabbitmq has limited + heartbeat support, which means heartbeats checked only during blocking + calls. + +``host`` + Hostname of the AMQP service + +``key`` + Path to the client key in PEM format. + +``login`` + Username to use to connect the AMQP service + +``password`` + Password to use to connect to the AMQP service + +``persistent`` (default: ``'false'``) + Whether the connection is persistent + +``port`` + Port of the AMQP service + +``read_timeout`` + Timeout in for income activity. Note: 0 or greater seconds. May be + fractional. + +``retry`` + (no description available) + +``sasl_method`` + (no description available) + +``connection_name`` + For custom connection names (requires at least version 1.10 of the PHP AMQP + extension) + +``verify`` + Enable or disable peer verification. If peer verification is enabled then + the common name in the server certificate must match the server name. Peer + verification is enabled by default. + +``vhost`` + Virtual Host to use with the AMQP service + +``write_timeout`` + Timeout in for outcome activity. Note: 0 or greater seconds. May be + fractional. + +``delay[queue_name_pattern]`` (default: ``delay_%exchange_name%_%routing_key%_%delay%``) + Pattern to use to create the queues + +``delay[exchange_name]`` (default: ``delays``) + Name of the exchange to be used for the delayed/retried messages + +``queues[name][arguments]`` + Extra arguments + +``queues[name][binding_arguments]`` + Arguments to be used while binding the queue. + +``queues[name][binding_keys]`` + The binding keys (if any) to bind to this queue + +``queues[name][flags]`` (default: ``AMQP_DURABLE``) + Queue flags + +``exchange[arguments]`` + Extra arguments for the exchange (e.g. ``alternate-exchange``) + +``exchange[default_publish_routing_key]`` + Routing key to use when publishing, if none is specified on the message + +``exchange[flags]`` (default: ``AMQP_DURABLE``) + Exchange flags + +``exchange[name]`` + Name of the exchange + +``exchange[type]`` (default: ``fanout``) + Type of exchange + +You can also configure AMQP-specific settings on your message by adding +:class:`Symfony\\Component\\Messenger\\Bridge\\Amqp\\Transport\\AmqpStamp` to +your Envelope:: + + use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpStamp; + // ... + + $attributes = []; + $bus->dispatch(new SmsNotification(), [ + new AmqpStamp('custom-routing-key', AMQP_NOPARAM, $attributes), + ]); + +.. warning:: + + The consumers do not show up in an admin panel as this transport does not rely on + ``\AmqpQueue::consume()`` which is blocking. Having a blocking receiver makes + the ``--time-limit/--memory-limit`` options of the ``messenger:consume`` command as well as + the ``messenger:stop-workers`` command inefficient, as they all rely on the fact that + the receiver returns immediately no matter if it finds a message or not. The consume + worker is responsible for iterating until it receives a message to handle and/or until one + of the stop conditions is reached. Thus, the worker's stop logic cannot be reached if it + is stuck in a blocking call. + +.. tip:: + + If your application faces socket exceptions or `high connection churn`_ + (shown by the rapid creation and deletion of connections), consider using + `AMQProxy`_. This tool works as a gateway between Symfony Messenger and AMQP server, + maintaining stable connections and minimizing overheads (which also improves + the overall performance). + +Doctrine Transport +~~~~~~~~~~~~~~~~~~ + +The Doctrine transport can be used to store messages in a database table. +Install it by running: + +.. code-block:: terminal + + $ composer require symfony/doctrine-messenger + +The Doctrine transport DSN may look like this: + +.. code-block:: env + + # .env + MESSENGER_TRANSPORT_DSN=doctrine://default + +The format is ``doctrine://``, in case you have multiple connections +and want to use one other than the "default". The transport will automatically create +a table named ``messenger_messages``. + +If you want to change the default table name, pass a custom table name in the +DSN by using the ``table_name`` option: + +.. code-block:: env + + # .env + MESSENGER_TRANSPORT_DSN=doctrine://default?table_name=your_custom_table_name + +Or, to create the table yourself, set the ``auto_setup`` option to ``false`` and +:ref:`generate a migration `. + +The transport has a number of options: + +``table_name`` (default: ``messenger_messages``) + Name of the table + +``queue_name`` (default: ``default``) + Name of the queue (a column in the table, to use one table for multiple + transports) + +``redeliver_timeout`` (default: ``3600``) + Timeout before retrying a message that's in the queue but in the "handling" + state (if a worker stopped for some reason, this will occur, eventually you + should retry the message) - in seconds. + + .. note:: + + Set ``redeliver_timeout`` to a greater value than your slowest message + duration. Otherwise, some messages will start a second time while the + first one is still being handled. + +``auto_setup`` + Whether the table should be created automatically during send / get. + +When using PostgreSQL, you have access to the following options to leverage +the `LISTEN/NOTIFY`_ feature. This allow for a more performant approach +than the default polling behavior of the Doctrine transport because +PostgreSQL will directly notify the workers when a new message is inserted +in the table. + +``use_notify`` (default: ``true``) + Whether to use LISTEN/NOTIFY. + +``check_delayed_interval`` (default: ``60000``) + The interval to check for delayed messages, in milliseconds. Set to 0 to + disable checks. + +``get_notify_timeout`` (default: ``0``) + The length of time to wait for a response when calling + ``PDO::pgsqlGetNotify``, in milliseconds. + +The Doctrine transport supports the ``--keepalive`` option by periodically updating +the ``delivered_at`` timestamp to prevent the message from being redelivered. + +.. versionadded:: 7.3 + + Keepalive support was introduced in Symfony 7.3. + +Beanstalkd Transport +~~~~~~~~~~~~~~~~~~~~ + +The Beanstalkd transport sends messages directly to a Beanstalkd work queue. Install +it by running: + +.. code-block:: terminal + + $ composer require symfony/beanstalkd-messenger + +The Beanstalkd transport DSN may looks like this: + +.. code-block:: env + + # .env + MESSENGER_TRANSPORT_DSN=beanstalkd://localhost:11300?tube_name=foo&timeout=4&ttr=120 + + # If no port, it will default to 11300 + MESSENGER_TRANSPORT_DSN=beanstalkd://localhost + +The transport has a number of options: + +``bury_on_reject`` (default: ``false``) + When set to ``true``, rejected messages are placed into a "buried" state + in Beanstalkd instead of being deleted. + + .. versionadded:: 7.3 + + The ``bury_on_reject`` option was introduced in Symfony 7.3. + +``timeout`` (default: ``0``) + Message reservation timeout - in seconds. 0 will cause the server to + immediately return either a response or a TransportException will be thrown. + +``ttr`` (default: ``90``) + The message time to run before it is put back in the ready queue - in + seconds. + +``tube_name`` (default: ``default``) + Name of the queue + +The Beanstalkd transport supports the ``--keepalive`` option by using Beanstalkd's +``touch`` command to periodically reset the job's ``ttr``. + +.. versionadded:: 7.2 + + Keepalive support was introduced in Symfony 7.2. + +The Beanstalkd transport lets you set the priority of the messages being dispatched. +Use the :class:`Symfony\\Component\\Messenger\\Bridge\\Beanstalkd\\Transport\\BeanstalkdPriorityStamp` +and pass a number to specify the priority (default = ``1024``; lower numbers mean higher priority):: + + use App\Message\SomeMessage; + use Symfony\Component\Messenger\Stamp\BeanstalkdPriorityStamp; + + $this->bus->dispatch(new SomeMessage('some data'), [ + // 0 = highest priority + // 2**32 - 1 = lowest priority + new BeanstalkdPriorityStamp(0), + ]); + +.. versionadded:: 7.3 + + ``BeanstalkdPriorityStamp`` support was introduced in Symfony 7.3. + +.. _messenger-redis-transport: + +Redis Transport +~~~~~~~~~~~~~~~ + +The Redis transport uses `streams`_ to queue messages. This transport requires +the Redis PHP extension (>=4.3) and a running Redis server (^5.0). Install it by +running: + +.. code-block:: terminal + + $ composer require symfony/redis-messenger + +The Redis transport DSN may looks like this: + +.. code-block:: env + + # .env + MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages + # Full DSN Example + MESSENGER_TRANSPORT_DSN=redis://password@localhost:6379/messages/symfony/consumer?auto_setup=true&serializer=1&stream_max_entries=0&dbindex=0 + # Redis Cluster Example + MESSENGER_TRANSPORT_DSN=redis://host-01:6379,redis://host-02:6379,redis://host-03:6379,redis://host-04:6379 + # Unix Socket Example + MESSENGER_TRANSPORT_DSN=redis:///var/run/redis.sock + # TLS Example + MESSENGER_TRANSPORT_DSN=rediss://localhost:6379/messages + # Multiple Redis Sentinel Hosts Example + MESSENGER_TRANSPORT_DSN=redis:?host[redis1:26379]&host[redis2:26379]&host[redis3:26379]&sentinel_master=db + +A number of options can be configured via the DSN or via the ``options`` key +under the transport in ``messenger.yaml``: + +``stream`` (default: ``messages``) + The Redis stream name + +``group`` (default: ``symfony``) + The Redis consumer group name + +``consumer`` (default: ``consumer``) + Consumer name used in Redis. Allows setting an explicit consumer name identifier. + Recommended in environments with multiple workers to prevent duplicate message + processing. Typically set via an environment variable: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + redis: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + options: + consumer: '%env(MESSENGER_CONSUMER_NAME)%' + +``auto_setup`` (default: ``true``) + Whether to create the Redis group automatically + +``auth`` + The Redis password + +``delete_after_ack`` (default: ``true``) + If ``true``, messages are deleted automatically after processing them + +``delete_after_reject`` (default: ``true``) + If ``true``, messages are deleted automatically if they are rejected + +``lazy`` (default: ``false``) + Connect only when a connection is really needed + +``serializer`` (default: ``Redis::SERIALIZER_PHP``) + How to serialize the final payload in Redis (the ``Redis::OPT_SERIALIZER`` option) + +``stream_max_entries`` (default: ``0``) + The maximum number of entries which the stream will be trimmed to. Set it to + a large enough number to avoid losing pending messages + +``redeliver_timeout`` (default: ``3600``) + Timeout (in seconds) before retrying a pending message which is owned by an abandoned consumer + (if a worker died for some reason, this will occur, eventually you should retry the message). + +``claim_interval`` (default: ``60000``) + Interval on which pending/abandoned messages should be checked for to claim - in milliseconds + +``persistent_id`` (default: ``null``) + String, if null connection is non-persistent. + +``retry_interval`` (default: ``0``) + Int, value in milliseconds + +``read_timeout`` (default: ``0``) + Float, value in seconds default indicates unlimited + +``timeout`` (default: ``0``) + Connection timeout. Float, value in seconds default indicates unlimited + +``sentinel_master`` (default: ``null``) + String, if null or empty Sentinel support is disabled + +``redis_sentinel`` (default: ``null``) + An alias of the ``sentinel_master`` option + + .. versionadded:: 7.1 + + The ``redis_sentinel`` option was introduced in Symfony 7.1. + +``ssl`` (default: ``null``) + Map of `SSL context options`_ for the TLS channel. This is useful for example + to change the requirements for the TLS channel in tests: + + .. code-block:: yaml + + # config/packages/test/messenger.yaml + framework: + messenger: + transports: + redis: + dsn: "rediss://localhost" + options: + ssl: + allow_self_signed: true + capture_peer_cert: true + capture_peer_cert_chain: true + disable_compression: true + SNI_enabled: true + verify_peer: true + verify_peer_name: true + +.. warning:: + + There should never be more than one ``messenger:consume`` command running with the same + combination of ``stream``, ``group`` and ``consumer``, or messages could end up being + handled more than once. If you run multiple queue workers, ``consumer`` can be set to an + environment variable, like ``%env(MESSENGER_CONSUMER_NAME)%``, set by Supervisor + (example below) or any other service used to manage the worker processes. + In a container environment, the ``HOSTNAME`` can be used as the consumer name, since + there is only one worker per container/host. If using Kubernetes to orchestrate the + containers, consider using a ``StatefulSet`` to have stable names. + +.. tip:: + + Set ``delete_after_ack`` to ``true`` (if you use a single group) or define + ``stream_max_entries`` (if you can estimate how many max entries is acceptable + in your case) to avoid memory leaks. Otherwise, all messages will remain + forever in Redis. + +The Redis transport supports the ``--keepalive`` option by using Redis's ``XCLAIM`` +command to periodically reset the message's idle time to zero. + +.. versionadded:: 7.3 + + Keepalive support was introduced in Symfony 7.3. + +In Memory Transport +~~~~~~~~~~~~~~~~~~~ + +The ``in-memory`` transport does not actually deliver messages. Instead, it +holds them in memory during the request, which can be useful for testing. +For example, if you have an ``async_priority_normal`` transport, you could +override it in the ``test`` environment to use this transport: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/test/messenger.yaml + framework: + messenger: + transports: + async_priority_normal: 'in-memory://' + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/test/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $messenger->transport('async_priority_normal') + ->dsn('in-memory://'); + }; + +Then, while testing, messages will *not* be delivered to the real transport. +Even better, in a test, you can check that exactly one message was sent +during a request:: + + // tests/Controller/DefaultControllerTest.php + namespace App\Tests\Controller; + + use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + use Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport; + + class DefaultControllerTest extends WebTestCase + { + public function testSomething(): void + { + $client = static::createClient(); + // ... + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + /** @var InMemoryTransport $transport */ + $transport = $this->getContainer()->get('messenger.transport.async_priority_normal'); + $this->assertCount(1, $transport->getSent()); + } + } + +The transport has a number of options: + +``serialize`` (boolean, default: ``false``) + Whether to serialize messages or not. This is useful to test an additional + layer, especially when you use your own message serializer. + +.. note:: + + All ``in-memory`` transports will be reset automatically after each test **in** + test classes extending + :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase` + or :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase`. + +Amazon SQS +~~~~~~~~~~ + +The Amazon SQS transport is perfect for applications hosted on AWS. Install it by +running: + +.. code-block:: terminal + + $ composer require symfony/amazon-sqs-messenger + +The SQS transport DSN may looks like this: + +.. code-block:: env + + # .env + MESSENGER_TRANSPORT_DSN=https://sqs.eu-west-3.amazonaws.com/123456789012/messages?access_key=AKIAIOSFODNN7EXAMPLE&secret_key=j17M97ffSVoKI0briFoo9a + MESSENGER_TRANSPORT_DSN=sqs://localhost:9494/messages?sslmode=disable + +.. note:: + + The transport will automatically create queues that are needed. This + can be disabled by setting the ``auto_setup`` option to ``false``. + +.. tip:: + + Before sending or receiving a message, Symfony needs to convert the queue + name into an AWS queue URL by calling the ``GetQueueUrl`` API in AWS. This + extra API call can be avoided by providing a DSN which is the queue URL. + +The transport has a number of options: + +``access_key`` + AWS access key (must be urlencoded) + +``account`` (default: The owner of the credentials) + Identifier of the AWS account + +``auto_setup`` (default: ``true``) + Whether the queue should be created automatically during send / get. + +``buffer_size`` (default: ``9``) + Number of messages to prefetch + +``debug`` (default: ``false``) + If ``true`` it logs all HTTP requests and responses (it impacts performance) + +``endpoint`` (default: ``https://sqs.eu-west-1.amazonaws.com``) + Absolute URL to the SQS service + +``poll_timeout`` (default: ``0.1``) + Wait for new message duration in seconds + +``queue_name`` (default: ``messages``) + Name of the queue + +``queue_attributes`` + Attributes of a queue as per `SQS CreateQueue API`_. Array of strings indexed by keys of ``AsyncAws\Sqs\Enum\QueueAttributeName``. + +``queue_tags`` + Cost allocation tags of a queue as per `SQS CreateQueue API`_. Array of strings indexed by strings. + +``region`` (default: ``eu-west-1``) + Name of the AWS region + +``secret_key`` + AWS secret key (must be urlencoded) + +``session_token`` + AWS session token + +``visibility_timeout`` (default: Queue's configuration) + Amount of seconds the message will not be visible (`Visibility Timeout`_) + +``wait_time`` (default: ``20``) + `Long polling`_ duration in seconds + +.. versionadded:: 7.3 + + The ``queue_attributes`` and ``queue_tags`` options were introduced in Symfony 7.3. + +.. note:: + + The ``wait_time`` parameter defines the maximum duration Amazon SQS should + wait until a message is available in a queue before sending a response. + It helps reducing the cost of using Amazon SQS by eliminating the number + of empty responses. + + The ``poll_timeout`` parameter defines the duration the receiver should wait + before returning null. It avoids blocking other receivers from being called. + +.. note:: + + If the queue name is suffixed by ``.fifo``, AWS will create a `FIFO queue`_. + Use the stamp :class:`Symfony\\Component\\Messenger\\Bridge\\AmazonSqs\\Transport\\AmazonSqsFifoStamp` + to define the ``Message group ID`` and the ``Message deduplication ID``. + + Another possibility is to enable the + :class:`Symfony\\Component\\Messenger\\Bridge\\AmazonSqs\\Middleware\\AddFifoStampMiddleware`. + If your message implements + :class:`Symfony\\Component\\Messenger\\Bridge\\AmazonSqs\\MessageDeduplicationAwareInterface`, + the middleware will automatically add the + :class:`Symfony\\Component\\Messenger\\Bridge\\AmazonSqs\\Transport\\AmazonSqsFifoStamp` + and set the ``Message deduplication ID``. Additionally, if your message implements the + :class:`Symfony\\Component\\Messenger\\Bridge\\AmazonSqs\\MessageGroupAwareInterface`, + the middleware will automatically set the ``Message group ID`` of the stamp. + + You can learn more about middlewares in + :ref:`the dedicated section `. + + FIFO queues don't support setting a delay per message, a value of ``delay: 0`` + is required in the retry strategy settings. + +The SQS transport supports the ``--keepalive`` option by using the ``ChangeMessageVisibility`` +action to periodically update the ``VisibilityTimeout`` of the message. + +.. versionadded:: 7.2 + + Keepalive support was introduced in Symfony 7.2. + +Serializing Messages +~~~~~~~~~~~~~~~~~~~~ + +When messages are sent to (and received from) a transport, they're serialized +using PHP's native ``serialize()`` & ``unserialize()`` functions. You can change +this globally (or for each transport) to a service that implements +:class:`Symfony\\Component\\Messenger\\Transport\\Serialization\\SerializerInterface`: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + serializer: + default_serializer: messenger.transport.symfony_serializer + symfony_serializer: + format: json + context: { } + + transports: + async_priority_normal: + dsn: # ... + serializer: messenger.transport.symfony_serializer + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $messenger->serializer() + ->defaultSerializer('messenger.transport.symfony_serializer') + ->symfonySerializer() + ->format('json') + ->context('foo', 'bar'); + + $messenger->transport('async_priority_normal') + ->dsn('...') + ->serializer('messenger.transport.symfony_serializer'); + }; + +The ``messenger.transport.symfony_serializer`` is a built-in service that uses +the :doc:`Serializer component ` and can be configured in a few ways. +If you *do* choose to use the Symfony serializer, you can control the context +on a case-by-case basis via the :class:`Symfony\\Component\\Messenger\\Stamp\\SerializerStamp` +(see `Envelopes & Stamps`_). + +.. tip:: + + When sending/receiving messages to/from another application, you may need + more control over the serialization process. Using a custom serializer + provides that control. See `SymfonyCasts' message serializer tutorial`_ for + details. + +Closing Connections +~~~~~~~~~~~~~~~~~~~ + +When using a transport that requires a connection, you can close it by calling the +:method:`Symfony\\Component\\Messenger\\Transport\\CloseableTransportInterface::close` +method to free up resources in long-running processes. + +This interface is implemented by the following transports: AmazonSqs, Amqp, and Redis. +If you need to close a Doctrine connection, you can do so +:ref:`using middleware `. + +.. versionadded:: 7.3 + + The ``CloseableTransportInterface`` and its ``close()`` method were introduced + in Symfony 7.3. + +Running Commands And External Processes +--------------------------------------- + +Trigger a Command +~~~~~~~~~~~~~~~~~ + +It is possible to trigger any command by dispatching a +:class:`Symfony\\Component\\Console\\Messenger\\RunCommandMessage`. Symfony +will take care of handling this message and execute the command passed +to the message parameter:: + + use Symfony\Component\Console\Messenger\RunCommandMessage; + use Symfony\Component\Messenger\MessageBusInterface; + + class CleanUpService + { + public function __construct(private readonly MessageBusInterface $bus) + { + } + + public function cleanUp(): void + { + // Long task with some caching... + + // Once finished, dispatch some clean up commands + $this->bus->dispatch(new RunCommandMessage('app:my-cache:clean-up --dir=var/temp')); + $this->bus->dispatch(new RunCommandMessage('cache:clear')); + } + } + +You can configure the behavior in the case of something going wrong during command +execution. To do so, you can use the ``throwOnFailure`` and ``catchExceptions`` +parameters when creating your instance of +:class:`Symfony\\Component\\Console\\Messenger\\RunCommandMessage`. + +Once handled, the handler will return a +:class:`Symfony\\Component\\Console\\Messenger\\RunCommandContext` which +contains many useful information such as the exit code or the output of the +process. You can refer to the page dedicated on +:ref:`handler results ` for more information. + +Trigger An External Process +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Messenger comes with a handy helper to run external processes by +dispatching a message. This takes advantages of the +:doc:`Process component `. By dispatching a +:class:`Symfony\\Component\\Process\\Messenger\\RunProcessMessage`, Messenger +will take care of creating a new process with the parameters you passed:: + + use Symfony\Component\Messenger\MessageBusInterface; + use Symfony\Component\Process\Messenger\RunProcessMessage; + + class CleanUpService + { + public function __construct( + private readonly MessageBusInterface $bus, + ) { + } + + public function cleanUp(): void + { + $this->bus->dispatch(new RunProcessMessage(['rm', '-rf', 'var/log/temp/*'], cwd: '/my/custom/working-dir')); + + // ... + } + } + +If you want to use shell features such as redirections or pipes, use the static +factory :method:Symfony\\Component\\Process\\Messenger\\RunProcessMessage::fromShellCommandline:: + + use Symfony\Component\Messenger\MessageBusInterface; + use Symfony\Component\Process\Messenger\RunProcessMessage; + + class CleanUpService + { + public function __construct( + private readonly MessageBusInterface $bus, + ) { + } + + public function cleanUp(): void + { + $this->bus->dispatch(RunProcessMessage::fromShellCommandline('echo "Hello World" > var/log/hello.txt')); + + // ... + } + } + +For more information, read the documentation about +:ref:`using features from the OS shell `. + +.. versionadded:: 7.3 + + The ``RunProcessMessage::fromShellCommandline()`` method was introduced in Symfony 7.3. + +Once handled, the handler will return a +:class:`Symfony\\Component\\Process\\Messenger\\RunProcessContext` which +contains many useful information such as the exit code or the output of the +process. You can refer to the page dedicated on +:ref:`handler results ` for more information. + +Pinging A Webservice +-------------------- + +Sometimes, you may need to regularly ping a webservice to get its status, e.g. +is it up or down. It is possible to do so by dispatching a +:class:`Symfony\\Component\\HttpClient\\Messenger\\PingWebhookMessage`:: + + use Symfony\Component\HttpClient\Messenger\PingWebhookMessage; + use Symfony\Component\Messenger\MessageBusInterface; + + class LivenessService + { + public function __construct(private readonly MessageBusInterface $bus) + { + } + + public function ping(): void + { + // An HttpExceptionInterface is thrown on 3xx/4xx/5xx + $this->bus->dispatch(new PingWebhookMessage('GET', 'https://example.com/status')); + + // Ping, but does not throw on 3xx/4xx/5xx + $this->bus->dispatch(new PingWebhookMessage('GET', 'https://example.com/status', throw: false)); + + // Any valid HttpClientInterface option can be used + $this->bus->dispatch(new PingWebhookMessage('POST', 'https://example.com/status', [ + 'headers' => [ + 'Authorization' => 'Bearer ...' + ], + 'json' => [ + 'data' => 'some-data', + ], + ])); + } + } + +The handler will return a +:class:`Symfony\\Contracts\\HttpClient\\ResponseInterface`, allowing you to +gather and process information returned by the HTTP request. + +Getting Results from your Handlers +---------------------------------- + +When a message is handled, the :class:`Symfony\\Component\\Messenger\\Middleware\\HandleMessageMiddleware` +adds a :class:`Symfony\\Component\\Messenger\\Stamp\\HandledStamp` for each object that handled the message. +You can use this to get the value returned by the handler(s):: + + use Symfony\Component\Messenger\MessageBusInterface; + use Symfony\Component\Messenger\Stamp\HandledStamp; + + $envelope = $messageBus->dispatch(new SomeMessage()); + + // get the value that was returned by the last message handler + $handledStamp = $envelope->last(HandledStamp::class); + $handledStamp->getResult(); + + // or get info about all of handlers + $handledStamps = $envelope->all(HandledStamp::class); + +.. _messenger-getting-handler-results: + +Getting Results when Working with Command & Query Buses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Messenger component can be used in CQRS architectures where command & query +buses are central pieces of the application. Read Martin Fowler's +`article about CQRS`_ to learn more and +:ref:`how to configure multiple buses `. + +As queries are usually synchronous and expected to be handled once, +getting the result from the handler is a common need. + +A :class:`Symfony\\Component\\Messenger\\HandleTrait` exists to get the result +of the handler when processing synchronously. It also ensures that exactly one +handler is registered. The ``HandleTrait`` can be used in any class that has a +``$messageBus`` property:: + + // src/Action/ListItems.php + namespace App\Action; + + use App\Message\ListItemsQuery; + use App\MessageHandler\ListItemsQueryResult; + use Symfony\Component\Messenger\HandleTrait; + use Symfony\Component\Messenger\MessageBusInterface; + + class ListItems + { + use HandleTrait; + + public function __construct( + private MessageBusInterface $messageBus, + ) { + } + + public function __invoke(): void + { + $result = $this->query(new ListItemsQuery(/* ... */)); + + // Do something with the result + // ... + } + + // Creating such a method is optional, but allows type-hinting the result + private function query(ListItemsQuery $query): ListItemsQueryResult + { + return $this->handle($query); + } + } + +Hence, you can use the trait to create command & query bus classes. +For example, you could create a special ``QueryBus`` class and inject it +wherever you need a query bus behavior instead of the ``MessageBusInterface``:: + + // src/MessageBus/QueryBus.php + namespace App\MessageBus; + + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\HandleTrait; + use Symfony\Component\Messenger\MessageBusInterface; + + class QueryBus + { + use HandleTrait; + + public function __construct( + private MessageBusInterface $messageBus, + ) { + } + + /** + * @param object|Envelope $query + * + * @return mixed The handler returned value + */ + public function query($query): mixed + { + return $this->handle($query); + } + } + +You can also add new stamps when handling a message; they will be appended +to the existing ones:: + + $this->handle(new SomeMessage($data), [new SomeStamp(), new AnotherStamp()]); + +.. versionadded:: 7.3 + + The ``$stamps`` parameter of the ``handle()`` method was introduced in Symfony 7.3. + +Customizing Handlers +-------------------- + +.. _messenger-handler-config: + +Manually Configuring Handlers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony will normally :ref:`find and register your handler automatically `. +But, you can also configure a handler manually - and pass it some extra config - +while using ``#AsMessageHandler`` attribute or tagging the handler service +with ``messenger.message_handler``. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/MessageHandler/SmsNotificationHandler.php + namespace App\MessageHandler; + + use App\Message\OtherSmsNotification; + use App\Message\SmsNotification; + use Symfony\Component\Messenger\Attribute\AsMessageHandler; + + #[AsMessageHandler(fromTransport: 'async', priority: 10)] + class SmsNotificationHandler + { + public function __invoke(SmsNotification $message): void + { + // ... + } + } + + .. code-block:: yaml + + # config/services.yaml + services: + App\MessageHandler\SmsNotificationHandler: + tags: [messenger.message_handler] + + # or configure with options + tags: + - + name: messenger.message_handler + # only needed if can't be guessed by type-hint + handles: App\Message\SmsNotification + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + use App\Message\SmsNotification; + use App\MessageHandler\SmsNotificationHandler; + + $container->register(SmsNotificationHandler::class) + ->addTag('messenger.message_handler', [ + // only needed if can't be guessed by type-hint + 'handles' => SmsNotification::class, + ]); + +Possible options to configure with tags are: + +``bus`` + Name of the bus from which the handler can receive messages, by default all buses. + +``from_transport`` + Name of the transport from which the handler can receive messages, by default + all transports. + +``handles`` + Type of messages (FQCN) that can be processed by the handler, only needed if + can't be guessed by type-hint. + +``method`` + Name of the method that will process the message. + +``priority`` + Priority of the handler when multiple handlers can process the same message. + +.. _handler-subscriber-options: + +Handling Multiple Messages +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A single handler class can handle multiple messages. For that add the +``#AsMessageHandler`` attribute to all the handling methods:: + + // src/MessageHandler/SmsNotificationHandler.php + namespace App\MessageHandler; + + use App\Message\OtherSmsNotification; + use App\Message\SmsNotification; + + class SmsNotificationHandler + { + #[AsMessageHandler] + public function handleSmsNotification(SmsNotification $message): void + { + // ... + } + + #[AsMessageHandler] + public function handleOtherSmsNotification(OtherSmsNotification $message): void + { + // ... + } + } + +.. _messenger-transactional-messages: + +Transactional Messages: Handle New Messages After Handling is Done +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A message handler can ``dispatch`` new messages while handling others, to either +the same or a different bus (if the application has +:ref:`multiple buses `). Any errors or exceptions that +occur during this process can have unintended consequences, such as: + +#. If using the ``DoctrineTransactionMiddleware`` and a dispatched message throws + an exception, then any database transactions in the original handler will be + rolled back. +#. If the message is dispatched to a different bus, then the dispatched message + will be handled even if some code later in the current handler throws an exception. + +An Example ``RegisterUser`` Process +................................... + +Consider an application with both a *command* and an *event* bus. The application +dispatches a command named ``RegisterUser`` to the command bus. The command is +handled by the ``RegisterUserHandler`` which creates a ``User`` object, stores +that object to a database and dispatches a ``UserRegistered`` message to the event bus. + +There are many handlers to the ``UserRegistered`` message, one handler may send +a welcome email to the new user. We are using the ``DoctrineTransactionMiddleware`` +to wrap all database queries in one database transaction. + +**Problem 1:** If an exception is thrown when sending the welcome email, then +the user will not be created because the ``DoctrineTransactionMiddleware`` will +rollback the Doctrine transaction, in which the user has been created. + +**Problem 2:** If an exception is thrown when saving the user to the database, +the welcome email is still sent because it is handled asynchronously. + +DispatchAfterCurrentBusMiddleware Middleware +............................................ + +For many applications, the desired behavior is to *only* handle messages that +are dispatched by a handler once that handler has fully finished. This can be done by +using the ``DispatchAfterCurrentBusMiddleware`` and adding a +``DispatchAfterCurrentBusStamp`` stamp to :ref:`the message Envelope `:: + + // src/Messenger/CommandHandler/RegisterUserHandler.php + namespace App\Messenger\CommandHandler; + + use App\Entity\User; + use App\Messenger\Command\RegisterUser; + use App\Messenger\Event\UserRegistered; + use Doctrine\ORM\EntityManagerInterface; + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\MessageBusInterface; + use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp; + + class RegisterUserHandler + { + public function __construct( + private MessageBusInterface $eventBus, + private EntityManagerInterface $em, + ) { + } + + public function __invoke(RegisterUser $command): void + { + $user = new User($command->getUuid(), $command->getName(), $command->getEmail()); + $this->em->persist($user); + + // The DispatchAfterCurrentBusStamp marks the event message to be handled + // only if this handler does not throw an exception. + + $event = new UserRegistered($command->getUuid()); + $this->eventBus->dispatch( + (new Envelope($event)) + ->with(new DispatchAfterCurrentBusStamp()) + ); + + // ... + } + } + +.. code-block:: php + + // src/Messenger/EventSubscriber/WhenUserRegisteredThenSendWelcomeEmail.php + namespace App\Messenger\EventSubscriber; + + use App\Entity\User; + use App\Messenger\Event\UserRegistered; + use Doctrine\ORM\EntityManagerInterface; + use Symfony\Component\Mailer\MailerInterface; + use Symfony\Component\Mime\RawMessage; + + class WhenUserRegisteredThenSendWelcomeEmail + { + public function __construct( + private MailerInterface $mailer, + EntityManagerInterface $em, + ) { + } + + public function __invoke(UserRegistered $event): void + { + $user = $this->em->getRepository(User::class)->find($event->getUuid()); + + $this->mailer->send(new RawMessage('Welcome '.$user->getFirstName())); + } + } + +This means that the ``UserRegistered`` message would not be handled until +*after* the ``RegisterUserHandler`` had completed and the new ``User`` was +persisted to the database. If the ``RegisterUserHandler`` encounters an +exception, the ``UserRegistered`` event will never be handled. And if an +exception is thrown while sending the welcome email, the Doctrine transaction +will not be rolled back. + +.. note:: + + If ``WhenUserRegisteredThenSendWelcomeEmail`` throws an exception, that + exception will be wrapped into a ``DelayedMessageHandlingException``. Using + ``DelayedMessageHandlingException::getWrappedExceptions`` will give you all + exceptions that are thrown while handling a message with the + ``DispatchAfterCurrentBusStamp``. + +The ``dispatch_after_current_bus`` middleware is enabled by default. If you're +configuring your middleware manually, be sure to register +``dispatch_after_current_bus`` before ``doctrine_transaction`` in the middleware +chain. Also, the ``dispatch_after_current_bus`` middleware must be loaded for +*all* of the buses being used. + +Binding Handlers to Different Transports +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each message can have multiple handlers, and when a message is consumed +*all* of its handlers are called. But you can also configure a handler to only +be called when it's received from a *specific* transport. This allows you to +have a single message where each handler is called by a different "worker" +that's consuming a different transport. + +Suppose you have an ``UploadedImage`` message with two handlers: + +* ``ThumbnailUploadedImageHandler``: you want this to be handled by + a transport called ``image_transport`` + +* ``NotifyAboutNewUploadedImageHandler``: you want this to be handled + by a transport called ``async_priority_normal`` + +To do this, add the ``from_transport`` option to each handler. For example:: + + // src/MessageHandler/ThumbnailUploadedImageHandler.php + namespace App\MessageHandler; + + use App\Message\UploadedImage; + + #[AsMessageHandler(fromTransport: 'image_transport')] + class ThumbnailUploadedImageHandler + { + public function __invoke(UploadedImage $uploadedImage): void + { + // do some thumbnailing + } + } + +And similarly:: + + // src/MessageHandler/NotifyAboutNewUploadedImageHandler.php + // ... + + #[AsMessageHandler(fromTransport: 'async_priority_normal')] + class NotifyAboutNewUploadedImageHandler + { + // ... + } + +Then, make sure to "route" your message to *both* transports: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + async_priority_normal: # ... + image_transport: # ... + + routing: + # ... + 'App\Message\UploadedImage': [image_transport, async_priority_normal] + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $messenger->transport('async_priority_normal')->dsn('...'); + $messenger->transport('image_transport')->dsn('...'); + + $messenger->routing('App\Message\UploadedImage') + ->senders(['image_transport', 'async_priority_normal']); + }; + +That's it! You can now consume each transport: + +.. code-block:: terminal + + # will only call ThumbnailUploadedImageHandler when handling the message + $ php bin/console messenger:consume image_transport -vv + + $ php bin/console messenger:consume async_priority_normal -vv + +.. warning:: + + If a handler does *not* have ``from_transport`` config, it will be executed + on *every* transport that the message is received from. + +Process Messages by Batches +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can declare "special" handlers which will process messages by batch. +By doing so, the handler will wait for a certain amount of messages to be +pending before processing them. The declaration of a batch handler is done +by implementing +:class:`Symfony\\Component\\Messenger\\Handler\\BatchHandlerInterface`. The +:class:`Symfony\\Component\\Messenger\\Handler\\BatchHandlerTrait` is also +provided in order to ease the declaration of these special handlers:: + + use Symfony\Component\Messenger\Handler\Acknowledger; + use Symfony\Component\Messenger\Handler\BatchHandlerInterface; + use Symfony\Component\Messenger\Handler\BatchHandlerTrait; + + class MyBatchHandler implements BatchHandlerInterface + { + use BatchHandlerTrait; + + public function __invoke(MyMessage $message, ?Acknowledger $ack = null): mixed + { + return $this->handle($message, $ack); + } + + private function process(array $jobs): void + { + foreach ($jobs as [$message, $ack]) { + try { + // Compute $result from $message... + + // Acknowledge the processing of the message + $ack->ack($result); + } catch (\Throwable $e) { + $ack->nack($e); + } + } + } + + // Optionally, you can override some of the trait methods, such as the + // `getBatchSize()` method, to specify your own batch size... + private function getBatchSize(): int + { + return 100; + } + } + +.. note:: + + When the ``$ack`` argument of ``__invoke()`` is ``null``, the message is + expected to be handled synchronously. Otherwise, ``__invoke()`` is + expected to return the number of pending messages. The + :class:`Symfony\\Component\\Messenger\\Handler\\BatchHandlerTrait` handles + this for you. + +.. note:: + + By default, pending batches are flushed when the worker is idle as well + as when it is stopped. + +Extending Messenger +------------------- + +Envelopes & Stamps +~~~~~~~~~~~~~~~~~~ + +A message can be any PHP object. Sometimes, you may need to configure something +extra about the message - like the way it should be handled inside AMQP or adding +a delay before the message should be handled. You can do that by adding a "stamp" +to your message:: + + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\MessageBusInterface; + use Symfony\Component\Messenger\Stamp\DelayStamp; + + public function index(MessageBusInterface $bus): void + { + // wait 5 seconds before processing + $bus->dispatch(new SmsNotification('...'), [ + new DelayStamp(5000), + ]); + + // or explicitly create an Envelope + $bus->dispatch(new Envelope(new SmsNotification('...'), [ + new DelayStamp(5000), + ])); + + // ... + } + +Internally, each message is wrapped in an ``Envelope``, which holds the message +and stamps. You can create this manually or allow the message bus to do it. There +are a variety of different stamps for different purposes and they're used internally +to track information about a message - like the message bus that's handling it +or if it's being retried after failure. + +.. _messenger_middleware: + +Middleware +~~~~~~~~~~ + +What happens when you dispatch a message to a message bus depends on its +collection of middleware and their order. By default, the middleware configured +for each bus looks like this: + +#. ``add_bus_name_stamp_middleware`` - adds a stamp to record which bus this + message was dispatched into; + +#. ``dispatch_after_current_bus``- see :ref:`messenger-transactional-messages`; + +#. ``failed_message_processing_middleware`` - processes messages that are being + retried via the :ref:`failure transport ` to make + them properly function as if they were being received from their original transport; + +#. Your own collection of middleware_; + +#. ``send_message`` - if routing is configured for the transport, this sends + messages to that transport and stops the middleware chain; + +#. ``handle_message`` - calls the message handler(s) for the given message. + +.. note:: + + These middleware names are actually shortcut names. The real service ids + are prefixed with ``messenger.middleware.`` (e.g. ``messenger.middleware.handle_message``). + +The middleware are executed when the message is dispatched but *also* again when +a message is received via the worker (for messages that were sent to a transport +to be handled asynchronously). Keep this in mind if you create your own middleware. + +You can add your own middleware to this list, or completely disable the default +middleware and *only* include your own. + +If a middleware service is abstract, you can configure its constructor's arguments +and a different instance will be created per bus. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + buses: + messenger.bus.default: + # disable the default middleware + default_middleware: false + + middleware: + # use and configure parts of the default middleware you want + - 'add_bus_name_stamp_middleware': ['messenger.bus.default'] + + # add your own services that implement Symfony\Component\Messenger\Middleware\MiddlewareInterface + - 'App\Middleware\MyMiddleware' + - 'App\Middleware\AnotherMiddleware' + + .. code-block:: xml + + + + + + + + + + + + + messenger.bus.default + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $bus = $messenger->bus('messenger.bus.default') + ->defaultMiddleware(false); // disable the default middleware + + // use and configure parts of the default middleware you want + $bus->middleware()->id('add_bus_name_stamp_middleware')->arguments(['messenger.bus.default']); + + // add your own services that implement Symfony\Component\Messenger\Middleware\MiddlewareInterface + $bus->middleware()->id('App\Middleware\MyMiddleware'); + $bus->middleware()->id('App\Middleware\AnotherMiddleware'); + }; + +.. tip:: + + If you have installed the MakerBundle, you can use the ``make:messenger-middleware`` + command to bootstrap the creation of your own messenger middleware. + +.. _middleware-doctrine: + +Middleware for Doctrine +~~~~~~~~~~~~~~~~~~~~~~~ + +If you use Doctrine in your app, a number of optional middleware exist that you +may want to use: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + buses: + command_bus: + middleware: + # each time a message is handled, the Doctrine connection + # is "pinged" and reconnected if it's closed. Useful + # if your workers run for a long time and the database + # connection is sometimes lost + - doctrine_ping_connection + + # After handling, the Doctrine connection is closed, + # which can free up database connections in a worker, + # instead of keeping them open forever + - doctrine_close_connection + + # logs an error when a Doctrine transaction was opened but not closed + - doctrine_open_transaction_logger + + # wraps all handlers in a single Doctrine transaction + # handlers do not need to call flush() and an error + # in any handler will cause a rollback + - doctrine_transaction + + # or pass a different entity manager to any + #- doctrine_transaction: ['custom'] + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $bus = $messenger->bus('command_bus'); + $bus->middleware()->id('doctrine_transaction'); + $bus->middleware()->id('doctrine_ping_connection'); + $bus->middleware()->id('doctrine_close_connection'); + $bus->middleware()->id('doctrine_open_transaction_logger'); + // Using another entity manager + $bus->middleware()->id('doctrine_transaction') + ->arguments(['custom']); + }; + +Other Middlewares +~~~~~~~~~~~~~~~~~ + +Add the ``router_context`` middleware if you need to generate absolute URLs in +the consumer (e.g. render a template with links). This middleware stores the +original request context (i.e. the host, the HTTP port, etc.) which is needed +when building absolute URLs. + +Add the ``validation`` middleware if you need to validate the message +object using the :doc:`Validator component ` before handling it. +If validation fails, a ``ValidationFailedException`` will be thrown. The +:class:`Symfony\\Component\\Messenger\\Stamp\\ValidationStamp` can be used +to configure the validation groups. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + buses: + command_bus: + middleware: + - router_context + - validation + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $bus = $messenger->bus('command_bus'); + $bus->middleware()->id('router_context'); + $bus->middleware()->id('validation'); + }; + +Messenger Events +~~~~~~~~~~~~~~~~ + +In addition to middleware, Messenger also dispatches several events. You can +:doc:`create an event listener ` to hook into various parts +of the process. For each, the event class is the event name: + +* :class:`Symfony\\Component\\Messenger\\Event\\SendMessageToTransportsEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageFailedEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageHandledEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageReceivedEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageRetriedEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerRateLimitedEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerRunningEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerStartedEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerStoppedEvent` + +Additional Handler Arguments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It's possible to have messenger pass additional data to the message handler +using the :class:`Symfony\\Component\\Messenger\\Stamp\\HandlerArgumentsStamp`. +Add this stamp to the envelope in a middleware and fill it with any additional +data you want to have available in the handler:: + + // src/Messenger/AdditionalArgumentMiddleware.php + namespace App\Messenger; + + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\Middleware\MiddlewareInterface; + use Symfony\Component\Messenger\Middleware\StackInterface; + use Symfony\Component\Messenger\Stamp\HandlerArgumentsStamp; + + final class AdditionalArgumentMiddleware implements MiddlewareInterface + { + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + $envelope = $envelope->with(new HandlerArgumentsStamp([ + $this->resolveAdditionalArgument($envelope->getMessage()), + ])); + + return $stack->next()->handle($envelope, $stack); + } + + private function resolveAdditionalArgument(object $message): mixed + { + // ... + } + } + +Then your handler will look like this:: + + // src/MessageHandler/SmsNotificationHandler.php + namespace App\MessageHandler; + + use App\Message\SmsNotification; + + final class SmsNotificationHandler + { + public function __invoke(SmsNotification $message, mixed $additionalArgument) + { + // ... + } + } + +Message Serializer For Custom Data Formats +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you receive messages from other applications, it's possible that they are not +exactly in the format you need. Not all applications will return a JSON message +with ``body`` and ``headers`` fields. In those cases, you'll need to create a +new message serializer implementing the +:class:`Symfony\\Component\\Messenger\\Transport\\Serialization\\SerializerInterface`. +Let's say you want to create a message decoder:: + + namespace App\Messenger\Serializer; + + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; + + class MessageWithTokenDecoder implements SerializerInterface + { + public function decode(array $encodedEnvelope): Envelope + { + try { + // parse the data you received with your custom fields + $data = $encodedEnvelope['data']; + $data['token'] = $encodedEnvelope['token']; + + // other operations like getting information from stamps + } catch (\Throwable $throwable) { + // wrap any exception that may occur in the envelope to send it to the failure transport + return new Envelope($throwable); + } + + return new Envelope($data); + } + + public function encode(Envelope $envelope): array + { + // this decoder does not encode messages, but you can implement it by returning + // an array with serialized stamps if you need to send messages in a custom format + throw new \LogicException('This serializer is only used for decoding messages.'); + } + } + +The next step is to tell Symfony to use this serializer in one or more of your +transports: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + my_transport: + dsn: '%env(MY_TRANSPORT_DSN)%' + serializer: 'App\Messenger\Serializer\MessageWithTokenDecoder' + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use App\Messenger\Serializer\MessageWithTokenDecoder; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $messenger->transport('my_transport') + ->dsn('%env(MY_TRANSPORT_DSN)%') + ->serializer(MessageWithTokenDecoder::class); + }; + +.. _messenger-multiple-buses: + +Multiple Buses, Command & Event Buses +------------------------------------- + +Messenger gives you a single message bus service by default. But, you can configure +as many as you want, creating "command", "query" or "event" buses and controlling +their middleware. + +A common architecture when building applications is to separate commands from +queries. Commands are actions that do something and queries fetch data. This +is called CQRS (Command Query Responsibility Segregation). See Martin Fowler's +`article about CQRS`_ to learn more. This architecture could be used together +with the Messenger component by defining multiple buses. + +A **command bus** is a little different from a **query bus**. For example, command +buses usually don't provide any results and query buses are rarely asynchronous. +You can configure these buses and their rules by using middleware. + +It might also be a good idea to separate actions from reactions by introducing +an **event bus**. The event bus could have zero or more subscribers. + +.. configuration-block:: + + .. code-block:: yaml + + framework: + messenger: + # The bus that is going to be injected when injecting MessageBusInterface + default_bus: command.bus + buses: + command.bus: + middleware: + - validation + - doctrine_transaction + query.bus: + middleware: + - validation + event.bus: + default_middleware: + enabled: true + # set "allow_no_handlers" to true (default is false) to allow having + # no handler configured for this bus without throwing an exception + allow_no_handlers: false + # set "allow_no_senders" to false (default is true) to throw an exception + # if no sender is configured for this bus + allow_no_senders: true + middleware: + - validation + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // The bus that is going to be injected when injecting MessageBusInterface + $framework->messenger()->defaultBus('command.bus'); + + $commandBus = $framework->messenger()->bus('command.bus'); + $commandBus->middleware()->id('validation'); + $commandBus->middleware()->id('doctrine_transaction'); + + $queryBus = $framework->messenger()->bus('query.bus'); + $queryBus->middleware()->id('validation'); + + $eventBus = $framework->messenger()->bus('event.bus'); + $eventBus->defaultMiddleware() + ->enabled(true) + // set "allowNoHandlers" to true (default is false) to allow having + // no handler configured for this bus without throwing an exception + ->allowNoHandlers(false) + // set "allowNoSenders" to false (default is true) to throw an exception + // if no sender is configured for this bus + ->allowNoSenders(true) + ; + $eventBus->middleware()->id('validation'); + }; + +This will create three new services: + +* ``command.bus``: autowireable with the :class:`Symfony\\Component\\Messenger\\MessageBusInterface` + type-hint (because this is the ``default_bus``); + +* ``query.bus``: autowireable with ``MessageBusInterface $queryBus``; + +* ``event.bus``: autowireable with ``MessageBusInterface $eventBus``. + +Restrict Handlers per Bus +~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, each handler will be available to handle messages on *all* +of your buses. To prevent dispatching a message to the wrong bus without an error, +you can restrict each handler to a specific bus using the ``messenger.message_handler`` tag: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + App\MessageHandler\SomeCommandHandler: + tags: [{ name: messenger.message_handler, bus: command.bus }] + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + $container->services() + ->set(App\MessageHandler\SomeCommandHandler::class) + ->tag('messenger.message_handler', ['bus' => 'command.bus']); + +This way, the ``App\MessageHandler\SomeCommandHandler`` handler will only be +known by the ``command.bus`` bus. + +You can also automatically add this tag to a number of classes by using +the :ref:`_instanceof service configuration `. Using this, +you can determine the message bus based on an implemented interface: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + _instanceof: + # all services implementing the CommandHandlerInterface + # will be registered on the command.bus bus + App\MessageHandler\CommandHandlerInterface: + tags: + - { name: messenger.message_handler, bus: command.bus } + + # while those implementing QueryHandlerInterface will be + # registered on the query.bus bus + App\MessageHandler\QueryHandlerInterface: + tags: + - { name: messenger.message_handler, bus: query.bus } + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\MessageHandler\CommandHandlerInterface; + use App\MessageHandler\QueryHandlerInterface; + + return function(ContainerConfigurator $container): void { + $services = $container->services(); + + // ... + + // all services implementing the CommandHandlerInterface + // will be registered on the command.bus bus + $services->instanceof(CommandHandlerInterface::class) + ->tag('messenger.message_handler', ['bus' => 'command.bus']); + + // while those implementing QueryHandlerInterface will be + // registered on the query.bus bus + $services->instanceof(QueryHandlerInterface::class) + ->tag('messenger.message_handler', ['bus' => 'query.bus']); + }; + +Debugging the Buses +~~~~~~~~~~~~~~~~~~~ + +The ``debug:messenger`` command lists available messages & handlers per bus. +You can also restrict the list to a specific bus by providing its name as an argument. + +.. code-block:: terminal + + $ php bin/console debug:messenger + + Messenger + ========= + + command.bus + ----------- + + The following messages can be dispatched: + + --------------------------------------------------------------------------------------- + App\Message\DummyCommand + handled by App\MessageHandler\DummyCommandHandler + App\Message\MultipleBusesMessage + handled by App\MessageHandler\MultipleBusesMessageHandler + --------------------------------------------------------------------------------------- + + query.bus + --------- + + The following messages can be dispatched: + + --------------------------------------------------------------------------------------- + App\Message\DummyQuery + handled by App\MessageHandler\DummyQueryHandler + App\Message\MultipleBusesMessage + handled by App\MessageHandler\MultipleBusesMessageHandler + --------------------------------------------------------------------------------------- + +.. tip:: + + The command will also show the PHPDoc description of the message and handler classes. + +Redispatching a Message +----------------------- + +If you want to redispatch a message (using the same transport and envelope), create +a new :class:`Symfony\\Component\\Messenger\\Message\\RedispatchMessage` and dispatch +it through your bus. Reusing the same ``SmsNotification`` example shown earlier:: + + // src/MessageHandler/SmsNotificationHandler.php + namespace App\MessageHandler; + + use App\Message\SmsNotification; + use Symfony\Component\Messenger\Attribute\AsMessageHandler; + use Symfony\Component\Messenger\Message\RedispatchMessage; + use Symfony\Component\Messenger\MessageBusInterface; + + #[AsMessageHandler] + class SmsNotificationHandler + { + public function __construct(private MessageBusInterface $bus) + { + } + + public function __invoke(SmsNotification $message): void + { + // do something with the message + // then redispatch it based on your own logic + + if ($needsRedispatch) { + $this->bus->dispatch(new RedispatchMessage($message)); + } + } + } + +The built-in :class:`Symfony\\Component\\Messenger\\Handler\\RedispatchMessageHandler` +will take care of this message to redispatch it through the same bus it was +dispatched at first. You can also use the second argument of the ``RedispatchMessage`` +constructor to provide transports to use when redispatching the message. + +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /messenger/* + +.. _`Enqueue's transport`: https://github.com/sroze/messenger-enqueue-transport +.. _`streams`: https://redis.io/topics/streams-intro +.. _`Supervisor docs`: http://supervisord.org/ +.. _`PCNTL`: https://www.php.net/manual/book.pcntl.php +.. _`systemd docs`: https://systemd.io/ +.. _`SymfonyCasts' message serializer tutorial`: https://symfonycasts.com/screencast/messenger/transport-serializer +.. _`Long polling`: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-short-and-long-polling.html +.. _`Visibility Timeout`: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html +.. _`FIFO queue`: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html +.. _`LISTEN/NOTIFY`: https://www.postgresql.org/docs/current/sql-notify.html +.. _`AMQProxy`: https://github.com/cloudamqp/amqproxy +.. _`high connection churn`: https://www.rabbitmq.com/connections.html#high-connection-churn +.. _`article about CQRS`: https://martinfowler.com/bliki/CQRS.html +.. _`SSL context options`: https://php.net/context.ssl +.. _`predefined constants`: https://www.php.net/pcntl.constants +.. _`SQS CreateQueue API`: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_CreateQueue.html diff --git a/messenger/custom-transport.rst b/messenger/custom-transport.rst new file mode 100644 index 00000000000..7d1698126d1 --- /dev/null +++ b/messenger/custom-transport.rst @@ -0,0 +1,224 @@ +How to Create Your own Messenger Transport +========================================== + +Once you have written your transport's sender and receiver, you can register your +transport factory to be able to use it via a DSN in the Symfony application. + +Create your Transport Factory +----------------------------- + +You need to give FrameworkBundle the opportunity to create your transport from a +DSN. You will need a transport factory:: + + use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; + use Symfony\Component\Messenger\Transport\Sender\SenderInterface; + use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; + use Symfony\Component\Messenger\Transport\TransportFactoryInterface; + use Symfony\Component\Messenger\Transport\TransportInterface; + + class YourTransportFactory implements TransportFactoryInterface + { + public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface + { + return new YourTransport(/* ... */); + } + + public function supports(string $dsn, array $options): bool + { + return 0 === strpos($dsn, 'my-transport://'); + } + } + +The transport object needs to implement the +:class:`Symfony\\Component\\Messenger\\Transport\\TransportInterface` +(which combines the :class:`Symfony\\Component\\Messenger\\Transport\\Sender\\SenderInterface` +and :class:`Symfony\\Component\\Messenger\\Transport\\Receiver\\ReceiverInterface`). +Here is a simplified example of a database transport:: + + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp; + use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer; + use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; + use Symfony\Component\Messenger\Transport\TransportInterface; + use Symfony\Component\Uid\Uuid; + + class YourTransport implements TransportInterface + { + private SerializerInterface $serializer; + + /** + * @param FakeDatabase $db is used for demo purposes. It is not a real class. + */ + public function __construct( + private FakeDatabase $db, + ?SerializerInterface $serializer = null, + ) { + $this->serializer = $serializer ?? new PhpSerializer(); + } + + public function get(): iterable + { + // Get a message from "my_queue" + $row = $this->db->createQuery( + 'SELECT * + FROM my_queue + WHERE (delivered_at IS NULL OR delivered_at < :redeliver_timeout) + AND handled = FALSE' + ) + ->setParameter('redeliver_timeout', new DateTimeImmutable('-5 minutes')) + ->getOneOrNullResult(); + + if (null === $row) { + return []; + } + + $envelope = $this->serializer->decode([ + 'body' => $row['envelope'], + ]); + + return [$envelope->with(new TransportMessageIdStamp($row['id']))]; + } + + public function ack(Envelope $envelope): void + { + $stamp = $envelope->last(TransportMessageIdStamp::class); + if (!$stamp instanceof TransportMessageIdStamp) { + throw new \LogicException('No TransportMessageIdStamp found on the Envelope.'); + } + + // Mark the message as "handled" + $this->db->createQuery('UPDATE my_queue SET handled = TRUE WHERE id = :id') + ->setParameter('id', $stamp->getId()) + ->execute(); + } + + public function reject(Envelope $envelope): void + { + $stamp = $envelope->last(TransportMessageIdStamp::class); + if (!$stamp instanceof TransportMessageIdStamp) { + throw new \LogicException('No TransportMessageIdStamp found on the Envelope.'); + } + + // Delete the message from the "my_queue" table + $this->db->createQuery('DELETE FROM my_queue WHERE id = :id') + ->setParameter('id', $stamp->getId()) + ->execute(); + } + + public function send(Envelope $envelope): Envelope + { + $encodedMessage = $this->serializer->encode($envelope); + $uuid = (string) Uuid::v4(); + // Add a message to the "my_queue" table + $this->db->createQuery( + 'INSERT INTO my_queue (id, envelope, delivered_at, handled) + VALUES (:id, :envelope, NULL, FALSE)' + ) + ->setParameters([ + 'id' => $uuid, + 'envelope' => $encodedMessage['body'], + ]) + ->execute(); + + return $envelope->with(new TransportMessageIdStamp($uuid)); + } + } + +The implementation above is not runnable code but illustrates how a +:class:`Symfony\\Component\\Messenger\\Transport\\TransportInterface` could +be implemented. For real implementations see :class:`Symfony\\Component\\Messenger\\Transport\\InMemory\\InMemoryTransport` +and :class:`Symfony\\Component\\Messenger\\Bridge\\Doctrine\\Transport\\DoctrineReceiver`. + +Register your Factory +--------------------- + +Before using your factory, you must register it. If you're using the +:ref:`default services.yaml configuration `, +this is already done for you, thanks to :ref:`autoconfiguration `. +Otherwise, add the following: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + Your\Transport\YourTransportFactory: + tags: [messenger.transport_factory] + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + use Your\Transport\YourTransportFactory; + + $container->register(YourTransportFactory::class) + ->setTags(['messenger.transport_factory']); + +Use your Transport +------------------ + +Within the ``framework.messenger.transports.*`` configuration, create your +named transport using your own DSN: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + yours: 'my-transport://...' + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->messenger() + ->transport('yours') + ->dsn('my-transport://...') + ; + }; + +In addition of being able to route your messages to the ``yours`` sender, this +will give you access to the following services: + +#. ``messenger.sender.yours``: the sender; +#. ``messenger.receiver.yours``: the receiver. diff --git a/migration.rst b/migration.rst new file mode 100644 index 00000000000..44485248545 --- /dev/null +++ b/migration.rst @@ -0,0 +1,493 @@ +Migrating an Existing Application to Symfony +============================================ + +When you have an existing application that was not built with Symfony, +you might want to move over parts of that application without rewriting +the existing logic completely. For those cases there is a pattern called +`Strangler Fig Application`_. The basic idea of this pattern is to create a +new application that gradually takes over functionality from an existing +application. This migration approach can be implemented with Symfony in +various ways and has some benefits over a rewrite such as being able +to introduce new features in the existing application and reducing risk +by avoiding a "big bang"-release for the new application. + +.. admonition:: Screencast + :class: screencast + + The topic of migrating from an existing application towards Symfony is + sometimes discussed during conferences. For example the talk + `Modernizing with Symfony`_ reiterates some of the points from this page. + +Prerequisites +------------- + +Before you start introducing Symfony to the existing application, you have to +ensure certain requirements are met by your existing application and +environment. Making the decisions and preparing the environment before +starting the migration process is crucial for its success. + +.. note:: + + The following steps do not require you to have the new Symfony + application in place and in fact it might be safer to introduce these + changes beforehand in your existing application. + +Choosing the Target Symfony Version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Most importantly, this means that you will have to decide which version you +are aiming to migrate to, either a current stable release or the long +term support version (LTS). The main difference is, how frequently you +will need to upgrade in order to use a supported version. In the context +of a migration, other factors, such as the supported PHP-version or +support for libraries/bundles you use, may have a strong impact as well. +Using the most recent, stable release will likely give you more features, +but it will also require you to update more frequently to ensure you will +get support for bug fixes and security patches and you will have to work +faster on fixing deprecations to be able to upgrade. + +.. tip:: + + When upgrading to Symfony you might be tempted to also use + :ref:`Flex `. Please keep in mind that it primarily + focuses on bootstrapping a new Symfony application according to best + practices regarding the directory structure. When you work in the + constraints of an existing application you might not be able to + follow these constraints, making Flex less useful. + +First of all your environment needs to be able to support the minimum +requirements for both applications. In other words, when the Symfony +release you aim to use requires PHP 7.1 and your existing application +does not yet support this PHP version, you will probably have to upgrade +your legacy project. Use the ``check:requirements`` command to check if your +server meets the :ref:`technical requirements for running Symfony applications ` +and compare them with your current application's environment to make sure you +are able to run both applications on the same system. Having a test +system, that is as close to the production environment as possible, +where you can just install a new Symfony project next to the existing one +and check if it is working will give you an even more reliable result. + +.. tip:: + + If your current project is running on an older PHP version such as + PHP 5.x upgrading to a recent version will give you a performance + boost without having to change your code. + +Setting up Composer +~~~~~~~~~~~~~~~~~~~ + +Another point you will have to look out for is conflicts between +dependencies in both applications. This is especially important if your +existing application already uses Symfony components or libraries commonly +used in Symfony applications such as Doctrine ORM or Twig. +A good way for ensuring compatibility is to use the same ``composer.json`` +for both project's dependencies. + +Once you have introduced composer for managing your project's dependencies +you can use its autoloader to ensure you do not run into any conflicts due +to custom autoloading from your existing framework. This usually entails +adding an `autoload`_-section to your ``composer.json`` and configuring it +based on your application and replacing your custom logic with something +like this:: + + require __DIR__.'/vendor/autoload.php'; + +Removing Global State from the Legacy Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In older PHP applications it was quite common to rely on global state and +even mutate it during runtime. This might have side effects on the newly +introduced Symfony application. In other words code relying on globals +in the existing application should be refactored to allow for both systems +to work simultaneously. Since relying on global state is considered an +anti-pattern nowadays you might want to start working on this even before +doing any integration. + +Setting up the Environment +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There might be additional steps you need to take depending on the libraries +you use, the original framework your project is based on and most importantly +the age of the project as PHP itself underwent many improvements throughout +the years that your code might not have caught on to, yet. As long as both +your existing code and a new Symfony project can run in parallel on the +same system you are on a good way. All these steps do not require you to +introduce Symfony just yet and will already open up some opportunities for +modernizing your existing code. + +Establishing a Safety Net for Regressions +----------------------------------------- + +Before you can safely make changes to the existing code, you must ensure that +nothing will break. One reason for choosing to migrate is making sure that the +application is in a state where it can run at all times. The best way for +ensuring a working state is to establish automated tests. + +It is quite common for an existing application to either not have a test suite +at all or have low code coverage. Introducing unit tests for this code is +likely not cost effective as the old code might be replaced with functionality +from Symfony components or might be adapted to the new application. +Additionally legacy code tends to be hard to write tests for, making the process +slow and cumbersome. + +Instead of providing low level tests, that ensure each class works as expected, it +might makes sense to write high level tests ensuring that at least anything user +facing works on at least a superficial level. These kinds of tests are commonly +called End-to-End tests, because they cover the whole application from what the +user sees in the browser down to the very code that is being run and connected +services like a database. To automate this you have to make sure that you can +get a test instance of your system running as easily as possible and making +sure that external systems do not change your production environment, e.g. +provide a separate test database with (anonymized) data from a production +system or being able to setup a new schema with a basic dataset for your test +environment. Since these tests do not rely as much on isolating testable code +and instead look at the interconnected system, writing them is usually easier +and more productive when doing a migration. You can then limit your effort on +writing lower level tests on parts of the code that you have to change or +replace in the new application making sure it is testable right from the start. + +There are tools aimed at End-to-End testing you can use such as +`Symfony Panther`_ or you can write :doc:`functional tests ` +in the new Symfony application as soon as the initial setup is completed. +For example you can add so called Smoke Tests, which only ensure a certain +path is accessible by checking the HTTP status code returned or looking for +a text snippet from the page. + +Introducing Symfony to the Existing Application +----------------------------------------------- + +The following instructions only provide an outline of common tasks for +setting up a Symfony application that falls back to a legacy application +whenever a route is not accessible. Your mileage may vary and likely you +will need to adjust some of this or even provide additional configuration +or retrofitting to make it work with your application. This guide is not +supposed to be comprehensive and instead aims to be a starting point. + +.. tip:: + + If you get stuck or need additional help you can reach out to the + :doc:`Symfony community ` whenever you need + concrete feedback on an issue you are facing. + +Booting Symfony in a Front Controller +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When looking at how a typical PHP application is bootstrapped there are +two major approaches. Nowadays most frameworks provide a so called +front controller which acts as an entrypoint. No matter which URL-path +in your application you are going to, every request is being sent to +this front controller, which then determines which parts of your +application to load, e.g. which controller and action to call. This is +also the approach that Symfony takes with ``public/index.php`` being +the front controller. Especially in older applications it was common +that different paths were handled by different PHP files. + +In any case you have to create a ``public/index.php`` that will start +your Symfony application by either copying the file from the +``FrameworkBundle``-recipe or by using Flex and requiring the +FrameworkBundle. You will also likely have to update your web server +(e.g. Apache or nginx) to always use this front controller. You can +look at :doc:`Web Server Configuration ` +for examples on how this might look. For example when using Apache you can +use Rewrite Rules to ensure PHP files are ignored and instead only index.php +is called: + +.. code-block:: apache + + RewriteEngine On + + RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ + RewriteRule ^(.*) - [E=BASE:%1] + + RewriteCond %{ENV:REDIRECT_STATUS} ^$ + RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L] + + RewriteRule ^index\.php - [L] + + RewriteCond %{REQUEST_FILENAME} -f + RewriteCond %{REQUEST_FILENAME} !^.+\.php$ + RewriteRule ^ - [L] + + RewriteRule ^ %{ENV:BASE}/index.php [L] + +This change will make sure that from now on your Symfony application is +the first one handling all requests. The next step is to make sure that +your existing application is started and taking over whenever Symfony +can not yet handle a path previously managed by the existing application. + +From this point, many tactics are possible and every project requires its +unique approach for migration. This guide shows two examples of commonly used +approaches, which you can use as a base for your own approach: + +* `Front Controller with Legacy Bridge`_, which leaves the legacy application + untouched and allows migrating it in phases to the Symfony application. +* `Legacy Route Loader`_, where the legacy application is integrated in phases + into Symfony, with a fully integrated final result. + +Front Controller with Legacy Bridge +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once you have a running Symfony application that takes over all requests, +falling back to your legacy application is done by extending the original front +controller script with some logic for going to your legacy system. The file +could look something like this:: + + // public/index.php + use App\Kernel; + use App\LegacyBridge; + use Symfony\Component\Dotenv\Dotenv; + use Symfony\Component\ErrorHandler\Debug; + use Symfony\Component\HttpFoundation\Request; + + require dirname(__DIR__).'/vendor/autoload.php'; + + (new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); + + /* + * The kernel will always be available globally, allowing you to + * access it from your existing application and through it the + * service container. This allows for introducing new features in + * the existing application. + */ + global $kernel; + + if ($_SERVER['APP_DEBUG']) { + umask(0000); + + Debug::enable(); + } + + if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) { + Request::setTrustedProxies( + explode(',', $trustedProxies), + Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO + ); + } + + if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false) { + Request::setTrustedHosts([$trustedHosts]); + } + + $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); + $request = Request::createFromGlobals(); + $response = $kernel->handle($request); + + if (false === $response->isNotFound()) { + // Symfony successfully handled the route. + $response->send(); + } else { + LegacyBridge::handleRequest($request, $response, __DIR__); + } + + $kernel->terminate($request, $response); + +There are 2 major deviations from the original file: + +Line 18 + First of all, ``$kernel`` is made globally available. This allows you to use + Symfony features inside your existing application and gives access to + services configured in our Symfony application. This helps you prepare your + own code to work better within the Symfony application before you transition + it over. For instance, by replacing outdated or redundant libraries with + Symfony components. + +Line 41 - 46 + If Symfony handled the response, it is sent; otherwise, the ``LegacyBridge`` + handles the request. + +This legacy bridge is responsible for figuring out which file should be loaded +in order to process the old application logic. This can either be a front +controller similar to Symfony's ``public/index.php`` or a specific script file +based on the current route. The basic outline of this LegacyBridge could look +somewhat like this:: + + // src/LegacyBridge.php + namespace App; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + class LegacyBridge + { + + /** + * Map the incoming request to the right file. This is the + * key function of the LegacyBridge. + * + * Sample code only. Your implementation will vary, depending on the + * architecture of the legacy code and how it's executed. + * + * If your mapping is complicated, you may want to write unit tests + * to verify your logic, hence this is public static. + */ + public static function getLegacyScript(Request $request): string + { + $requestPathInfo = $request->getPathInfo(); + $legacyRoot = __DIR__ . '/../'; + + // Map a route to a legacy script: + if ($requestPathInfo == '/customer/') { + return "{$legacyRoot}src/customers/list.php"; + } + + // Map a direct file call, e.g. an ajax call: + if ($requestPathInfo == 'inc/ajax_cust_details.php') { + return "{$legacyRoot}inc/ajax_cust_details.php"; + } + + // ... etc. + + throw new \Exception("Unhandled legacy mapping for $requestPathInfo"); + } + + public static function handleRequest(Request $request, Response $response, string $publicDirectory): void + { + $legacyScriptFilename = LegacyBridge::getLegacyScript($request); + + // Possibly (re-)set some env vars (e.g. to handle forms + // posting to PHP_SELF): + $p = $request->getPathInfo(); + $_SERVER['PHP_SELF'] = $p; + $_SERVER['SCRIPT_NAME'] = $p; + $_SERVER['SCRIPT_FILENAME'] = $legacyScriptFilename; + + require $legacyScriptFilename; + } + } + +This is the most generic approach you can take, that is likely to work +no matter what your previous system was. You might have to account for +certain "quirks", but since your original application is only started +after Symfony finished handling the request you reduced the chances +for side effects and any interference. + +Since the old script is called in the global variable scope it will reduce side +effects on the old code which can sometimes require variables from the global +scope. At the same time, because your Symfony application will always be +booted first, you can access the container via the ``$kernel`` variable and +then fetch any service (using :method:`Symfony\\Component\\HttpKernel\\KernelInterface::getContainer`). +This can be helpful if you want to introduce new features to your legacy +application, without switching over the whole action to the new application. +For example, you could now use the Symfony Translator in your old application +or instead of using your old database logic, you could use Doctrine to refactor +old queries. This will also allow you to incrementally improve the legacy code +making it easier to transition it over to the new Symfony application. + +The major downside is, that both systems are not well integrated +into each other leading to some redundancies and possibly duplicated code. +For example, since the Symfony application is already done handling the +request you can not take advantage of kernel events or utilize Symfony's +routing for determining which legacy script to call. + +Legacy Route Loader +~~~~~~~~~~~~~~~~~~~ + +The major difference to the LegacyBridge-approach from before is, that the +logic is moved inside the Symfony application. It removes some of the +redundancies and allows us to also interact with parts of the legacy +application from inside Symfony, instead of just the other way around. + +.. tip:: + + The following route loader is just a generic example that you might + have to tweak for your legacy application. You can familiarize + yourself with the concepts by reading up on it in :doc:`Routing `. + +The legacy route loader is :doc:`a custom route loader `. +The legacy route loader has a similar functionality as the previous +LegacyBridge, but it is a service that is registered inside Symfony's Routing +component:: + + // src/Legacy/LegacyRouteLoader.php + namespace App\Legacy; + + use Symfony\Component\Config\Loader\Loader; + use Symfony\Component\Routing\Route; + use Symfony\Component\Routing\RouteCollection; + + class LegacyRouteLoader extends Loader + { + // ... + + public function load($resource, $type = null): RouteCollection + { + $collection = new RouteCollection(); + $finder = new Finder(); + $finder->files()->name('*.php'); + + /** @var SplFileInfo $legacyScriptFile */ + foreach ($finder->in($this->webDir) as $legacyScriptFile) { + // This assumes all legacy files use ".php" as extension + $filename = basename($legacyScriptFile->getRelativePathname(), '.php'); + $routeName = sprintf('app.legacy.%s', str_replace('/', '__', $filename)); + + $collection->add($routeName, new Route($legacyScriptFile->getRelativePathname(), [ + '_controller' => 'App\Controller\LegacyController::loadLegacyScript', + 'requestPath' => '/' . $legacyScriptFile->getRelativePathname(), + 'legacyScript' => $legacyScriptFile->getPathname(), + ])); + } + + return $collection; + } + } + +You will also have to register the loader in your application's +``routing.yaml`` as described in the documentation for +:doc:`Custom Route Loaders `. +Depending on your configuration, you might also have to tag the service with +``routing.loader``. Afterwards you should be able to see all the legacy routes +in your route configuration, e.g. when you call the ``debug:router``-command: + +.. code-block:: terminal + + $ php bin/console debug:router + +In order to use these routes you will need to create a controller that handles +these routes. You might have noticed the ``_controller`` attribute in the +previous code example, which tells Symfony which Controller to call whenever it +tries to access one of our legacy routes. The controller itself can then use the +other route attributes (i.e. ``requestPath`` and ``legacyScript``) to determine +which script to call and wrap the output in a response class:: + + // src/Controller/LegacyController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\StreamedResponse; + + class LegacyController + { + public function loadLegacyScript(string $requestPath, string $legacyScript): StreamedResponse + { + return new StreamedResponse( + function () use ($requestPath, $legacyScript): void { + $_SERVER['PHP_SELF'] = $requestPath; + $_SERVER['SCRIPT_NAME'] = $requestPath; + $_SERVER['SCRIPT_FILENAME'] = $legacyScript; + + chdir(dirname($legacyScript)); + + require $legacyScript; + } + ); + } + } + +This controller will set some server variables that might be needed by +the legacy application. This will simulate the legacy script being called +directly, in case it relies on these variables (e.g. when determining +relative paths or file names). Finally the action requires the old script, +which essentially calls the original script as before, but it runs inside +our current application scope, instead of the global scope. + +There are some risks to this approach, as it is no longer run in the global +scope. However, since the legacy code now runs inside a controller action, you gain +access to many functionalities from the new Symfony application, including the +chance to use Symfony's event lifecycle. For instance, this allows you to +transition the authentication and authorization of the legacy application over +to the Symfony application using the Security component and its firewalls. + +.. _`Strangler Fig Application`: https://martinfowler.com/bliki/StranglerFigApplication.html +.. _`autoload`: https://getcomposer.org/doc/04-schema.md#autoload +.. _`Modernizing with Symfony`: https://youtu.be/YzyiZNY9htQ +.. _`Symfony Panther`: https://github.com/symfony/panther diff --git a/notifier.rst b/notifier.rst new file mode 100644 index 00000000000..49a1c2d533b --- /dev/null +++ b/notifier.rst @@ -0,0 +1,1350 @@ +Creating and Sending Notifications +================================== + +Installation +------------ + +Current web applications use many different channels to send messages to +the users (e.g. SMS, Slack messages, emails, push notifications, etc.). The +Notifier component in Symfony is an abstraction on top of all these +channels. It provides a dynamic way to manage how the messages are sent. +Get the Notifier installed using: + +.. code-block:: terminal + + $ composer require symfony/notifier + +.. _channels-chatters-texters-email-and-browser: +.. _channels-chatters-texters-email-browser-and-push: + +Channels +-------- + +Channels refer to the different mediums through which notifications can be delivered. +These channels include email, SMS, chat services, push notifications, etc. Each +channel can integrate with different providers (e.g. Slack or Twilio SMS) by +using transports. + +The notifier component supports the following channels: + +* :ref:`SMS channel ` sends notifications to phones via + SMS messages; +* :ref:`Chat channel ` sends notifications to chat + services like Slack and Telegram; +* :ref:`Email channel ` integrates the :doc:`Symfony Mailer `; +* Browser channel uses :ref:`flash messages `. +* :ref:`Push channel ` sends notifications to phones and + browsers via push notifications. +* :ref:`Desktop channel ` displays desktop notifications + on the same host machine. + +.. versionadded:: 7.2 + + The ``Desktop`` channel was introduced in Symfony 7.2. + +.. _notifier-sms-channel: + +SMS Channel +~~~~~~~~~~~ + +The SMS channel uses :class:`Symfony\\Component\\Notifier\\Texter` classes +to send SMS messages to mobile phones. This feature requires subscribing to +a third-party service that sends SMS messages. Symfony provides integration +with a couple popular SMS services: + +.. warning:: + + If any of the DSN values contains any character considered special in a + URI (such as ``: / ? # [ ] @ ! $ & ' ( ) * + , ; =``), you must + encode them. See `RFC 3986`_ for the full list of reserved characters or use the + :phpfunction:`urlencode` function to encode them. + +================== ==================================================================================================================================== +Service +================== ==================================================================================================================================== +`46elks`_ **Install**: ``composer require symfony/forty-six-elks-notifier`` \ + **DSN**: ``forty-six-elks://API_USERNAME:API_PASSWORD@default?from=FROM`` \ + **Webhook support**: No +`AllMySms`_ **Install**: ``composer require symfony/all-my-sms-notifier`` \ + **DSN**: ``allmysms://LOGIN:APIKEY@default?from=FROM`` \ + **Webhook support**: No + **Extra properties in SentMessage**: ``nbSms``, ``balance``, ``cost`` +`AmazonSns`_ **Install**: ``composer require symfony/amazon-sns-notifier`` \ + **DSN**: ``sns://ACCESS_KEY:SECRET_KEY@default?region=REGION`` \ + **Webhook support**: No +`Bandwidth`_ **Install**: ``composer require symfony/bandwidth-notifier`` \ + **DSN**: ``bandwidth://USERNAME:PASSWORD@default?from=FROM&account_id=ACCOUNT_ID&application_id=APPLICATION_ID&priority=PRIORITY`` \ + **Webhook support**: No +`Brevo`_ **Install**: ``composer require symfony/brevo-notifier`` \ + **DSN**: ``brevo://API_KEY@default?sender=SENDER`` \ + **Webhook support**: Yes +`Clickatell`_ **Install**: ``composer require symfony/clickatell-notifier`` \ + **DSN**: ``clickatell://ACCESS_TOKEN@default?from=FROM`` \ + **Webhook support**: No +`ContactEveryone`_ **Install**: ``composer require symfony/contact-everyone-notifier`` \ + **DSN**: ``contact-everyone://TOKEN@default?&diffusionname=DIFFUSION_NAME&category=CATEGORY`` \ + **Webhook support**: No +`Esendex`_ **Install**: ``composer require symfony/esendex-notifier`` \ + **DSN**: ``esendex://USER_NAME:PASSWORD@default?accountreference=ACCOUNT_REFERENCE&from=FROM`` \ + **Webhook support**: No +`FakeSms`_ **Install**: ``composer require symfony/fake-sms-notifier`` \ + **DSN**: ``fakesms+email://MAILER_SERVICE_ID?to=TO&from=FROM`` or ``fakesms+logger://default`` \ + **Webhook support**: No +`FreeMobile`_ **Install**: ``composer require symfony/free-mobile-notifier`` \ + **DSN**: ``freemobile://LOGIN:API_KEY@default?phone=PHONE`` \ + **Webhook support**: No +`GatewayApi`_ **Install**: ``composer require symfony/gateway-api-notifier`` \ + **DSN**: ``gatewayapi://TOKEN@default?from=FROM`` \ + **Webhook support**: No +`GoIP`_ **Install**: ``composer require symfony/go-ip-notifier`` \ + **DSN**: ``goip://USERNAME:PASSWORD@HOST:80?sim_slot=SIM_SLOT`` \ + **Webhook support**: No +`Infobip`_ **Install**: ``composer require symfony/infobip-notifier`` \ + **DSN**: ``infobip://AUTH_TOKEN@HOST?from=FROM`` \ + **Webhook support**: No +`Iqsms`_ **Install**: ``composer require symfony/iqsms-notifier`` \ + **DSN**: ``iqsms://LOGIN:PASSWORD@default?from=FROM`` \ + **Webhook support**: No +`iSendPro`_ **Install**: ``composer require symfony/isendpro-notifier`` \ + **DSN**: ``isendpro://ACCOUNT_KEY_ID@default?from=FROM&no_stop=NO_STOP&sandbox=SANDBOX`` \ + **Webhook support**: No +`KazInfoTeh`_ **Install**: ``composer require symfony/kaz-info-teh-notifier`` \ + **DSN**: ``kaz-info-teh://USERNAME:PASSWORD@default?sender=FROM`` \ + **Webhook support**: No +`LightSms`_ **Install**: ``composer require symfony/light-sms-notifier`` \ + **DSN**: ``lightsms://LOGIN:TOKEN@default?from=PHONE`` \ + **Webhook support**: No +`LOX24`_ **Install**: ``composer require symfony/lox24-notifier`` \ + **DSN**: ``lox24://USER:TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Mailjet`_ **Install**: ``composer require symfony/mailjet-notifier`` \ + **DSN**: ``mailjet://TOKEN@default?from=FROM`` \ + **Webhook support**: No +`MessageBird`_ **Install**: ``composer require symfony/message-bird-notifier`` \ + **DSN**: ``messagebird://TOKEN@default?from=FROM`` \ + **Webhook support**: No +`MessageMedia`_ **Install**: ``composer require symfony/message-media-notifier`` \ + **DSN**: ``messagemedia://API_KEY:API_SECRET@default?from=FROM`` \ + **Webhook support**: No +`Mobyt`_ **Install**: ``composer require symfony/mobyt-notifier`` \ + **DSN**: ``mobyt://USER_KEY:ACCESS_TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Nexmo`_ **Install**: ``composer require symfony/nexmo-notifier`` \ + Abandoned in favor of Vonage (see below) \ +`Octopush`_ **Install**: ``composer require symfony/octopush-notifier`` \ + **DSN**: ``octopush://USERLOGIN:APIKEY@default?from=FROM&type=TYPE`` \ + **Webhook support**: No +`OrangeSms`_ **Install**: ``composer require symfony/orange-sms-notifier`` \ + **DSN**: ``orange-sms://CLIENT_ID:CLIENT_SECRET@default?from=FROM&sender_name=SENDER_NAME`` \ + **Webhook support**: No +`OvhCloud`_ **Install**: ``composer require symfony/ovh-cloud-notifier`` \ + **DSN**: ``ovhcloud://APPLICATION_KEY:APPLICATION_SECRET@default?consumer_key=CONSUMER_KEY&service_name=SERVICE_NAME`` \ + **Webhook support**: No + **Extra properties in SentMessage**:: ``totalCreditsRemoved`` +`Plivo`_ **Install**: ``composer require symfony/plivo-notifier`` \ + **DSN**: ``plivo://AUTH_ID:AUTH_TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Primotexto`_ **Install**: ``composer require symfony/primotexto-notifier`` \ + **DSN**: ``primotexto://API_KEY@default?from=FROM`` \ + **Webhook support**: No +`Redlink`_ **Install**: ``composer require symfony/redlink-notifier`` \ + **DSN**: ``redlink://API_KEY:APP_KEY@default?from=SENDER_NAME&version=API_VERSION`` \ + **Webhook support**: No +`RingCentral`_ **Install**: ``composer require symfony/ring-central-notifier`` \ + **DSN**: ``ringcentral://API_TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Sendberry`_ **Install**: ``composer require symfony/sendberry-notifier`` \ + **DSN**: ``sendberry://USERNAME:PASSWORD@default?auth_key=AUTH_KEY&from=FROM`` \ + **Webhook support**: No +`Sendinblue`_ **Install**: ``composer require symfony/sendinblue-notifier`` \ + **DSN**: ``sendinblue://API_KEY@default?sender=PHONE`` \ + **Webhook support**: No +`Sms77`_ **Install**: ``composer require symfony/sms77-notifier`` \ + **DSN**: ``sms77://API_KEY@default?from=FROM`` \ + **Webhook support**: No +`SimpleTextin`_ **Install**: ``composer require symfony/simple-textin-notifier`` \ + **DSN**: ``simpletextin://API_KEY@default?from=FROM`` \ + **Webhook support**: No +`Sinch`_ **Install**: ``composer require symfony/sinch-notifier`` \ + **DSN**: ``sinch://ACCOUNT_ID:AUTH_TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Sipgate`_ **Install**: ``composer require symfony/sipgate-notifier`` \ + **DSN**: ``sipgate://TOKEN_ID:TOKEN@default?senderId=SENDER_ID`` \ + **Webhook support**: No +`SmsSluzba`_ **Install**: ``composer require symfony/sms-sluzba-notifier`` \ + **DSN**: ``sms-sluzba://USERNAME:PASSWORD@default`` \ + **Webhook support**: No +`Smsapi`_ **Install**: ``composer require symfony/smsapi-notifier`` \ + **DSN**: ``smsapi://TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Smsbox`_ **Install**: ``composer require symfony/smsbox-notifier`` \ + **DSN**: ``smsbox://APIKEY@default?mode=MODE&strategy=STRATEGY&sender=SENDER`` \ + **Webhook support**: Yes +`SmsBiuras`_ **Install**: ``composer require symfony/sms-biuras-notifier`` \ + **DSN**: ``smsbiuras://UID:API_KEY@default?from=FROM&test_mode=0`` \ + **Webhook support**: No +`Smsc`_ **Install**: ``composer require symfony/smsc-notifier`` \ + **DSN**: ``smsc://LOGIN:PASSWORD@default?from=FROM`` \ + **Webhook support**: No +`SMSense`_ **Install**: ``composer require smsense-notifier`` \ + **DSN**: ``smsense://API_TOKEN@default?from=FROM`` \ + **Webhook support**: No +`SMSFactor`_ **Install**: ``composer require symfony/sms-factor-notifier`` \ + **DSN**: ``sms-factor://TOKEN@default?sender=SENDER&push_type=PUSH_TYPE`` \ + **Webhook support**: No +`SpotHit`_ **Install**: ``composer require symfony/spot-hit-notifier`` \ + **DSN**: ``spothit://TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Sweego`_ **Install**: ``composer require symfony/sweego-notifier`` \ + **DSN**: ``sweego://API_KEY@default?region=REGION&campaign_type=CAMPAIGN_TYPE`` \ + **Webhook support**: Yes +`Telnyx`_ **Install**: ``composer require symfony/telnyx-notifier`` \ + **DSN**: ``telnyx://API_KEY@default?from=FROM&messaging_profile_id=MESSAGING_PROFILE_ID`` \ + **Webhook support**: No +`TurboSms`_ **Install**: ``composer require symfony/turbo-sms-notifier`` \ + **DSN**: ``turbosms://AUTH_TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Twilio`_ **Install**: ``composer require symfony/twilio-notifier`` \ + **DSN**: ``twilio://SID:TOKEN@default?from=FROM`` \ + **Webhook support**: Yes +`Unifonic`_ **Install**: ``composer require symfony/unifonic-notifier`` \ + **DSN**: ``unifonic://APP_SID@default?from=FROM`` \ + **Webhook support**: No +`Vonage`_ **Install**: ``composer require symfony/vonage-notifier`` \ + **DSN**: ``vonage://KEY:SECRET@default?from=FROM`` \ + **Webhook support**: Yes +`Yunpian`_ **Install**: ``composer require symfony/yunpian-notifier`` \ + **DSN**: ``yunpian://APIKEY@default`` \ + **Webhook support**: No +================== ==================================================================================================================================== + +.. tip:: + + Use :doc:`Symfony configuration secrets ` to securely + store your API tokens. + +.. tip:: + + Some third party transports, when using the API, support status callbacks + via webhooks. See the :doc:`Webhook documentation ` for more + details. + +.. versionadded:: 7.1 + + The ``Smsbox``, ``SmsSluzba``, ``SMSense``, ``LOX24`` and ``Unifonic`` + integrations were introduced in Symfony 7.1. + +.. versionadded:: 7.2 + + The ``Primotexto``, ``Sipgate`` and ``Sweego`` integrations were introduced in Symfony 7.2. + +.. versionadded:: 7.3 + + Webhook support for the ``Brevo`` integration was introduced in Symfony 7.3. + The extra properties in ``SentMessage`` for ``AllMySms`` and ``OvhCloud`` + providers were introduced in Symfony 7.3 too. + +.. deprecated:: 7.1 + + The `Sms77`_ integration is deprecated since + Symfony 7.1, use the `Seven.io`_ integration instead. + +To enable a texter, add the correct DSN in your ``.env`` file and +configure the ``texter_transports``: + +.. code-block:: bash + + # .env + TWILIO_DSN=twilio://SID:TOKEN@default?from=FROM + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/notifier.yaml + framework: + notifier: + texter_transports: + twilio: '%env(TWILIO_DSN)%' + + .. code-block:: xml + + + + + + + + + %env(TWILIO_DSN)% + + + + + + .. code-block:: php + + // config/packages/notifier.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->notifier() + ->texterTransport('twilio', env('TWILIO_DSN')) + ; + }; + +.. _sending-sms: + +The :class:`Symfony\\Component\\Notifier\\TexterInterface` class allows you to +send SMS messages:: + + // src/Controller/SecurityController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Notifier\Message\SmsMessage; + use Symfony\Component\Notifier\TexterInterface; + use Symfony\Component\Routing\Attribute\Route; + + class SecurityController + { + #[Route('/login/success')] + public function loginSuccess(TexterInterface $texter): Response + { + $options = (new ProviderOptions()) + ->setPriority('high') + ; + + $sms = new SmsMessage( + // the phone number to send the SMS message to + '+1411111111', + // the message + 'A new login was detected!', + // optionally, you can override default "from" defined in transports + '+1422222222', + // you can also add options object implementing MessageOptionsInterface + $options + ); + + $sentMessage = $texter->send($sms); + + // ... + } + } + +The ``send()`` method returns a variable of type +:class:`Symfony\\Component\\Notifier\\Message\\SentMessage` which provides +information such as the message ID and the original message contents. + +.. _notifier-chat-channel: + +Chat Channel +~~~~~~~~~~~~ + +.. warning:: + + If any of the DSN values contains any character considered special in a + URI (such as ``: / ? # [ ] @ ! $ & ' ( ) * + , ; =``), you must + encode them. See `RFC 3986`_ for the full list of reserved characters or use the + :phpfunction:`urlencode` function to encode them. + +The chat channel is used to send chat messages to users by using +:class:`Symfony\\Component\\Notifier\\Chatter` classes. Symfony provides +integration with these chat services: + +====================================== ===================================================================================== +Service +====================================== ===================================================================================== +`AmazonSns`_ **Install**: ``composer require symfony/amazon-sns-notifier`` \ + **DSN**: ``sns://ACCESS_KEY:SECRET_KEY@default?region=REGION`` +`Bluesky`_ **Install**: ``composer require symfony/bluesky-notifier`` \ + **DSN**: ``bluesky://USERNAME:PASSWORD@default`` + **Extra properties in SentMessage**: ``cid`` +`Chatwork`_ **Install**: ``composer require symfony/chatwork-notifier`` \ + **DSN**: ``chatwork://API_TOKEN@default?room_id=ID`` +`Discord`_ **Install**: ``composer require symfony/discord-notifier`` \ + **DSN**: ``discord://TOKEN@default?webhook_id=ID`` +`FakeChat`_ **Install**: ``composer require symfony/fake-chat-notifier`` \ + **DSN**: ``fakechat+email://default?to=TO&from=FROM`` or ``fakechat+logger://default`` +`Firebase`_ **Install**: ``composer require symfony/firebase-notifier`` \ + **DSN**: ``firebase://USERNAME:PASSWORD@default`` +`GoogleChat`_ **Install**: ``composer require symfony/google-chat-notifier`` \ + **DSN**: ``googlechat://ACCESS_KEY:ACCESS_TOKEN@default/SPACE?thread_key=THREAD_KEY`` +`LINE Bot`_ **Install**: ``composer require symfony/line-bot-notifier`` \ + **DSN**: ``linebot://TOKEN@default?receiver=RECEIVER`` +`LINE Notify`_ **Install**: ``composer require symfony/line-notify-notifier`` \ + **DSN**: ``linenotify://TOKEN@default`` +`LinkedIn`_ **Install**: ``composer require symfony/linked-in-notifier`` \ + **DSN**: ``linkedin://TOKEN:USER_ID@default`` +`Mastodon`_ **Install**: ``composer require symfony/mastodon-notifier`` \ + **DSN**: ``mastodon://ACCESS_TOKEN@HOST`` +`Matrix`_ **Install**: ``composer require symfony/matrix-notifier`` \ + **DSN**: ``matrix://HOST:PORT/?accessToken=ACCESSTOKEN&ssl=SSL`` +`Mattermost`_ **Install**: ``composer require symfony/mattermost-notifier`` \ + **DSN**: ``mattermost://ACCESS_TOKEN@HOST/PATH?channel=CHANNEL`` +`Mercure`_ **Install**: ``composer require symfony/mercure-notifier`` \ + **DSN**: ``mercure://HUB_ID?topic=TOPIC`` +`MicrosoftTeams`_ **Install**: ``composer require symfony/microsoft-teams-notifier`` \ + **DSN**: ``microsoftteams://default/PATH`` +`RocketChat`_ **Install**: ``composer require symfony/rocket-chat-notifier`` \ + **DSN**: ``rocketchat://TOKEN@ENDPOINT?channel=CHANNEL`` +`Slack`_ **Install**: ``composer require symfony/slack-notifier`` \ + **DSN**: ``slack://TOKEN@default?channel=CHANNEL`` +`Telegram`_ **Install**: ``composer require symfony/telegram-notifier`` \ + **DSN**: ``telegram://TOKEN@default?channel=CHAT_ID`` +`Twitter`_ **Install**: ``composer require symfony/twitter-notifier`` \ + **DSN**: ``twitter://API_KEY:API_SECRET:ACCESS_TOKEN:ACCESS_SECRET@default`` +`Zendesk`_ **Install**: ``composer require symfony/zendesk-notifier`` \ + **DSN**: ``zendesk://EMAIL:TOKEN@SUBDOMAIN`` +`Zulip`_ **Install**: ``composer require symfony/zulip-notifier`` \ + **DSN**: ``zulip://EMAIL:TOKEN@HOST?channel=CHANNEL`` +====================================== ===================================================================================== + +.. versionadded:: 7.1 + + The ``Bluesky`` integration was introduced in Symfony 7.1. + +.. versionadded:: 7.2 + + The ``LINE Bot`` integration was introduced in Symfony 7.2. + +.. deprecated:: 7.2 + + The ``Gitter`` integration was removed in Symfony 7.2 because that service + no longer provides an API. + +.. versionadded:: 7.3 + + The ``Matrix`` integration was introduced in Symfony 7.3. + +.. warning:: + + By default, if you have the :doc:`Messenger component ` installed, + the notifications will be sent through the MessageBus. If you don't have a + message consumer running, messages will never be sent. + + To change this behavior, add the following configuration to send messages + directly via the transport: + + .. code-block:: yaml + + # config/packages/notifier.yaml + framework: + notifier: + message_bus: false + +Chatters are configured using the ``chatter_transports`` setting: + +.. code-block:: bash + + # .env + SLACK_DSN=slack://TOKEN@default?channel=CHANNEL + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/notifier.yaml + framework: + notifier: + chatter_transports: + slack: '%env(SLACK_DSN)%' + + .. code-block:: xml + + + + + + + + + %env(SLACK_DSN)% + + + + + + .. code-block:: php + + // config/packages/notifier.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->notifier() + ->chatterTransport('slack', env('SLACK_DSN')) + ; + }; + +.. _sending-chat-messages: + +The :class:`Symfony\\Component\\Notifier\\ChatterInterface` class allows +you to send messages to chat services:: + + // src/Controller/CheckoutController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Notifier\ChatterInterface; + use Symfony\Component\Notifier\Message\ChatMessage; + use Symfony\Component\Routing\Attribute\Route; + + class CheckoutController extends AbstractController + { + #[Route('/checkout/thankyou')] + public function thankyou(ChatterInterface $chatter): Response + { + $message = (new ChatMessage('You got a new invoice for 15 EUR.')) + // if not set explicitly, the message is sent to the + // default transport (the first one configured) + ->transport('slack'); + + $sentMessage = $chatter->send($message); + + // ... + } + } + +The ``send()`` method returns a variable of type +:class:`Symfony\\Component\\Notifier\\Message\\SentMessage` which provides +information such as the message ID and the original message contents. + +.. _notifier-email-channel: + +Email Channel +~~~~~~~~~~~~~ + +The email channel uses the :doc:`Symfony Mailer ` to send +notifications using the special +:class:`Symfony\\Bridge\\Twig\\Mime\\NotificationEmail`. It is +required to install the Twig bridge along with the Inky and CSS Inliner +Twig extensions: + +.. code-block:: terminal + + $ composer require symfony/twig-pack twig/cssinliner-extra twig/inky-extra + +After this, :ref:`configure the mailer `. You can +also set the default "from" email address that should be used to send the +notification emails: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mailer.yaml + framework: + mailer: + dsn: '%env(MAILER_DSN)%' + envelope: + sender: 'notifications@example.com' + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/mailer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->mailer() + ->dsn(env('MAILER_DSN')) + ->envelope() + ->sender('notifications@example.com') + ; + }; + +.. _notifier-push-channel: + +Push Channel +~~~~~~~~~~~~ + +.. warning:: + + If any of the DSN values contains any character considered special in a + URI (such as ``: / ? # [ ] @ ! $ & ' ( ) * + , ; =``), you must + encode them. See `RFC 3986`_ for the full list of reserved characters or use the + :phpfunction:`urlencode` function to encode them. + +The push channel is used to send notifications to users by using +:class:`Symfony\\Component\\Notifier\\Texter` classes. Symfony provides +integration with these push services: + +=============== ======================================================================================= +Service +=============== ======================================================================================= +`Engagespot`_ **Install**: ``composer require symfony/engagespot-notifier`` \ + **DSN**: ``engagespot://API_KEY@default?campaign_name=CAMPAIGN_NAME`` +`Expo`_ **Install**: ``composer require symfony/expo-notifier`` \ + **DSN**: ``expo://TOKEN@default`` +`Novu`_ **Install**: ``composer require symfony/novu-notifier`` \ + **DSN**: ``novu://API_KEY@default`` +`Ntfy`_ **Install**: ``composer require symfony/ntfy-notifier`` \ + **DSN**: ``ntfy://default/TOPIC`` +`OneSignal`_ **Install**: ``composer require symfony/one-signal-notifier`` \ + **DSN**: ``onesignal://APP_ID:API_KEY@default?defaultRecipientId=DEFAULT_RECIPIENT_ID`` +`PagerDuty`_ **Install**: ``composer require symfony/pager-duty-notifier`` \ + **DSN**: ``pagerduty://TOKEN@SUBDOMAIN`` +`Pushover`_ **Install**: ``composer require symfony/pushover-notifier`` \ + **DSN**: ``pushover://USER_KEY:APP_TOKEN@default`` +`Pushy`_ **Install**: ``composer require symfony/pushy-notifier`` \ + **DSN**: ``pushy://API_KEY@default`` +=============== ======================================================================================= + +To enable a texter, add the correct DSN in your ``.env`` file and +configure the ``texter_transports``: + +.. versionadded:: 7.1 + + The `Pushy`_ integration was introduced in Symfony 7.1. + +.. code-block:: bash + + # .env + EXPO_DSN=expo://TOKEN@default + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/notifier.yaml + framework: + notifier: + texter_transports: + expo: '%env(EXPO_DSN)%' + + .. code-block:: xml + + + + + + + + + %env(EXPO_DSN)% + + + + + + .. code-block:: php + + // config/packages/notifier.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->notifier() + ->texterTransport('expo', env('EXPO_DSN')) + ; + }; + +.. _notifier-desktop-channel: + +Desktop Channel +~~~~~~~~~~~~~~~ + +The desktop channel is used to display local desktop notifications on the same +host machine using :class:`Symfony\\Component\\Notifier\\Texter` classes. Currently, +Symfony is integrated with the following providers: + +=============== ================================================ ============================================================================== +Provider Install DSN +=============== ================================================ ============================================================================== +`JoliNotif`_ ``composer require symfony/joli-notif-notifier`` ``jolinotif://default`` +=============== ================================================ ============================================================================== + +.. versionadded:: 7.2 + + The JoliNotif bridge was introduced in Symfony 7.2. + +If you are using :ref:`Symfony Flex `, installing that package will +also create the necessary environment variable and configuration. Otherwise, you'll +need to add the following manually: + +1) Add the correct DSN in your ``.env`` file: + +.. code-block:: bash + + # .env + JOLINOTIF=jolinotif://default + +2) Update the Notifier configuration to add a new texter transport: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/notifier.yaml + framework: + notifier: + texter_transports: + jolinotif: '%env(JOLINOTIF)%' + + .. code-block:: xml + + + + + + + + + %env(JOLINOTIF)% + + + + + + .. code-block:: php + + // config/packages/notifier.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->notifier() + ->texterTransport('jolinotif', env('JOLINOTIF')) + ; + }; + +Now you can send notifications to your desktop as follows:: + + // src/Notifier/SomeService.php + use Symfony\Component\Notifier\Message\DesktopMessage; + use Symfony\Component\Notifier\TexterInterface; + // ... + + class SomeService + { + public function __construct( + private TexterInterface $texter, + ) { + } + + public function notifyNewSubscriber(User $user): void + { + $message = new DesktopMessage( + 'New subscription! 🎉', + sprintf('%s is a new subscriber', $user->getFullName()) + ); + + $this->texter->send($message); + } + } + +These notifications can be customized further, and depending on your operating system, +they may support features like custom sounds, icons, and more:: + + use Symfony\Component\Notifier\Bridge\JoliNotif\JoliNotifOptions; + // ... + + $options = (new JoliNotifOptions()) + ->setIconPath('/path/to/icons/error.png') + ->setExtraOption('sound', 'sosumi') + ->setExtraOption('url', 'https://example.com'); + + $message = new DesktopMessage('Production is down', <<send($message); + +Configure to use Failover or Round-Robin Transports +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Besides configuring one or more separate transports, you can also use the +special ``||`` and ``&&`` characters to implement a failover or round-robin +transport: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/notifier.yaml + framework: + notifier: + chatter_transports: + # Send notifications to Slack and use Telegram if + # Slack errored + main: '%env(SLACK_DSN)% || %env(TELEGRAM_DSN)%' + + # Send notifications to the next scheduled transport calculated by round robin + roundrobin: '%env(SLACK_DSN)% && %env(TELEGRAM_DSN)%' + + .. code-block:: xml + + + + + + + + + + %env(SLACK_DSN)% || %env(TELEGRAM_DSN)% + + + + + + + + + .. code-block:: php + + // config/packages/notifier.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->notifier() + // Send notifications to Slack and use Telegram if + // Slack errored + ->chatterTransport('main', env('SLACK_DSN').' || '.env('TELEGRAM_DSN')) + + // Send notifications to the next scheduled transport calculated by round robin + ->chatterTransport('roundrobin', env('SLACK_DSN').' && '.env('TELEGRAM_DSN')) + ; + }; + +Creating & Sending Notifications +-------------------------------- + +To send a notification, autowire the +:class:`Symfony\\Component\\Notifier\\NotifierInterface` (service ID +``notifier``). This class has a ``send()`` method that allows you to send a +:class:`Symfony\\Component\\Notifier\\Notification\\Notification` to a +:class:`Symfony\\Component\\Notifier\\Recipient\\Recipient`:: + + // src/Controller/InvoiceController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Notifier\Notification\Notification; + use Symfony\Component\Notifier\NotifierInterface; + use Symfony\Component\Notifier\Recipient\Recipient; + + class InvoiceController extends AbstractController + { + #[Route('/invoice/create')] + public function create(NotifierInterface $notifier): Response + { + // ... + + // Create a Notification that has to be sent + // using the "email" channel + $notification = (new Notification('New Invoice', ['email'])) + ->content('You got a new invoice for 15 EUR.'); + + // The receiver of the Notification + $recipient = new Recipient( + $user->getEmail(), + $user->getPhonenumber() + ); + + // Send the notification to the recipient + $notifier->send($notification, $recipient); + + // ... + } + } + +The ``Notification`` is created by using two arguments: the subject and +channels. The channels specify which channel (or transport) should be used +to send the notification. For instance, ``['email', 'sms']`` will send +both an email and sms notification to the user. + +The default notification also has a ``content()`` and ``emoji()`` method to +set the notification content and icon. + +Symfony provides the following recipients: + +:class:`Symfony\\Component\\Notifier\\Recipient\\NoRecipient` + This is the default and is useful when there is no need to have + information about the receiver. For example, the browser channel uses + the current requests' :ref:`session flashbag `; + +:class:`Symfony\\Component\\Notifier\\Recipient\\Recipient` + This can contain both the email address and the phone number of the user. This + recipient can be used for all channels (depending on whether they are + actually set). + +Configuring Channel Policies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Instead of specifying the target channels on creation, Symfony also allows +you to use notification importance levels. Update the configuration to +specify what channels should be used for specific levels (using +``channel_policy``): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/notifier.yaml + framework: + notifier: + # ... + channel_policy: + # Use SMS, Slack and email for urgent notifications + urgent: ['sms', 'chat/slack', 'email'] + + # Use Slack for highly important notifications + high: ['chat/slack'] + + # Use browser for medium and low notifications + medium: ['browser'] + low: ['browser'] + + .. code-block:: xml + + + + + + + + + + + + sms + chat/slack + email + + + chat/slack + + + browser + browser + + + + + + .. code-block:: php + + // config/packages/notifier.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + $framework->notifier() + // Use SMS, Slack and email for urgent notifications + ->channelPolicy('urgent', ['sms', 'chat/slack', 'email']) + // Use Slack for highly important notifications + ->channelPolicy('high', ['chat/slack']) + // Use browser for medium and low notifications + ->channelPolicy('medium', ['browser']) + ->channelPolicy('low', ['browser']) + ; + }; + +Now, whenever the notification's importance is set to "high", it will be +sent using the Slack transport:: + + // ... + class InvoiceController extends AbstractController + { + #[Route('/invoice/create')] + public function invoice(NotifierInterface $notifier): Response + { + // ... + + $notification = (new Notification('New Invoice')) + ->content('You got a new invoice for 15 EUR.') + ->importance(Notification::IMPORTANCE_HIGH); + + $notifier->send($notification, new Recipient('wouter@example.com')); + + // ... + } + } + +Customize Notifications +----------------------- + +You can extend the ``Notification`` or ``Recipient`` base classes to +customize their behavior. For instance, you can overwrite the +``getChannels()`` method to only return ``sms`` if the invoice price is +very high and the recipient has a phone number:: + + namespace App\Notifier; + + use Symfony\Component\Notifier\Notification\Notification; + use Symfony\Component\Notifier\Recipient\RecipientInterface; + use Symfony\Component\Notifier\Recipient\SmsRecipientInterface; + + class InvoiceNotification extends Notification + { + public function __construct( + private int $price, + ) { + } + + public function getChannels(RecipientInterface $recipient): array + { + if ( + $this->price > 10000 + && $recipient instanceof SmsRecipientInterface + ) { + return ['sms']; + } + + return ['email']; + } + } + +Customize Notification Messages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each channel has its own notification interface that you can implement to +customize the notification message. For instance, if you want to modify the +message based on the chat service, implement +:class:`Symfony\\Component\\Notifier\\Notification\\ChatNotificationInterface` +and its ``asChatMessage()`` method:: + + // src/Notifier/InvoiceNotification.php + namespace App\Notifier; + + use Symfony\Component\Notifier\Message\ChatMessage; + use Symfony\Component\Notifier\Notification\ChatNotificationInterface; + use Symfony\Component\Notifier\Notification\Notification; + use Symfony\Component\Notifier\Recipient\RecipientInterface; + + class InvoiceNotification extends Notification implements ChatNotificationInterface + { + public function __construct( + private int $price, + ) { + } + + public function asChatMessage(RecipientInterface $recipient, ?string $transport = null): ?ChatMessage + { + // Add a custom subject and emoji if the message is sent to Slack + if ('slack' === $transport) { + $this->subject('You\'re invoiced '.strval($this->price).' EUR.'); + $this->emoji("money"); + return ChatMessage::fromNotification($this); + } + + // If you return null, the Notifier will create the ChatMessage + // based on this notification as it would without this method. + return null; + } + } + +The +:class:`Symfony\\Component\\Notifier\\Notification\\SmsNotificationInterface`, +:class:`Symfony\\Component\\Notifier\\Notification\\EmailNotificationInterface`, +:class:`Symfony\\Component\\Notifier\\Notification\\PushNotificationInterface` +and +:class:`Symfony\\Component\\Notifier\\Notification\\DesktopNotificationInterface` +also exists to modify messages sent to those channels. + +Customize Browser Notifications (Flash Messages) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The default behavior for browser channel notifications is to add a +:ref:`flash message ` with ``notification`` as its key. + +However, you might prefer to map the importance level of the notification to the +type of flash message, so you can tweak their style. + +You can do that by overriding the default ``notifier.flash_message_importance_mapper`` +service with your own implementation of +:class:`Symfony\\Component\\Notifier\\FlashMessage\\FlashMessageImportanceMapperInterface` +where you can provide your own "importance" to "alert level" mapping. + +Symfony currently provides an implementation for the Bootstrap CSS framework's +typical alert levels, which you can implement immediately using: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + notifier.flash_message_importance_mapper: + class: Symfony\Component\Notifier\FlashMessage\BootstrapFlashMessageImportanceMapper + + .. code-block:: xml + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\Notifier\FlashMessage\BootstrapFlashMessageImportanceMapper; + + return function(ContainerConfigurator $containerConfigurator) { + $containerConfigurator->services() + ->set('notifier.flash_message_importance_mapper', BootstrapFlashMessageImportanceMapper::class) + ; + }; + +Testing Notifier +---------------- + +Symfony provides a :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\NotificationAssertionsTrait` +which provide useful methods for testing your Notifier implementation. +You can benefit from this class by using it directly or extending the +:class:`Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase`. + +See :ref:`testing documentation ` for the list of available assertions. + +Disabling Delivery +------------------ + +While developing (or testing), you may want to disable delivery of notifications +entirely. You can do this by forcing Notifier to use the ``NullTransport`` for +all configured texter and chatter transports only in the ``dev`` (and/or +``test``) environment: + +.. code-block:: yaml + + # config/packages/dev/notifier.yaml + framework: + notifier: + texter_transports: + twilio: 'null://null' + chatter_transports: + slack: 'null://null' + +.. _notifier-events: + +Using Events +------------ + +The :class:`Symfony\\Component\\Notifier\\Transport` class of the Notifier component +allows you to optionally hook into the lifecycle via events. + +The ``MessageEvent`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Typical Purposes**: Doing something before the message is sent (like logging +which message is going to be sent, or displaying something about the event +to be executed. + +Just before sending the message, the event class ``MessageEvent`` is +dispatched. Listeners receive a +:class:`Symfony\\Component\\Notifier\\Event\\MessageEvent` event:: + + use Symfony\Component\Notifier\Event\MessageEvent; + + $dispatcher->addListener(MessageEvent::class, function (MessageEvent $event): void { + // gets the message instance + $message = $event->getMessage(); + + // log something + $this->logger(sprintf('Message with subject: %s will be send to %s', $message->getSubject(), $message->getRecipientId())); + }); + +The ``FailedMessageEvent`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Typical Purposes**: Doing something before the exception is thrown +(Retry to send the message or log additional information). + +Whenever an exception is thrown while sending the message, the event class +``FailedMessageEvent`` is dispatched. A listener can do anything useful before +the exception is thrown. + +Listeners receive a +:class:`Symfony\\Component\\Notifier\\Event\\FailedMessageEvent` event:: + + use Symfony\Component\Notifier\Event\FailedMessageEvent; + + $dispatcher->addListener(FailedMessageEvent::class, function (FailedMessageEvent $event): void { + // gets the message instance + $message = $event->getMessage(); + + // gets the error instance + $error = $event->getError(); + + // log something + $this->logger(sprintf('The message with subject: %s has not been sent successfully. The error is: %s', $message->getSubject(), $error->getMessage())); + }); + +The ``SentMessageEvent`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Typical Purposes**: To perform some action when the message is successfully +sent (like retrieve the id returned when the message is sent). + +After the message has been successfully sent, the event class ``SentMessageEvent`` +is dispatched. Listeners receive a +:class:`Symfony\\Component\\Notifier\\Event\\SentMessageEvent` event:: + + use Symfony\Component\Notifier\Event\SentMessageEvent; + + $dispatcher->addListener(SentMessageEvent::class, function (SentMessageEvent $event): void { + // gets the message instance + $message = $event->getMessage(); + + // log something + $this->logger(sprintf('The message has been successfully sent and has id: %s', $message->getMessageId())); + }); + +.. TODO +.. - Using the message bus for asynchronous notification +.. - Describe notifier monolog handler +.. - Describe notification_on_failed_messages integration + +.. _`46elks`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/FortySixElks/README.md +.. _`AllMySms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/AllMySms/README.md +.. _`AmazonSns`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md +.. _`Bandwidth`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Bandwidth/README.md +.. _`Bluesky`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Bluesky/README.md +.. _`Brevo`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Brevo/README.md +.. _`Chatwork`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Chatwork/README.md +.. _`Clickatell`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Clickatell/README.md +.. _`ContactEveryone`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/ContactEveryone/README.md +.. _`Discord`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Discord/README.md +.. _`Engagespot`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Engagespot/README.md +.. _`Esendex`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Esendex/README.md +.. _`Expo`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Expo/README.md +.. _`FakeChat`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/FakeChat/README.md +.. _`FakeSms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/FakeSms/README.md +.. _`Firebase`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Firebase/README.md +.. _`FreeMobile`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/FreeMobile/README.md +.. _`GatewayApi`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/GatewayApi/README.md +.. _`GoIP`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/GoIP/README.md +.. _`GoogleChat`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/GoogleChat/README.md +.. _`Infobip`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Infobip/README.md +.. _`Iqsms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Iqsms/README.md +.. _`iSendPro`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Isendpro/README.md +.. _`JoliNotif`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/JoliNotif/README.md +.. _`KazInfoTeh`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/KazInfoTeh/README.md +.. _`LINE Bot`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/LineBot/README.md +.. _`LINE Notify`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/LineNotify/README.md +.. _`LightSms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/LightSms/README.md +.. _`LinkedIn`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/LinkedIn/README.md +.. _`LOX24`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Lox24/README.md +.. _`Mailjet`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Mailjet/README.md +.. _`Mastodon`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Mastodon/README.md +.. _`Matrix`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Matrix/README.md +.. _`Mattermost`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Mattermost/README.md +.. _`Mercure`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Mercure/README.md +.. _`MessageBird`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/MessageBird/README.md +.. _`MessageMedia`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/MessageMedia/README.md +.. _`MicrosoftTeams`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/README.md +.. _`Mobyt`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Mobyt/README.md +.. _`Nexmo`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Nexmo/README.md +.. _`Novu`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Novu/README.md +.. _`Ntfy`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Ntfy/README.md +.. _`Octopush`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Octopush/README.md +.. _`OneSignal`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/OneSignal/README.md +.. _`OrangeSms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/OrangeSms/README.md +.. _`OvhCloud`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/OvhCloud/README.md +.. _`PagerDuty`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/PagerDuty/README.md +.. _`Plivo`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Plivo/README.md +.. _`Primotexto`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Primotexto/README.md +.. _`Pushover`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Pushover/README.md +.. _`Pushy`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Pushy/README.md +.. _`Redlink`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Redlink/README.md +.. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt +.. _`RingCentral`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/RingCentral/README.md +.. _`RocketChat`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/RocketChat/README.md +.. _`SMSFactor`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SmsFactor/README.md +.. _`Sendberry`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sendberry/README.md +.. _`Sendinblue`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sendinblue/README.md +.. _`Seven.io`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sevenio/README.md +.. _`SimpleTextin`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SimpleTextin/README.md +.. _`Sinch`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sinch/README.md +.. _`Sipgate`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sipgate/README.md +.. _`Slack`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Slack/README.md +.. _`Sms77`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sms77/README.md +.. _`SmsBiuras`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SmsBiuras/README.md +.. _`Smsbox`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Smsbox/README.md +.. _`Smsapi`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Smsapi/README.md +.. _`Smsc`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Smsc/README.md +.. _`SMSense`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SMSense/README.md +.. _`SmsSluzba`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SmsSluzba/README.md +.. _`SpotHit`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SpotHit/README.md +.. _`Sweego`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sweego/README.md +.. _`Telegram`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Telegram/README.md +.. _`Telnyx`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Telnyx/README.md +.. _`TurboSms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/TurboSms/README.md +.. _`Twilio`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Twilio/README.md +.. _`Twitter`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Twitter/README.md +.. _`Unifonic`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Unifonic/README.md +.. _`Vonage`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Vonage/README.md +.. _`Yunpian`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Yunpian/README.md +.. _`Zendesk`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Zendesk/README.md +.. _`Zulip`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Zulip/README.md diff --git a/page_creation.rst b/page_creation.rst new file mode 100644 index 00000000000..f8b2fdaf251 --- /dev/null +++ b/page_creation.rst @@ -0,0 +1,306 @@ +.. _creating-pages-in-symfony2: +.. _creating-pages-in-symfony: + +Create your First Page in Symfony +================================= + +Creating a new page - whether it's an HTML page or a JSON endpoint - is a +two-step process: + +#. **Create a controller**: A controller is the PHP function you write that + builds the page. You take the incoming request information and use it to + create a Symfony ``Response`` object, which can hold HTML content, a JSON + string or even a binary file like an image or PDF; + +#. **Create a route**: A route is the URL (e.g. ``/about``) to your page and + points to a controller. + +.. admonition:: Screencast + :class: screencast + + Do you prefer video tutorials? Check out the `Harmonious Development with Symfony`_ + screencast series. + +.. seealso:: + + Symfony *embraces* the HTTP Request-Response lifecycle. To find out more, + see :doc:`/introduction/http_fundamentals`. + +Creating a Page: Route and Controller +------------------------------------- + +.. tip:: + + Before continuing, make sure you've read the :doc:`Setup ` + article and can access your new Symfony app in the browser. + +Suppose you want to create a page - ``/lucky/number`` - that generates a lucky (well, +random) number and prints it. To do that, create a "Controller" class and a +"controller" method inside of it:: + + Lucky number: '.$number.'' + ); + } + } + +.. _annotation-routes: +.. _attribute-routes: + +Now you need to associate this controller function with a public URL (e.g. ``/lucky/number``) +so that the ``number()`` method is called when a user browses to it. This association +is defined with the ``#[Route]`` attribute (in PHP, `attributes`_ are used to add +metadata to code): + +.. code-block:: diff + + // src/Controller/LuckyController.php + + // ... + + use Symfony\Component\Routing\Attribute\Route; + + class LuckyController + { + + #[Route('/lucky/number')] + public function number(): Response + { + // this looks exactly the same + } + } + +That's it! If you are using :doc:`the Symfony web server `, +try it out by going to: http://localhost:8000/lucky/number + +.. tip:: + + Symfony recommends defining routes as attributes to have the controller code + and its route configuration at the same location. However, if you prefer, you can + :doc:`define routes in separate files ` using YAML, XML and PHP formats. + +If you see a lucky number being printed back to you, congratulations! But before +you run off to play the lottery, check out how this works. Remember the two steps +to create a page? + +#. *Create a controller and a method*: This is a function where *you* build the page and ultimately + return a ``Response`` object. You'll learn more about :doc:`controllers ` + in their own section, including how to return JSON responses; + +#. *Create a route*: In ``config/routes.yaml``, the route defines the URL to your + page (``path``) and what ``controller`` to call. You'll learn more about :doc:`routing ` + in its own section, including how to make *variable* URLs. + +The bin/console Command +----------------------- + +Your project already has a powerful debugging tool inside: the ``bin/console`` command. +Try running it: + +.. code-block:: terminal + + $ php bin/console + +You should see a list of commands that can give you debugging information, help generate +code, generate database migrations and a lot more. As you install more packages, +you'll see more commands. + +To get a list of *all* of the routes in your system, use the ``debug:router`` command: + +.. code-block:: terminal + + $ php bin/console debug:router + +You should see your ``app_lucky_number`` route in the list: + +.. code-block:: terminal + + ---------------- ------- ------- ----- -------------- + Name Method Scheme Host Path + ---------------- ------- ------- ----- -------------- + app_lucky_number ANY ANY ANY /lucky/number + ---------------- ------- ------- ----- -------------- + +You will also see debugging routes besides ``app_lucky_number`` -- more on +the debugging routes in the next section. + +You'll learn about many more commands as you continue! + +.. tip:: + + If your shell is supported, you can also set up console completion support. + This autocompletes commands and other input when using ``bin/console``. + See :ref:`the Console document ` for more + information on how to set up completion. + +.. _web-debug-toolbar: + +The Web Debug Toolbar: Debugging Dream +-------------------------------------- + +One of Symfony's *amazing* features is the Web Debug Toolbar: a bar that displays +a *huge* amount of debugging information along the bottom of your page while +developing. This is all included out of the box using a :ref:`Symfony pack ` +called ``symfony/profiler-pack``. + +You will see a dark bar along the bottom of the page. You'll learn more about +all the information it holds along the way, but feel free to experiment: hover +over and click the different icons to get information about routing, +performance, logging and more. + +Rendering a Template +-------------------- + +If you're returning HTML from your controller, you'll probably want to render +a template. Fortunately, Symfony comes with `Twig`_: a templating language that's +minimal, powerful and actually quite fun. + +Install the twig package with: + +.. code-block:: terminal + + $ composer require twig + +Make sure that ``LuckyController`` extends Symfony's base +:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController` class: + +.. code-block:: diff + + // src/Controller/LuckyController.php + + // ... + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + + - class LuckyController + + class LuckyController extends AbstractController + { + // ... + } + +Now, use the handy ``render()`` method to render a template. Pass it a ``number`` +variable so you can use it in Twig:: + + // src/Controller/LuckyController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + // ... + + class LuckyController extends AbstractController + { + #[Route('/lucky/number')] + public function number(): Response + { + $number = random_int(0, 100); + + return $this->render('lucky/number.html.twig', [ + 'number' => $number, + ]); + } + } + +Template files live in the ``templates/`` directory, which was created for you automatically +when you installed Twig. Create a new ``templates/lucky`` directory with a new +``number.html.twig`` file inside: + +.. code-block:: html+twig + + {# templates/lucky/number.html.twig #} +

          Your lucky number is {{ number }}

          + +The ``{{ number }}`` syntax is used to *print* variables in Twig. Refresh your browser +to get your *new* lucky number! + + http://localhost:8000/lucky/number + +Now you may wonder where the Web Debug Toolbar has gone: that's because there is +no ```` tag in the current template. You can add the body element yourself, +or extend ``base.html.twig``, which contains all default HTML elements. + +In the :doc:`templates ` article, you'll learn all about Twig: how +to loop, render other templates and leverage its powerful layout inheritance system. + +Checking out the Project Structure +---------------------------------- + +Great news! You've already worked inside the most important directories in your +project: + +``config/`` + Contains... configuration!. You will configure routes, + :doc:`services ` and packages. + +``src/`` + All your PHP code lives here. + +``templates/`` + All your Twig templates live here. + +Most of the time, you'll be working in ``src/``, ``templates/`` or ``config/``. +As you keep reading, you'll learn what can be done inside each of these. + +So what about the other directories in the project? + +``bin/`` + The famous ``bin/console`` file lives here (and other, less important + executable files). + +``var/`` + This is where automatically-created files are stored, like cache files + (``var/cache/``) and logs (``var/log/``). + +``vendor/`` + Third-party (i.e. "vendor") libraries live here! These are downloaded via the `Composer`_ + package manager. + +``public/`` + This is the document root for your project: you put any publicly accessible files + here. + +And when you install new packages, new directories will be created automatically +when needed. + +What's Next? +------------ + +Congrats! You're already starting to master Symfony and learn a whole new +way of building beautiful, functional, fast and maintainable applications. + +OK, time to finish mastering the fundamentals by reading these articles: + +* :doc:`/routing` +* :doc:`/controller` +* :doc:`/templates` +* :doc:`/frontend` +* :doc:`/configuration` + +Then, learn about other important topics like the +:doc:`service container `, +the :doc:`form system `, using :doc:`Doctrine ` +(if you need to query a database) and more! + +Have fun! + +Go Deeper with HTTP & Framework Fundamentals +-------------------------------------------- + +.. toctree:: + :maxdepth: 1 + :glob: + + introduction/* + +.. _`Twig`: https://twig.symfony.com +.. _`Composer`: https://getcomposer.org +.. _`Harmonious Development with Symfony`: https://symfonycasts.com/screencast/symfony/setup +.. _`attributes`: https://www.php.net/manual/en/language.attributes.overview.php diff --git a/performance.rst b/performance.rst new file mode 100644 index 00000000000..828333f338b --- /dev/null +++ b/performance.rst @@ -0,0 +1,417 @@ +Performance +=========== + +Symfony is fast, right out of the box. However, you can make it faster if you +optimize your servers and your applications as explained in the following +performance checklists. + +Performance Checklists +---------------------- + +Use these checklists to verify that your application and server are configured +for maximum performance: + +* **Symfony Application Checklist**: + + #. :ref:`Install APCu Polyfill if your server uses APC ` + #. :ref:`Restrict the number of locales enabled in the application ` + +* **Production Server Checklist**: + + #. :ref:`Dump the service container into a single file ` + #. :ref:`Use the OPcache byte code cache ` + #. :ref:`Configure OPcache for maximum performance ` + #. :ref:`Don't check PHP files timestamps ` + #. :ref:`Configure the PHP realpath Cache ` + #. :ref:`Optimize Composer Autoloader ` + +.. _performance-install-apcu-polyfill: + +Install APCu Polyfill if your Server Uses APC +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your production server still uses the legacy APC PHP extension instead of +OPcache, install the `APCu Polyfill component`_ in your application to enable +compatibility with `APCu PHP functions`_ and unlock support for advanced Symfony +features, such as the APCu Cache adapter. + +.. _performance-enabled-locales: + +Restrict the Number of Locales Enabled in the Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use the :ref:`framework.enabled_locales ` +option to only generate the translation files actually used in your application. + +.. _performance-service-container-single-file: + +Dump the Service Container into a Single File +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony compiles the :doc:`service container ` into multiple +small files by default. Set this parameter to ``true`` to compile the entire +container into a single file, which could improve performance when using +"class preloading" in PHP 7.4 or newer versions: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + # ... + .container.dumper.inline_factories: true + + .. code-block:: xml + + + + + + + + true + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $container): void { + $container->parameters()->set('.container.dumper.inline_factories', true); + }; + +.. _performance-use-opcache: + +.. tip:: + + The ``.`` prefix denotes a parameter that is only used during compilation of the container. + See :ref:`Configuration Parameters ` for more details. + +Use the OPcache Byte Code Cache +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +OPcache stores the compiled PHP files to avoid having to recompile them for +every request. There are some `byte code caches`_ available, but as of PHP +5.5, PHP comes with `OPcache`_ built-in. For older versions, the most widely +used byte code cache is APC. + +.. _performance-use-preloading: + +Use the OPcache class preloading +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Starting from PHP 7.4, OPcache can compile and load classes at start-up and +make them available to all requests until the server is restarted, improving +performance significantly. + +During container compilation (e.g. when running the ``cache:clear`` command), +Symfony generates a file with the list of classes to preload in the +``var/cache/`` directory. Rather than use this file directly, use the +``config/preload.php`` file that is created when +:doc:`using Symfony Flex in your project `: + +.. code-block:: ini + + ; php.ini + opcache.preload=/path/to/project/config/preload.php + + ; required for opcache.preload: + opcache.preload_user=www-data + +If this file is missing, run this command to update the Symfony Flex recipe: +``composer recipes:update symfony/framework-bundle``. + +Use the :ref:`container.preload ` and +:ref:`container.no_preload ` service tags to define +which classes should or should not be preloaded by PHP. + +.. _performance-configure-opcache: + +Configure OPcache for Maximum Performance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The default OPcache configuration is not suited for Symfony applications, so +it's recommended to change these settings as follows: + +.. code-block:: ini + + ; php.ini + ; maximum memory that OPcache can use to store compiled PHP files + opcache.memory_consumption=256 + + ; maximum number of files that can be stored in the cache + opcache.max_accelerated_files=20000 + +.. _performance-dont-check-timestamps: + +Don't Check PHP Files Timestamps +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In production servers, PHP files should never change, unless a new application +version is deployed. However, by default OPcache checks if cached files have +changed their contents since they were cached. This check introduces some +overhead that can be avoided as follows: + +.. code-block:: ini + + ; php.ini + opcache.validate_timestamps=0 + +After each deployment, you must empty and regenerate the cache of OPcache. Otherwise +you won't see the updates made in the application. Given that in PHP, the CLI +and the web processes don't share the same OPcache, you cannot clear the web +server OPcache by executing some command in your terminal. These are some of the +possible solutions: + +1. Restart the web server; +2. Call the ``apc_clear_cache()`` or ``opcache_reset()`` functions via the + web server (i.e. by having these in a script that you execute over the web); +3. Use the `cachetool`_ utility to control APC and OPcache from the CLI. + +.. _performance-configure-realpath-cache: + +Configure the PHP ``realpath`` Cache +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a relative path is transformed into its real and absolute path, PHP +caches the result to improve performance. Applications that open many PHP files, +such as Symfony projects, should use at least these values: + +.. code-block:: ini + + ; php.ini + ; maximum memory allocated to store the results + realpath_cache_size=4096K + + ; save the results for 10 minutes (600 seconds) + realpath_cache_ttl=600 + +.. note:: + + PHP disables the ``realpath`` cache when the `open_basedir`_ config option + is enabled. + +.. _performance-optimize-composer-autoloader: + +Optimize Composer Autoloader +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The class loader used while developing the application is optimized to find new +and changed classes. In production servers, PHP files should never change, +unless a new application version is deployed. That's why you can optimize +Composer's autoloader to scan the entire application once and build an +optimized "class map", which is a big array of the locations of all the classes +and it's stored in ``vendor/composer/autoload_classmap.php``. + +Execute this command to generate the new class map (and make it part of your +deployment process too): + +.. code-block:: terminal + + $ composer dump-autoload --no-dev --classmap-authoritative + +* ``--no-dev`` excludes the classes that are only needed in the development + environment (i.e. ``require-dev`` dependencies and ``autoload-dev`` rules); +* ``--classmap-authoritative`` creates a class map for PSR-0 and PSR-4 compatible classes + used in your application and prevents Composer from scanning the file system for + classes that are not found in the class map. (see: `Composer's autoloader optimization`_). + +Disable Dumping the Container as XML in Debug Mode +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In :ref:`debug mode `, Symfony generates an XML file with all the +:doc:`service container ` information (services, arguments, etc.) +This XML file is used by various debugging commands such as ``debug:container`` +and ``debug:autowiring``. + +When the container grows larger and larger, so does the size of the file and the +time to generate it. If the benefit of this XML file does not outweigh the decrease +in performance, you can stop generating the file as follows: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + # ... + debug.container.dump: false + + .. code-block:: xml + + + + + + + + false + + + + .. code-block:: php + + // config/services.php + + // ... + $container->parameters()->set('debug.container.dump', false); + +.. _profiling-applications: + +Profiling Symfony Applications +------------------------------ + +Profiling with Blackfire +~~~~~~~~~~~~~~~~~~~~~~~~ + +`Blackfire`_ is the best tool to profile and optimize performance of Symfony +applications during development, test and production. It's a commercial service, +but provides a `full-featured demo`_. + +Profiling with Symfony Stopwatch +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides a basic performance profiler in the development +:ref:`config environment `. Click on the "time panel" +of the :ref:`web debug toolbar ` to see how much time Symfony +spent on tasks such as making database queries and rendering templates. + +You can measure the execution time and memory consumption of your own code and +display the result in the Symfony profiler thanks to the `Stopwatch component`_. + +When using :ref:`autowiring `, type-hint any controller or +service argument with the :class:`Symfony\\Component\\Stopwatch\\Stopwatch` class +and Symfony will inject the ``debug.stopwatch`` service:: + + use Symfony\Component\Stopwatch\Stopwatch; + + class DataExporter + { + public function __construct( + private Stopwatch $stopwatch, + ) { + } + + public function export(): void + { + // the argument is the name of the "profiling event" + $this->stopwatch->start('export-data'); + + // ...do things to export data... + + // reset the stopwatch to delete all the data measured so far + // $this->stopwatch->reset(); + + $this->stopwatch->stop('export-data'); + } + } + +If the request calls this service during its execution, you'll see a new +event called ``export-data`` in the Symfony profiler. + +The ``start()``, ``stop()`` and ``getEvent()`` methods return a +:class:`Symfony\\Component\\Stopwatch\\StopwatchEvent` object that provides +information about the current event, even while it's still running. This +object can be converted to a string for a quick summary:: + + // ... + dump((string) $this->stopwatch->getEvent('export-data')); // dumps e.g. '4.50 MiB - 26 ms' + +You can also profile your template code with the :ref:`stopwatch Twig tag `: + +.. code-block:: twig + + {% stopwatch 'render-blog-posts' %} + {% for post in blog_posts %} + {# ... #} + {% endfor %} + {% endstopwatch %} + +Profiling Categories +.................... + +Use the second optional argument of the ``start()`` method to define the +category or tag of the event. This helps keep events organized by type:: + + $this->stopwatch->start('export-data', 'export'); + +Profiling Periods +................. + +A `real-world stopwatch`_ not only includes the start/stop button but also a +"lap button" to measure each partial lap. This is exactly what the ``lap()`` +method does, which stops an event and then restarts it immediately:: + + $this->stopwatch->start('process-data-records', 'export'); + + foreach ($records as $record) { + // ... some code goes here + $this->stopwatch->lap('process-data-records'); + } + + $event = $this->stopwatch->stop('process-data-records'); + // $event->getDuration(), $event->getMemory(), etc. + + // Lap information is stored as "periods" within the event: + // $event->getPeriods(); + + // Gets the last event period: + // $event->getLastPeriod(); + +.. versionadded:: 7.2 + + The ``getLastPeriod()`` method was introduced in Symfony 7.2. + +Profiling Sections +.................. + +Sections are a way to split the profile timeline into groups. Example:: + + $this->stopwatch->openSection(); + $this->stopwatch->start('validating-file', 'validation'); + $this->stopwatch->stopSection('parsing'); + + $events = $this->stopwatch->getSectionEvents('parsing'); + + // later you can reopen a section passing its name to the openSection() method + $this->stopwatch->openSection('parsing'); + $this->stopwatch->start('processing-file'); + $this->stopwatch->stopSection('parsing'); + +All events that don't belong to any named section are added to the special section +called ``__root__``. This way you can get all stopwatch events, even if you don't +know their names, as follows:: + + use Symfony\Component\Stopwatch\Stopwatch; + + foreach($this->stopwatch->getSectionEvents(Stopwatch::ROOT) as $event) { + echo (string) $event; + } + +.. versionadded:: 7.2 + + The ``Stopwatch::ROOT`` constant as a shortcut for ``__root__`` was introduced in Symfony 7.2. + +Learn more +---------- + +* :doc:`/http_cache/varnish` + +.. _`byte code caches`: https://en.wikipedia.org/wiki/List_of_PHP_accelerators +.. _`OPcache`: https://www.php.net/manual/en/book.opcache.php +.. _`Composer's autoloader optimization`: https://getcomposer.org/doc/articles/autoloader-optimization.md +.. _`APCu Polyfill component`: https://github.com/symfony/polyfill-apcu +.. _`APCu PHP functions`: https://www.php.net/manual/en/ref.apcu.php +.. _`cachetool`: https://github.com/gordalina/cachetool +.. _`open_basedir`: https://www.php.net/manual/ini.core.php#ini.open-basedir +.. _`Blackfire`: https://blackfire.io/docs/introduction?utm_source=symfony&utm_medium=symfonycom_docs&utm_campaign=performance +.. _`full-featured demo`: https://demo.blackfire.io?utm_source=symfony&utm_medium=symfonycom_docs&utm_campaign=performance +.. _`Stopwatch component`: https://symfony.com/components/Stopwatch +.. _`real-world stopwatch`: https://en.wikipedia.org/wiki/Stopwatch diff --git a/profiler.rst b/profiler.rst new file mode 100644 index 00000000000..7fc97c8ee33 --- /dev/null +++ b/profiler.rst @@ -0,0 +1,608 @@ +Profiler +======== + +The profiler is a powerful **development tool** that gives detailed information +about the execution of any request. + +.. danger:: + + **Never** enable the profiler in production environments + as it will lead to major security vulnerabilities in your project. + +Installation +------------ + +In applications using :ref:`Symfony Flex `, run this command to +install the ``profiler`` :ref:`Symfony pack ` before using it: + +.. code-block:: terminal + + $ composer require --dev symfony/profiler-pack + +Now, browse any page of your application in the development environment to let +the profiler collect information. Then, click on any element of the debug +toolbar injected at the bottom of your pages to open the web interface of the +Symfony Profiler, which will look like this: + +.. image:: /_images/profiler/web-interface.png + :alt: The Symfony Web profiler page. + :class: with-browser + +.. note:: + + The debug toolbar is only injected into HTML responses. For other kinds of + contents (e.g. JSON responses in API requests) the profiler URL is available + in the ``X-Debug-Token-Link`` HTTP response header. Browse the ``/_profiler`` + URL to see all profiles. + +.. note:: + + To limit the storage used by profiles on disk, they are probabilistically + removed after 2 days. + +Accessing Profiling Data Programmatically +----------------------------------------- + +Most of the time, the profiler information is accessed and analyzed using its +web-based interface. However, you can also retrieve profiling information +programmatically thanks to the methods provided by the ``profiler`` service. + +When the response object is available, use the +:method:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler::loadProfileFromResponse` +method to access to its associated profile:: + + // ... $profiler is the 'profiler' service + $profile = $profiler->loadProfileFromResponse($response); + +.. note:: + + The ``profiler`` service will be :doc:`autowired ` + automatically when type-hinting any service argument with the + :class:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler` class. + +When the profiler stores data about a request, it also associates a token with it; +this token is available in the ``X-Debug-Token`` HTTP header of the response. +Using this token, you can access the profile of any past response thanks to the +:method:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler::loadProfile` method:: + + $token = $response->headers->get('X-Debug-Token'); + $profile = $profiler->loadProfile($token); + +.. tip:: + + When the profiler is enabled but not the web debug toolbar, inspect the page + with your browser's developer tools to get the value of the ``X-Debug-Token`` + HTTP header. + +The ``profiler`` service also provides the +:method:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler::find` method to +look for tokens based on some criteria:: + + // gets the latest 10 tokens + $tokens = $profiler->find('', '', 10, '', '', ''); + + // gets the latest 10 tokens for all URLs containing /admin/ + $tokens = $profiler->find('', '/admin/', 10, '', '', ''); + + // gets the latest 10 tokens for all URLs not containing /api/ + $tokens = $profiler->find('', '!/api/', 10, '', '', ''); + + // gets the latest 10 tokens for local POST requests + $tokens = $profiler->find('127.0.0.1', '', 10, 'POST', '', ''); + + // gets the latest 10 tokens for requests that happened between 2 and 4 days ago + $tokens = $profiler->find('', '', 10, '', '4 days ago', '2 days ago'); + +Data Collectors +--------------- + +The profiler gets its information using some services called "data collectors". +Symfony comes with several collectors that get information about the request, +the logger, the routing, the cache, etc. + +Run this command to get the list of collectors actually enabled in your app: + +.. code-block:: terminal + + $ php bin/console debug:container --tag=data_collector + +You can also :ref:`create your own data collector ` to +store any data generated by your app and display it in the debug toolbar and the +profiler web interface. + +.. _profiler-timing-execution: + +Timing the Execution of the Application +--------------------------------------- + +If you want to measure the time some tasks take in your application, there's no +need to create a custom data collector. Instead, use the built-in utilities to +:ref:`profile Symfony applications `. + +.. tip:: + + Consider using a professional profiler such as `Blackfire`_ to measure and + analyze the execution of your application in detail. + +.. _enabling-the-profiler-programmatically: + +Enabling the Profiler Programmatically or Conditionally +------------------------------------------------------- + +Symfony Profiler can be enabled and disabled programmatically. You can use the ``enable()`` +and ``disable()`` methods of the :class:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler` +class in your controllers to manage the profiler programmatically:: + + use Symfony\Component\HttpKernel\Profiler\Profiler; + // ... + + class DefaultController + { + // ... + + public function someMethod(?Profiler $profiler): Response + { + // $profiler won't be set if your environment doesn't have the profiler (like prod, by default) + if (null !== $profiler) { + // if it exists, disable the profiler for this particular controller action + $profiler->disable(); + } + + // ... + } + } + +In order for the profiler to be injected into your controller you need to +create an alias pointing to the existing ``profiler`` service: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services_dev.yaml + services: + Symfony\Component\HttpKernel\Profiler\Profiler: '@profiler' + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/services_dev.php + use Symfony\Component\HttpKernel\Profiler\Profiler; + + $container->setAlias(Profiler::class, 'profiler'); + +.. _enabling-the-profiler-conditionally: + +Enabling the Profiler Conditionally +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Instead of enabling the profiler programmatically as explained in the previous +section, you can also enable it when a certain condition is met (e.g. a certain +parameter is included in the URL): + +.. code-block:: yaml + + # config/packages/dev/web_profiler.yaml + framework: + profiler: + collect: false + collect_parameter: 'profile' + +This configuration disables the profiler by default (``collect: false``) to +improve the application performance; but enables it for requests that include a +query parameter called ``profile`` (you can freely choose this query parameter name). + +In addition to the query parameter, this feature also works when submitting a +form field with that name (useful to enable the profiler in ``POST`` requests) +or when including it as a request attribute. + +Updating the Web Debug Toolbar After AJAX Requests +-------------------------------------------------- + +`Single-page applications`_ (SPA) are web applications that interact with the +user by dynamically rewriting the current page rather than loading entire new +pages from a server. + +By default, the debug toolbar displays the information of the initial page load +and doesn't refresh after each AJAX request. However, you can configure the +toolbar to be refreshed after each AJAX request by enabling ``ajax_replace`` in the +``web_profiler`` configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/web_profiler.yaml + web_profiler: + toolbar: + ajax_replace: true + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/web_profiler.php + use Symfony\Config\WebProfilerConfig; + + return static function (WebProfilerConfig $profiler): void { + $profiler->toolbar() + ->ajaxReplace(true); + }; + +If you need a more sophisticated solution, you can set the +``Symfony-Debug-Toolbar-Replace`` header to a value of ``'1'`` in the response +yourself:: + + $response->headers->set('Symfony-Debug-Toolbar-Replace', '1'); + +Ideally this header should only be set during development and not for +production. To do that, create an :doc:`event subscriber ` +and listen to the :ref:`kernel.response ` +event:: + + use Symfony\Component\DependencyInjection\Attribute\When; + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpKernel\Event\ResponseEvent; + use Symfony\Component\HttpKernel\KernelInterface; + + // ... + + #[When(env: 'dev')] + class MySubscriber implements EventSubscriberInterface + { + // ... + + public function onKernelResponse(ResponseEvent $event): void + { + // Your custom logic here + + $response = $event->getResponse(); + $response->headers->set('Symfony-Debug-Toolbar-Replace', '1'); + } + } + +.. _profiler-data-collector: + +Creating a Data Collector +------------------------- + +The Symfony Profiler obtains its profiling and debug information using some +special classes called data collectors. Symfony comes bundled with a few of +them, but you can also create your own. + +A data collector is a PHP class that implements the +:class:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface`. +For convenience, your data collectors can also extend from the +:class:`Symfony\\Bundle\\FrameworkBundle\\DataCollector\\AbstractDataCollector` +class, which implements the interface and provides some utilities and the +``$this->data`` property to store the collected information. + +The following example shows a custom collector that stores information about the +request:: + + // src/DataCollector/RequestCollector.php + namespace App\DataCollector; + + use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + class RequestCollector extends AbstractDataCollector + { + public function collect(Request $request, Response $response, ?\Throwable $exception = null): void + { + $this->data = [ + 'method' => $request->getMethod(), + 'acceptable_content_types' => $request->getAcceptableContentTypes(), + ]; + } + } + +These are the method that you can define in the data collector class: + +:method:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface::collect` method: + Stores the collected data in local properties (``$this->data`` if you extend + from ``AbstractDataCollector``). If you need some services to collect the + data, inject those services in the data collector constructor. + + .. warning:: + + The ``collect()`` method is only called once. It is not used to "gather" + data but is there to "pick up" the data that has been stored by your + service. + + .. warning:: + + As the profiler serializes data collector instances, you should not + store objects that cannot be serialized (like PDO objects) or you need + to provide your own ``serialize()`` method. + +:method:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface::reset` method: + It's called between requests to reset the state of the profiler. By default + it only empties the ``$this->data`` contents, but you can override this method + to do additional cleaning. + +:method:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface::getName` method: + Returns the collector identifier, which must be unique in the application. + By default it returns the FQCN of the data collector class, but you can + override this method to return a custom name (e.g. ``app.request_collector``). + This value is used later to access the collector information (see + :doc:`/testing/profiling`) so you may prefer using short strings instead of FQCN strings. + +The ``collect()`` method is called during the :ref:`kernel.response ` +event. If you need to collect data that is only available later, implement +:class:`Symfony\\Component\\HttpKernel\\DataCollector\\LateDataCollectorInterface` +and define the ``lateCollect()`` method, which is invoked right before the profiler +data serialization (during :ref:`kernel.terminate ` event). + +.. note:: + + If you're using the :ref:`default services.yaml configuration ` + with ``autoconfigure``, then Symfony will start using your data collector after the + next page refresh. Otherwise, :ref:`enable the data collector by hand `. + +Adding Web Profiler Templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The information collected by your data collector can be displayed both in the +web debug toolbar and in the web profiler. To do so, you need to create a Twig +template that includes some specific blocks. + +First, add the ``getTemplate()`` method in your data collector class to return +the path of the Twig template to use. Then, add some *getters* to give the +template access to the collected information:: + + // src/DataCollector/RequestCollector.php + namespace App\DataCollector; + + use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector; + use Symfony\Component\VarDumper\Cloner\Data; + + class RequestCollector extends AbstractDataCollector + { + // ... + + public static function getTemplate(): ?string + { + return 'data_collector/template.html.twig'; + } + + public function getMethod(): string + { + return $this->data['method']; + } + + public function getAcceptableContentTypes(): array + { + return $this->data['acceptable_content_types']; + } + + public function getSomeObject(): Data + { + // use the cloneVar() method to dump collected data in the profiler + return $this->cloneVar($this->data['method']); + } + } + +In the simplest case, you want to display the information in the toolbar +without providing a profiler panel. This requires to define the ``toolbar`` +block and set the value of two variables called ``icon`` and ``text``: + +.. code-block:: html+twig + + {# templates/data_collector/template.html.twig #} + {% extends '@WebProfiler/Profiler/layout.html.twig' %} + + {% block toolbar %} + {% set icon %} + {# this is the content displayed as a panel in the toolbar #} + ... + Request + {% endset %} + + {% set text %} + {# this is the content displayed when hovering the mouse over + the toolbar panel #} +
          + Method + {{ collector.method }} +
          + +
          + Accepted content type + {{ collector.acceptableContentTypes|join(', ') }} +
          + {% endset %} + + {# the 'link' value set to 'false' means that this panel doesn't + show a section in the web profiler #} + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: false }) }} + {% endblock %} + +.. tip:: + + Symfony Profiler icons are selected from `Tabler icons`_, a large and open + source collection of SVG icons. It's recommended to also use those icons for + your own profiler panels to get a consistent look. + +.. tip:: + + Built-in collector templates define all their images as embedded SVG files. + This makes them work everywhere without having to mess with web assets links: + + .. code-block:: twig + + {% set icon %} + {{ include('data_collector/icon.svg') }} + {# ... #} + {% endset %} + +If the toolbar panel includes extended web profiler information, the Twig template +must also define additional blocks: + +.. code-block:: html+twig + + {# templates/data_collector/template.html.twig #} + {% extends '@WebProfiler/Profiler/layout.html.twig' %} + + {% block toolbar %} + {% set icon %} + {# ... #} + {% endset %} + + {% set text %} +
          + {# ... #} +
          + {% endset %} + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': true }) }} + {% endblock %} + + {% block head %} + {# Optional. Here you can link to or define your own CSS and JS contents. #} + {# Use {{ parent() }} to extend the default styles instead of overriding them. #} + {% endblock %} + + {% block menu %} + {# This left-hand menu appears when using the full-screen profiler. #} + + + Request + + {% endblock %} + + {% block panel %} + {# Optional, for showing the most details. #} +

          Acceptable Content Types

          + + + + + + {% for type in collector.acceptableContentTypes %} + + + + {% endfor %} + + {# use the profiler_dump() function to render the contents of dumped objects #} + + {{ profiler_dump(collector.someObject) }} + +
          Content Type
          {{ type }}
          + {% endblock %} + +The ``menu`` and ``panel`` blocks are the only required blocks to define the +contents displayed in the web profiler panel associated with this data collector. +All blocks have access to the ``collector`` object. + +.. note:: + + The position of each panel in the toolbar is determined by the collector + priority, which can only be defined when :ref:`configuring the data collector by hand `. + +.. note:: + + If you're using the :ref:`default services.yaml configuration ` + with ``autoconfigure``, then Symfony will start displaying your collector data + in the toolbar after the next page refresh. Otherwise, :ref:`enable the data collector by hand `. + +.. _data_collector_tag: + +Enabling Custom Data Collectors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you don't use Symfony's default configuration with +:ref:`autowire and autoconfigure ` +you'll need to configure the data collector explicitly: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + App\DataCollector\RequestCollector: + tags: + - + name: data_collector + # must match the value returned by the getName() method + id: 'App\DataCollector\RequestCollector' + # optional template (it has more priority than the value returned by getTemplate()) + template: 'data_collector/template.html.twig' + # optional priority (positive or negative integer; default = 0) + # priority: 300 + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\DataCollector\RequestCollector; + + return function(ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set(RequestCollector::class) + ->tag('data_collector', [ + 'id' => RequestCollector::class, + // optional template (it has more priority than the value returned by getTemplate()) + 'template' => 'data_collector/template.html.twig', + // optional priority (positive or negative integer; default = 0) + // 'priority' => 300, + ]); + }; + +.. _`Single-page applications`: https://en.wikipedia.org/wiki/Single-page_application +.. _`Blackfire`: https://blackfire.io/docs/introduction?utm_source=symfony&utm_medium=symfonycom_docs&utm_campaign=profiler +.. _`Tabler icons`: https://github.com/tabler/tabler-icons diff --git a/quick_tour/flex_recipes.rst b/quick_tour/flex_recipes.rst new file mode 100644 index 00000000000..856b4271205 --- /dev/null +++ b/quick_tour/flex_recipes.rst @@ -0,0 +1,256 @@ +Flex: Compose your Application +============================== + +After reading the first part of this tutorial, you have decided that Symfony was +worth another 10 minutes. Great choice! In this second part, you'll learn about +Symfony Flex: the amazing tool that makes adding new features as simple as running +one command. It's also the reason why Symfony is ideal for a small micro-service +or a huge application. Curious? Perfect! + +Symfony: Start Micro! +--------------------- + +Unless you're building a pure API (more on that soon!), you'll probably want to +render HTML. To do that, you'll use `Twig`_. Twig is a flexible, fast, and secure +template engine for PHP. It makes your templates more readable and concise; it also +makes them more friendly for web designers. + +Is Twig already installed in our application? Actually, not yet! And that's great! +When you start a new Symfony project, it's *small*: only the most critical dependencies +are included in your ``composer.json`` file: + +.. code-block:: text + + "require": { + "...", + "symfony/console": "^6.1", + "symfony/flex": "^2.0", + "symfony/framework-bundle": "^6.1", + "symfony/yaml": "^6.1" + } + +This makes Symfony different from any other PHP framework! Instead of starting with +a *bulky* app with *every* possible feature you might ever need, a Symfony app is +small, simple and *fast*. And you're in total control of what you add. + +Flex Recipes and Aliases +------------------------ + +So how can we install and configure Twig? By running one single command: + +.. code-block:: terminal + + $ composer require twig + +Two *very* interesting things happen behind the scenes thanks to Symfony Flex: a +Composer plugin that is already installed in our project. + +First, ``twig`` is not the name of a Composer package: it's a Flex *alias* that +points to ``symfony/twig-bundle``. Flex resolves that alias for Composer. + +And second, Flex installs a *recipe* for ``symfony/twig-bundle``. What's a recipe? +It's a way for a library to automatically configure itself by adding and modifying +files. Thanks to recipes, adding features is seamless and automated: install a package +and you're done! + +You can find a full list of recipes and aliases inside `RECIPES.md on the recipes repository`_. + +What did this recipe do? In addition to automatically enabling the feature in +``config/bundles.php``, it added 3 things: + +``config/packages/twig.yaml`` + A configuration file that sets up Twig with sensible defaults. + +``config/packages/test/twig.yaml`` + A configuration file that changes some Twig options when running tests. + +``templates/`` + This is the directory where template files will live. The recipe also added + a ``base.html.twig`` layout file. + +Twig: Rendering a Template +-------------------------- + +Thanks to Flex, after one command, you can start using Twig immediately: + +.. code-block:: diff + + render('default/index.html.twig', [ + + 'name' => $name, + + ]); + } + } + +By extending ``AbstractController``, you now have access to a number of shortcut +methods and tools, like ``render()``. Create the new template: + +.. code-block:: html+twig + + {# templates/default/index.html.twig #} +

          Hello {{ name }}

          + +That's it! The ``{{ name }}`` syntax will print the ``name`` variable that's passed +in from the controller. If you're new to Twig, welcome! You'll learn more about +its syntax and power later. + +But, right now, the page *only* contains the ``h1`` tag. To give it an HTML layout, +extend ``base.html.twig``: + +.. code-block:: html+twig + + {# templates/default/index.html.twig #} + {% extends 'base.html.twig' %} + + {% block body %} +

          Hello {{ name }}

          + {% endblock %} + +This is called template inheritance: our page now inherits the HTML structure from +``base.html.twig``. + +Profiler: Debugging Paradise +---------------------------- + +One of the *coolest* features of Symfony isn't even installed yet! Let's fix that: + +.. code-block:: terminal + + $ composer require profiler + +Yes! This is another alias! And Flex *also* installs another recipe, which automates +the configuration of Symfony's Profiler. What's the result? Refresh! + +See that black bar on the bottom? That's the web debug toolbar, and it's your new +best friend. By hovering over each icon, you can get information about what controller +was executed, performance information, cache hits & misses and a lot more. Click +any icon to go into the *profiler* where you have even *more* detailed debugging +and performance data! + +Oh, and as you install more libraries, you'll get more tools (like a web debug toolbar +icon that shows database queries). + +You can now directly use the profiler because it configured *itself* thanks to +the recipe. What else can we install? + +Rich API Support +---------------- + +Are you building an API? You can already return JSON from any controller:: + + // src/Controller/DefaultController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\JsonResponse; + use Symfony\Component\Routing\Attribute\Route; + + class DefaultController extends AbstractController + { + // ... + + #[Route('/api/hello/{name}', methods: ['GET'])] + public function apiHello(string $name): JsonResponse + { + return $this->json([ + 'name' => $name, + 'symfony' => 'rocks', + ]); + } + } + +But for a *truly* rich API, try installing `API Platform`_: + +.. code-block:: terminal + + $ composer require api + +This is an alias to ``api-platform/api-pack`` :ref:`Symfony pack `, +which has dependencies on several other packages, like Symfony's Validator and +Security components, as well as the Doctrine ORM. In fact, Flex installed *5* recipes! + +But like usual, we can immediately start using the new library. Want to create a +rich API for a ``product`` table? Create a ``Product`` entity and give it the +``#[ApiResource]`` attribute:: + + // src/Entity/Product.php + namespace App\Entity; + + use ApiPlatform\Core\Annotation\ApiResource; + use Doctrine\ORM\Mapping as ORM; + + #[ORM\Entity] + #[ApiResource] + class Product + { + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + #[ORM\Column(type: 'integer')] + private int $id; + + #[ORM\Column(type: 'string')] + private string $name; + + #[ORM\Column(type: 'integer')] + private int $price; + + // ... + } + +Done! You now have endpoints to list, add, update and delete products! Don't believe +me? List your routes by running: + +.. code-block:: terminal + + $ php bin/console debug:router + + ------------------------------ -------- ------------------------------------- + Name Method Path + ------------------------------ -------- ------------------------------------- + api_products_get_collection GET /api/products.{_format} + api_products_post_collection POST /api/products.{_format} + api_products_get_item GET /api/products/{id}.{_format} + api_products_put_item PUT /api/products/{id}.{_format} + api_products_delete_item DELETE /api/products/{id}.{_format} + ... + ------------------------------ -------- ------------------------------------- + +.. _ easily-remove-recipes: + +Removing Recipes +---------------- + +Not convinced yet? No problem: remove the library: + +.. code-block:: terminal + + $ composer remove api + +Flex will *uninstall* the recipes: removing files and undoing changes to put your +app back in its original state. Experiment without worry. + +More Features, Architecture and Speed +------------------------------------- + +I hope you're as excited about Flex as I am! But we still have *one* more chapter, +and it's the most important yet. I want to show you how Symfony empowers you to quickly +build features *without* sacrificing code quality or performance. It's all about +the service container, and it's Symfony's super power. Read on: about :doc:`/quick_tour/the_architecture`. + +.. _`RECIPES.md on the recipes repository`: https://github.com/symfony/recipes/blob/flex/main/RECIPES.md +.. _`API Platform`: https://api-platform.com/ +.. _`Twig`: https://twig.symfony.com/ diff --git a/quick_tour/index.rst b/quick_tour/index.rst index 47972e911b3..6239a463be0 100644 --- a/quick_tour/index.rst +++ b/quick_tour/index.rst @@ -5,6 +5,5 @@ The Quick Tour :maxdepth: 1 the_big_picture - the_view - the_controller + flex_recipes the_architecture diff --git a/quick_tour/the_architecture.rst b/quick_tour/the_architecture.rst index 30cffa72fb3..a323461885d 100644 --- a/quick_tour/the_architecture.rst +++ b/quick_tour/the_architecture.rst @@ -1,342 +1,371 @@ The Architecture ================ -You are my hero! Who would have thought that you would still be here after the -first three parts? Your efforts will be well rewarded soon. The first three -parts didn't look too deeply at the architecture of the framework. Because it -makes Symfony2 stand apart from the framework crowd, let's dive into the -architecture now. +You are my hero! Who would have thought that you would still be here after the first +two parts? Your efforts will be well-rewarded soon. The first two parts didn't look +too deeply at the architecture of the framework. Because it makes Symfony stand apart +from the framework crowd, let's dive into the architecture now. -Understanding the Directory Structure -------------------------------------- +Add Logging +----------- -The directory structure of a Symfony2 :term:`application` is rather flexible, -but the directory structure of the *Standard Edition* distribution reflects -the typical and recommended structure of a Symfony2 application: +A new Symfony app is micro: it's basically just a routing & controller system. But +thanks to Flex, installing more features is simple. -* ``app/``: The application configuration; -* ``src/``: The project's PHP code; -* ``vendor/``: The third-party dependencies; -* ``web/``: The web root directory. +Want a logging system? No problem: -The ``web/`` Directory -~~~~~~~~~~~~~~~~~~~~~~ +.. code-block:: terminal -The web root directory is the home of all public and static files like images, -stylesheets, and JavaScript files. It is also where each :term:`front controller` -lives:: + $ composer require logger - // web/app.php - require_once __DIR__.'/../app/bootstrap.php.cache'; - require_once __DIR__.'/../app/AppKernel.php'; +This installs and configures (via a recipe) the powerful `Monolog`_ library. To +use the logger in a controller, add a new argument type-hinted with ``LoggerInterface``:: - use Symfony\Component\HttpFoundation\Request; + // src/Controller/DefaultController.php + namespace App\Controller; - $kernel = new AppKernel('prod', false); - $kernel->loadClassCache(); - $kernel->handle(Request::createFromGlobals())->send(); + use Psr\Log\LoggerInterface; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; -The kernel first requires the ``bootstrap.php.cache`` file, which bootstraps -the framework and registers the autoloader (see below). + class DefaultController extends AbstractController + { + #[Route('/hello/{name}', methods: ['GET'])] + public function index(string $name, LoggerInterface $logger): Response + { + $logger->info("Saying hello to $name!"); + + // ... + } + } + +That's it! The new log message will be written to ``var/log/dev.log``. The log +file path or even a different method of logging can be configured by updating +one of the config files added by the recipe. -Like any front controller, ``app.php`` uses a Kernel Class, ``AppKernel``, to -bootstrap the application. +Services & Autowiring +--------------------- -.. _the-app-dir: +But wait! Something *very* cool just happened. Symfony read the ``LoggerInterface`` +type-hint and automatically figured out that it should pass us the Logger object! +This is called *autowiring*. -The ``app/`` Directory -~~~~~~~~~~~~~~~~~~~~~~ +Every bit of work that's done in a Symfony app is done by an *object*: the Logger +object logs things and the Twig object renders templates. These objects are called +*services* and they are *tools* that help you build rich features. -The ``AppKernel`` class is the main entry point of the application -configuration and as such, it is stored in the ``app/`` directory. +To make life awesome, you can ask Symfony to pass you a service by using a type-hint. +What other possible classes or interfaces could you use? Find out by running: -This class must implement two methods: +.. code-block:: terminal -* ``registerBundles()`` must return an array of all bundles needed to run the - application; + $ php bin/console debug:autowiring -* ``registerContainerConfiguration()`` loads the application configuration - (more on this later). + # this is just a *small* sample of the output... -Autoloading is handled automatically via `Composer`_, which means that you -can use any PHP classes without doing anything at all! If you need more flexibility, -you can extend the autoloader in the ``app/autoload.php`` file. All dependencies -are stored under the ``vendor/`` directory, but this is just a convention. -You can store them wherever you want, globally on your server or locally -in your projects. + Describes a logger instance. + Psr\Log\LoggerInterface - alias:monolog.logger -.. note:: + Request stack that controls the lifecycle of requests. + Symfony\Component\HttpFoundation\RequestStack - alias:request_stack - If you want to learn more about Composer's autoloader, read `Composer-Autoloader`_. - Symfony also has an autoloading component - read ":doc:`/components/class_loader`". + RouterInterface is the interface that all Router classes must implement. + Symfony\Component\Routing\RouterInterface - alias:router.default -Understanding the Bundle System -------------------------------- + [...] -This section introduces one of the greatest and most powerful features of -Symfony2, the :term:`bundle` system. +This is just a short summary of the full list! And as you add more packages, this +list of tools will grow! -A bundle is kind of like a plugin in other software. So why is it called a -*bundle* and not a *plugin*? This is because *everything* is a bundle in -Symfony2, from the core framework features to the code you write for your -application. Bundles are first-class citizens in Symfony2. This gives you -the flexibility to use pre-built features packaged in third-party bundles -or to distribute your own bundles. It makes it easy to pick and choose which -features to enable in your application and optimize them the way you want. -And at the end of the day, your application code is just as *important* as -the core framework itself. +Creating Services +----------------- -Registering a Bundle -~~~~~~~~~~~~~~~~~~~~ +To keep your code organized, you can even create your own services! Suppose you +want to generate a random greeting (e.g. "Hello", "Yo", etc). Instead of putting +this code directly in your controller, create a new class:: -An application is made up of bundles as defined in the ``registerBundles()`` -method of the ``AppKernel`` class. Each bundle is a directory that contains -a single ``Bundle`` class that describes it:: + // src/GreetingGenerator.php + namespace App; - // app/AppKernel.php - public function registerBundles() + class GreetingGenerator { - $bundles = array( - new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), - new Symfony\Bundle\SecurityBundle\SecurityBundle(), - new Symfony\Bundle\TwigBundle\TwigBundle(), - new Symfony\Bundle\MonologBundle\MonologBundle(), - new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(), - new Symfony\Bundle\DoctrineBundle\DoctrineBundle(), - new Symfony\Bundle\AsseticBundle\AsseticBundle(), - new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), - new JMS\SecurityExtraBundle\JMSSecurityExtraBundle(), - ); - - if (in_array($this->getEnvironment(), array('dev', 'test'))) { - $bundles[] = new Acme\DemoBundle\AcmeDemoBundle(); - $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); - $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle(); - $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle(); + public function getRandomGreeting(): string + { + $greetings = ['Hey', 'Yo', 'Aloha']; + $greeting = $greetings[array_rand($greetings)]; + + return $greeting; } + } + +Great! You can use it immediately in your controller:: + + // src/Controller/DefaultController.php + namespace App\Controller; + + use App\GreetingGenerator; + use Psr\Log\LoggerInterface; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; - return $bundles; + class DefaultController extends AbstractController + { + #[Route('/hello/{name}', methods: ['GET'])] + public function index(string $name, LoggerInterface $logger, GreetingGenerator $generator): Response + { + $greeting = $generator->getRandomGreeting(); + + $logger->info("Saying $greeting to $name!"); + + // ... + } } -In addition to the ``AcmeDemoBundle`` that was already talked about, notice -that the kernel also enables other bundles such as the ``FrameworkBundle``, -``DoctrineBundle``, ``SwiftmailerBundle``, and ``AsseticBundle`` bundle. -They are all part of the core framework. - -Configuring a Bundle -~~~~~~~~~~~~~~~~~~~~ - -Each bundle can be customized via configuration files written in YAML, XML, or -PHP. Have a look at the default configuration: - -.. code-block:: yaml - - # app/config/config.yml - imports: - - { resource: parameters.yml } - - { resource: security.yml } - - framework: - #esi: ~ - #translator: { fallback: "%locale%" } - secret: "%secret%" - router: - resource: "%kernel.root_dir%/config/routing.yml" - strict_requirements: "%kernel.debug%" - form: true - csrf_protection: true - validation: { enable_annotations: true } - templating: { engines: ['twig'] } #assets_version: SomeVersionScheme - default_locale: "%locale%" - trusted_proxies: ~ - session: ~ - - # Twig Configuration - twig: - debug: "%kernel.debug%" - strict_variables: "%kernel.debug%" - - # Assetic Configuration - assetic: - debug: "%kernel.debug%" - use_controller: false - bundles: [ ] - #java: /usr/bin/java - filters: - cssrewrite: ~ - #closure: - # jar: "%kernel.root_dir%/Resources/java/compiler.jar" - #yui_css: - # jar: "%kernel.root_dir%/Resources/java/yuicompressor-2.4.7.jar" - - # Doctrine Configuration - doctrine: - dbal: - driver: "%database_driver%" - host: "%database_host%" - port: "%database_port%" - dbname: "%database_name%" - user: "%database_user%" - password: "%database_password%" - charset: UTF8 - - orm: - auto_generate_proxy_classes: "%kernel.debug%" - auto_mapping: true - - # Swiftmailer Configuration - swiftmailer: - transport: "%mailer_transport%" - host: "%mailer_host%" - username: "%mailer_user%" - password: "%mailer_password%" - spool: { type: memory } - -Each entry like ``framework`` defines the configuration for a specific bundle. -For example, ``framework`` configures the ``FrameworkBundle`` while ``swiftmailer`` -configures the ``SwiftmailerBundle``. - -Each :term:`environment` can override the default configuration by providing a -specific configuration file. For example, the ``dev`` environment loads the -``config_dev.yml`` file, which loads the main configuration (i.e. ``config.yml``) -and then modifies it to add some debugging tools: - -.. code-block:: yaml - - # app/config/config_dev.yml - imports: - - { resource: config.yml } - - framework: - router: { resource: "%kernel.root_dir%/config/routing_dev.yml" } - profiler: { only_exceptions: false } - - web_profiler: - toolbar: true - intercept_redirects: false - - monolog: - handlers: - main: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: debug - firephp: - type: firephp - level: info - - assetic: - use_controller: true - -Extending a Bundle -~~~~~~~~~~~~~~~~~~ - -In addition to being a nice way to organize and configure your code, a bundle -can extend another bundle. Bundle inheritance allows you to override any existing -bundle in order to customize its controllers, templates, or any of its files. -This is where the logical names (e.g. ``@AcmeDemoBundle/Controller/SecuredController.php``) -come in handy: they abstract where the resource is actually stored. - -Logical File Names -.................. - -When you want to reference a file from a bundle, use this notation: -``@BUNDLE_NAME/path/to/file``; Symfony2 will resolve ``@BUNDLE_NAME`` -to the real path to the bundle. For instance, the logical path -``@AcmeDemoBundle/Controller/DemoController.php`` would be converted to -``src/Acme/DemoBundle/Controller/DemoController.php``, because Symfony knows -the location of the ``AcmeDemoBundle``. - -Logical Controller Names -........................ - -For controllers, you need to reference method names using the format -``BUNDLE_NAME:CONTROLLER_NAME:ACTION_NAME``. For instance, -``AcmeDemoBundle:Welcome:index`` maps to the ``indexAction`` method from the -``Acme\DemoBundle\Controller\WelcomeController`` class. - -Logical Template Names -...................... - -For templates, the logical name ``AcmeDemoBundle:Welcome:index.html.twig`` is -converted to the file path ``src/Acme/DemoBundle/Resources/views/Welcome/index.html.twig``. -Templates become even more interesting when you realize they don't need to be -stored on the filesystem. You can easily store them in a database table for -instance. - -Extending Bundles -................. - -If you follow these conventions, then you can use :doc:`bundle inheritance` -to "override" files, controllers or templates. For example, you can create -a bundle - ``AcmeNewBundle`` - and specify that it overrides ``AcmeDemoBundle``. -When Symfony loads the ``AcmeDemoBundle:Welcome:index`` controller, it will -first look for the ``WelcomeController`` class in ``AcmeNewBundle`` and, if -it doesn't exist, then look inside ``AcmeDemoBundle``. This means that one bundle -can override almost any part of another bundle! - -Do you understand now why Symfony2 is so flexible? Share your bundles between -applications, store them locally or globally, your choice. - -.. _using-vendors: - -Using Vendors -------------- - -Odds are that your application will depend on third-party libraries. Those -should be stored in the ``vendor/`` directory. This directory already contains -the Symfony2 libraries, the SwiftMailer library, the Doctrine ORM, the Twig -templating system, and some other third party libraries and bundles. - -Understanding the Cache and Logs --------------------------------- - -Symfony2 is probably one of the fastest full-stack frameworks around. But how -can it be so fast if it parses and interprets tens of YAML and XML files for -each request? The speed is partly due to its cache system. The application -configuration is only parsed for the very first request and then compiled down -to plain PHP code stored in the ``app/cache/`` directory. In the development -environment, Symfony2 is smart enough to flush the cache when you change a -file. But in the production environment, it is your responsibility to clear -the cache when you update your code or change its configuration. - -When developing a web application, things can go wrong in many ways. The log -files in the ``app/logs/`` directory tell you everything about the requests -and help you fix the problem quickly. - -Using the Command Line Interface --------------------------------- - -Each application comes with a command line interface tool (``app/console``) -that helps you maintain your application. It provides commands that boost your -productivity by automating tedious and repetitive tasks. - -Run it without any arguments to learn more about its capabilities: +That's it! Symfony will instantiate the ``GreetingGenerator`` automatically and +pass it as an argument. But, could we *also* move the logger logic to ``GreetingGenerator``? +Yes! You can use autowiring inside a service to access *other* services. The only +difference is that it's done in the constructor: -.. code-block:: bash +.. code-block:: diff + + logger->info('Using the greeting: '.$greeting); + + return $greeting; + } + } + +Yes! This works too: no configuration, no time wasted. Keep coding! + +Twig Extension & Autoconfiguration +---------------------------------- + +Thanks to Symfony's service handling, you can *extend* Symfony in many ways, like +by creating an event subscriber or a security voter for complex authorization +rules. Let's add a new filter to Twig called ``greet``. How? Create a class +that extends ``AbstractExtension``:: + + // src/Twig/GreetExtension.php + namespace App\Twig; + + use App\GreetingGenerator; + use Twig\Extension\AbstractExtension; + use Twig\TwigFilter; + + class GreetExtension extends AbstractExtension + { + public function __construct( + private GreetingGenerator $greetingGenerator, + ) { + } + + public function getFilters(): array + { + return [ + new TwigFilter('greet', [$this, 'greetUser']), + ]; + } + + public function greetUser(string $name): string + { + $greeting = $this->greetingGenerator->getRandomGreeting(); + + return "$greeting $name!"; + } + } + +After creating just *one* file, you can use this immediately: + +.. code-block:: html+twig + + {# templates/default/index.html.twig #} + {# Will print something like "Hey Symfony!" #} +

          {{ name|greet }}

          - $ php app/console +How does this work? Symfony notices that your class extends ``AbstractExtension`` +and so *automatically* registers it as a Twig extension. This is called autoconfiguration, +and it works for *many* many things. Create a class and then extend a base class +(or implement an interface). Symfony takes care of the rest. -The ``--help`` option helps you discover the usage of a command: +Blazing Speed: The Cached Container +----------------------------------- + +After seeing how much Symfony handles automatically, you might be wondering: "Doesn't +this hurt performance?" Actually, no! Symfony is blazing fast. + +How is that possible? The service system is managed by a very important object called +the "container". Most frameworks have a container, but Symfony's is unique because +it's *cached*. When you loaded your first page, all of the service information was +compiled and saved. This means that the autowiring and autoconfiguration features +add *no* overhead! It also means that you get *great* errors: Symfony inspects and +validates *everything* when the container is built. + +Now you might be wondering what happens when you update a file and the cache needs +to rebuild? I like your thinking! It's smart enough to rebuild on the next page +load. But that's really the topic of the next section. + +Development Versus Production: Environments +------------------------------------------- + +One of a framework's main jobs is to make debugging easy! And our app is *full* of +great tools for this: the web debug toolbar displays at the bottom of the page, errors +are big, beautiful & explicit, and any configuration cache is automatically rebuilt +whenever needed. + +But what about when you deploy to production? We will need to hide those tools and +optimize for speed! + +This is solved by Symfony's *environment* system. Symfony applications begin with +three environments: ``dev``, ``prod``, and ``test``. You can define options for +specific environments in the configuration files from the ``config/`` directory +using the special ``when@`` keyword: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/routing.yaml + framework: + router: + utf8: true + + when@prod: + framework: + router: + strict_requirements: null + + .. code-block:: xml + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework, ContainerConfigurator $container): void { + $framework->router() + ->utf8(true) + ; + + if ('prod' === $container->env()) { + $framework->router() + ->strictRequirements(null) + ; + } + }; + +This is a *powerful* idea: by changing one piece of configuration (the environment), +your app is transformed from a debugging-friendly experience to one that's optimized +for speed. + +Oh, how do you change the environment? Change the ``APP_ENV`` environment variable +from ``dev`` to ``prod``: + +.. code-block:: diff + + # .env + - APP_ENV=dev + + APP_ENV=prod + +But I want to talk more about environment variables next. Change the value back +to ``dev``: debugging tools are great when you're working locally. + +Environment Variables +--------------------- + +Every app contains configuration that's different on each server - like database +connection information or passwords. How should these be stored? In files? Or another way? + +Symfony follows the industry best practice by storing server-based configuration +as *environment* variables. This means that Symfony works *perfectly* with +Platform as a Service (PaaS) deployment systems as well as Docker. + +But setting environment variables while developing can be a pain. That's why your +app automatically loads a ``.env`` file. The keys in this file then become environment +variables and are read by your app: .. code-block:: bash - $ php app/console router:debug --help + # .env + ###> symfony/framework-bundle ### + APP_ENV=dev + APP_SECRET=cc86c7ca937636d5ddf1b754beb22a10 + ###< symfony/framework-bundle ### + +At first, the file doesn't contain much. But as your app grows, you'll add more +configuration as you need it. But, actually, it gets much more interesting! Suppose +your app needs a database ORM. Let's install the Doctrine ORM: + +.. code-block:: terminal + + $ composer require doctrine + +Thanks to a new recipe installed by Flex, look at the ``.env`` file again: + +.. code-block:: diff + + ###> symfony/framework-bundle ### + APP_ENV=dev + APP_SECRET=cc86c7ca937636d5ddf1b754beb22a10 + ###< symfony/framework-bundle ### + + + ###> doctrine/doctrine-bundle ### + + # ... + + DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name + + ###< doctrine/doctrine-bundle ### + +The new ``DATABASE_URL`` environment variable was added *automatically* and is already +referenced by the new ``doctrine.yaml`` configuration file. By combining environment +variables and Flex, you're using industry best practices without any extra effort. -Final Thoughts --------------- +Keep Going! +----------- -Call me crazy, but after reading this part, you should be comfortable with -moving things around and making Symfony2 work for you. Everything in Symfony2 -is designed to get out of your way. So, feel free to rename and move directories -around as you see fit. +Call me crazy, but after reading this part, you should be comfortable with the most +*important* parts of Symfony. Everything in Symfony is designed to get out of your +way so you can keep coding and adding features, all with the speed and quality you +demand. -And that's all for the quick tour. From testing to sending emails, you still -need to learn a lot to become a Symfony2 master. Ready to dig into these -topics now? Look no further - go to the official :doc:`/book/index` and pick -any topic you want. +That's all for the quick tour. From authentication, to forms, to caching, there is +so much more to discover. Ready to dig into these topics now? Look no further - go +to the official :doc:`/index` and pick any guide you want. -.. _standards: http://symfony.com/PSR0 -.. _convention: http://pear.php.net/ -.. _Composer: http://getcomposer.org -.. _`Composer-Autoloader`: http://getcomposer.org/doc/01-basic-usage.md#autoloading +.. _`Monolog`: https://github.com/Seldaek/monolog diff --git a/quick_tour/the_big_picture.rst b/quick_tour/the_big_picture.rst index 9c8a77a6b80..b069cb4f716 100644 --- a/quick_tour/the_big_picture.rst +++ b/quick_tour/the_big_picture.rst @@ -1,518 +1,163 @@ The Big Picture =============== -Start using Symfony2 in 10 minutes! This chapter will walk you through some -of the most important concepts behind Symfony2 and explain how you can get -started quickly by showing you a simple project in action. +Start using Symfony in 10 minutes! Really! That's all you need to understand the +most important concepts and start building a real project! If you've used a web framework before, you should feel right at home with -Symfony2. If not, welcome to a whole new way of developing web applications! +Symfony. If not, welcome to a whole new way of developing web applications. Symfony +*embraces* best practices, keeps backwards compatibility (Yes! Upgrading is always +safe & easy!) and offers long-term support. -.. tip:: +.. _installing-symfony2: - Want to learn why and when you need to use a framework? Read the "`Symfony - in 5 minutes`_" document. +Downloading Symfony +------------------- -Downloading Symfony2 --------------------- +First, make sure you've installed `Composer`_ and have PHP 8.1 or higher. -First, check that you have installed and configured a Web server (such as -Apache) with PHP 5.3.3 or higher. +Ready? In a terminal, run: -.. tip:: +.. code-block:: terminal - If you have PHP 5.4, you could use the built-in web server. The built-in - server should be used only for development purpose, but it can help you - to start your project quickly and easily. + $ composer create-project symfony/skeleton quick_tour - Just use this command to launch the server: - - .. code-block:: bash - - $ php -S localhost:80 -t /path/to/www - - where "/path/to/www" is the path to some directory on your machine that - you'll extract Symfony into so that the eventual URL to your application - is "http://localhost/Symfony/app_dev.php". You can also extract Symfony - first and then start the web server in the Symfony "web" directory. If - you do this, the URL to your application will be "http://localhost/app_dev.php". - -Ready? Start by downloading the "`Symfony2 Standard Edition`_", a Symfony -:term:`distribution` that is preconfigured for the most common use cases and -also contains some code that demonstrates how to use Symfony2 (get the archive -with the *vendors* included to get started even faster). - -After unpacking the archive under your web server root directory, you should -have a ``Symfony/`` directory that looks like this: - -.. code-block:: text - - www/ <- your web root directory - Symfony/ <- the unpacked archive - app/ - cache/ - config/ - logs/ - Resources/ - bin/ - src/ - Acme/ - DemoBundle/ - Controller/ - Resources/ - ... - vendor/ - symfony/ - doctrine/ - ... - web/ - app.php - ... - -.. note:: - - If you are familiar with Composer, you can run the following command - instead of downloading the archive: - - .. code-block:: bash - - $ composer.phar create-project symfony/framework-standard-edition path/to/install 2.2.0 - - # remove the Git history - $ rm -rf .git - - For an exact version, replace `2.2.0` with the latest Symfony version - (e.g. 2.1.1). For details, see the `Symfony Installation Page`_ - -.. tip:: - - If you have PHP 5.4, you can use the built-in web server: - - .. code-block:: bash - - # check your PHP CLI configuration - $ php ./app/check.php - - # run the built-in web server - $ php ./app/console server:run - - Then the URL to your application will be "http://localhost:8000/app_dev.php" - - The built-in server should be used only for development purpose, but it - can help you to start your project quickly and easily. - -Checking the Configuration --------------------------- - -Symfony2 comes with a visual server configuration tester to help avoid some -headaches that come from Web server or PHP misconfiguration. Use the following -URL to see the diagnostics for your machine: - -.. code-block:: text - - http://localhost/config.php - -.. note:: - - All of the example URLs assume that you've downloaded and unzipped Symfony - directly into the web server web root. If you've followed the directions - above and unzipped the `Symfony` directory into your web root, then add - `/Symfony/web` after `localhost` for all the URLs you see: - - .. code-block:: text - - http://localhost/Symfony/web/config.php - -.. note:: - - All of the example URLs assume that you've downloaded and unzipped ``Symfony`` - directly into the web server web root. If you've followed the directions - above and done this, then add ``/Symfony/web`` after ``localhost`` for all - the URLs you see: - - .. code-block:: text - - http://localhost/Symfony/web/config.php - - To get nice and short urls you should point the document root of your - webserver or virtual host to the ``Symfony/web/`` directory. In that - case, your URLs will look like ``http://localhost/config.php`` or - ``http://site.local/config.php``, if you created a virtual host to a - local domain called, for example, ``site.local``. - -If there are any outstanding issues listed, correct them. You might also tweak -your configuration by following any given recommendations. When everything is -fine, click on "*Bypass configuration and go to the Welcome page*" to request -your first "real" Symfony2 webpage: - -.. code-block:: text - - http://localhost/app_dev.php/ - -Symfony2 should welcome and congratulate you for your hard work so far! - -.. image:: /images/quick_tour/welcome.png - :align: center - -Understanding the Fundamentals ------------------------------- - -One of the main goals of a framework is to ensure `Separation of Concerns`_. -This keeps your code organized and allows your application to evolve easily -over time by avoiding the mixing of database calls, HTML tags, and business -logic in the same script. To achieve this goal with Symfony, you'll first -need to learn a few fundamental concepts and terms. - -.. tip:: - - Want proof that using a framework is better than mixing everything - in the same script? Read the ":doc:`/book/from_flat_php_to_symfony2`" - chapter of the book. - -The distribution comes with some sample code that you can use to learn more -about the main Symfony2 concepts. Go to the following URL to be greeted by -Symfony2 (replace *Fabien* with your first name): +This creates a new ``quick_tour/`` directory with a small, but powerful new +Symfony application: .. code-block:: text - http://localhost/app_dev.php/demo/hello/Fabien - -.. image:: /images/quick_tour/hello_fabien.png - :align: center - -What's going on here? Let's dissect the URL: - -* ``app_dev.php``: This is a :term:`front controller`. It is the unique entry - point of the application and it responds to all user requests; - -* ``/demo/hello/Fabien``: This is the *virtual path* to the resource the user - wants to access. - -Your responsibility as a developer is to write the code that maps the user's -*request* (``/demo/hello/Fabien``) to the *resource* associated with it -(the ``Hello Fabien!`` HTML page). - -Routing -~~~~~~~ - -Symfony2 routes the request to the code that handles it by trying to match the -requested URL against some configured paths. By default, these paths -(called routes) are defined in the ``app/config/routing.yml`` configuration -file. When you're in the ``dev`` :ref:`environment` - -indicated by the app_**dev**.php front controller - the ``app/config/routing_dev.yml`` -configuration file is also loaded. In the Standard Edition, the routes to -these "demo" pages are imported from this file: + quick_tour/ + ├─ .env + ├─ bin/console + ├─ composer.json + ├─ composer.lock + ├─ config/ + ├─ public/index.php + ├─ src/ + ├─ symfony.lock + ├─ var/ + └─ vendor/ -.. code-block:: yaml +Can we already load the project in a browser? Yes! You can set up +:doc:`Nginx or Apache ` and configure their +document root to be the ``public/`` directory. But, for development, it's better +to :doc:`install the Symfony local web server ` and run +it as follows: - # app/config/routing_dev.yml - # ... +.. code-block:: terminal - # AcmeDemoBundle routes (to be removed) - _acme_demo: - resource: "@AcmeDemoBundle/Resources/config/routing.yml" + $ symfony server:start -This imports a ``routing.yml`` file that lives inside the AcmeDemoBundle: +Try your new app by going to ``http://localhost:8000`` in a browser! -.. code-block:: yaml +.. image:: /_images/quick_tour/no_routes_page.png + :alt: The default Symfony welcome page. + :class: with-browser - # src/Acme/DemoBundle/Resources/config/routing.yml - _welcome: - path: / - defaults: { _controller: AcmeDemoBundle:Welcome:index } +Fundamentals: Route, Controller, Response +----------------------------------------- - _demo: - resource: "@AcmeDemoBundle/Controller/DemoController.php" - type: annotation - prefix: /demo +Our project only has about 15 files, but it's ready to become a sleek API, a robust +web app, or a microservice. Symfony starts small, but scales with you. - # ... +But before we go too far, let's dig into the fundamentals by building our first page. -The first three lines (after the comment) define the code that is executed -when the user requests the "``/``" resource (i.e. the welcome page you saw -earlier). When requested, the ``AcmeDemoBundle:Welcome:index`` controller -will be executed. In the next section, you'll learn exactly what that means. +In ``src/Controller``, create a new ``DefaultController`` class and an ``index`` +method inside:: -.. tip:: - - The Symfony2 Standard Edition uses `YAML`_ for its configuration files, - but Symfony2 also supports XML, PHP, and annotations natively. The - different formats are compatible and may be used interchangeably within an - application. Also, the performance of your application does not depend on - the configuration format you choose as everything is cached on the very - first request. - -Controllers -~~~~~~~~~~~ - -A controller is a fancy name for a PHP function or method that handles incoming -*requests* and returns *responses* (often HTML code). Instead of using the -PHP global variables and functions (like ``$_GET`` or ``header()``) to manage -these HTTP messages, Symfony uses objects: :class:`Symfony\\Component\\HttpFoundation\\Request` -and :class:`Symfony\\Component\\HttpFoundation\\Response`. The simplest possible -controller might create the response by hand, based on the request:: + // src/Controller/DefaultController.php + namespace App\Controller; use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; - $name = $request->query->get('name'); - - return new Response('Hello '.$name, 200, array('Content-Type' => 'text/plain')); - -.. note:: - - Symfony2 embraces the HTTP Specification, which are the rules that govern - all communication on the Web. Read the ":doc:`/book/http_fundamentals`" - chapter of the book to learn more about this and the added power that - this brings. - -Symfony2 chooses the controller based on the ``_controller`` value from the -routing configuration: ``AcmeDemoBundle:Welcome:index``. This string is the -controller *logical name*, and it references the ``indexAction`` method from -the ``Acme\DemoBundle\Controller\WelcomeController`` class:: - - // src/Acme/DemoBundle/Controller/WelcomeController.php - namespace Acme\DemoBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - - class WelcomeController extends Controller + class DefaultController { - public function indexAction() + #[Route('/', name: 'index')] + public function index(): Response { - return $this->render('AcmeDemoBundle:Welcome:index.html.twig'); + return new Response('Hello!'); } } -.. tip:: +That's it! Try going to the homepage: ``http://localhost:8000/``. Symfony sees +that the URL matches our route and then executes the new ``index()`` method. - You could have used the full class and method name - - ``Acme\DemoBundle\Controller\WelcomeController::indexAction`` - for the - ``_controller`` value. But if you follow some simple conventions, the - logical name is shorter and allows for more flexibility. +A controller is just a normal function with *one* rule: it must return a Symfony +``Response`` object. But that response can contain anything: simple text, JSON or +a full HTML page. -The ``WelcomeController`` class extends the built-in ``Controller`` class, -which provides useful shortcut methods, like the -:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::render` -method that loads and renders a template -(``AcmeDemoBundle:Welcome:index.html.twig``). The returned value is a Response -object populated with the rendered content. So, if the needs arise, the -Response can be tweaked before it is sent to the browser:: +But the routing system is *much* more powerful. So let's make the route more interesting: - public function indexAction() - { - $response = $this->render('AcmeDemoBundle:Welcome:index.txt.twig'); - $response->headers->set('Content-Type', 'text/plain'); +.. code-block:: diff - return $response; - } + // src/Controller/DefaultController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; -No matter how you do it, the end goal of your controller is always to return -the ``Response`` object that should be delivered back to the user. This ``Response`` -object can be populated with HTML code, represent a client redirect, or even -return the contents of a JPG image with a ``Content-Type`` header of ``image/jpg``. + class DefaultController + { + - #[Route('/', name: 'index')] + + #[Route('/hello/{name}', name: 'index')] + public function index(): Response + { + return new Response('Hello!'); + } + } -.. tip:: +The URL to this page has changed: it is *now* ``/hello/*``: the ``{name}`` acts +like a wildcard that matches anything. And it gets better! Update the controller too: - Extending the ``Controller`` base class is optional. As a matter of fact, - a controller can be a plain PHP function or even a PHP closure. - ":doc:`The Controller`" chapter of the book tells you - everything about Symfony2 controllers. +.. code-block:: diff -The template name, ``AcmeDemoBundle:Welcome:index.html.twig``, is the template -*logical name* and it references the -``Resources/views/Welcome/index.html.twig`` file inside the ``AcmeDemoBundle`` -(located at ``src/Acme/DemoBundle``). The bundles section below will explain -why this is useful. + $name); + return new Response('Simple! Easy! Great!'); } - - // ... } -The ``@Route()`` annotation defines a new route with a path of -``/hello/{name}`` that executes the ``helloAction`` method when matched. A -string enclosed in curly brackets like ``{name}`` is called a placeholder. As -you can see, its value can be retrieved through the ``$name`` method argument. - -.. note:: - - Even if annotations are not natively supported by PHP, you use them - extensively in Symfony2 as a convenient way to configure the framework - behavior and keep the configuration next to the code. - -If you take a closer look at the controller code, you can see that instead of -rendering a template and returning a ``Response`` object like before, it -just returns an array of parameters. The ``@Template()`` annotation tells -Symfony to render the template for you, passing in each variable of the array -to the template. The name of the template that's rendered follows the name -of the controller. So, in this example, the ``AcmeDemoBundle:Demo:hello.html.twig`` -template is rendered (located at ``src/Acme/DemoBundle/Resources/views/Demo/hello.html.twig``). - -.. tip:: - - The ``@Route()`` and ``@Template()`` annotations are more powerful than - the simple examples shown in this tutorial. Learn more about "`annotations - in controllers`_" in the official documentation. - -Templates -~~~~~~~~~ - -The controller renders the -``src/Acme/DemoBundle/Resources/views/Demo/hello.html.twig`` template (or -``AcmeDemoBundle:Demo:hello.html.twig`` if you use the logical name): +Routing can do *even* more, but we'll save that for another time! Right now, our +app needs more features! Like a template engine, logging, debugging tools and more. -.. code-block:: jinja - - {# src/Acme/DemoBundle/Resources/views/Demo/hello.html.twig #} - {% extends "AcmeDemoBundle::layout.html.twig" %} - - {% block title "Hello " ~ name %} - - {% block content %} -

          Hello {{ name }}!

          - {% endblock %} - -By default, Symfony2 uses `Twig`_ as its template engine but you can also use -traditional PHP templates if you choose. The next chapter will introduce how -templates work in Symfony2. - -Bundles -~~~~~~~ - -You might have wondered why the :term:`bundle` word is used in many names you -have seen so far. All the code you write for your application is organized in -bundles. In Symfony2 speak, a bundle is a structured set of files (PHP files, -stylesheets, JavaScripts, images, ...) that implements a single feature (a -blog, a forum, ...) and which can be easily shared with other developers. As -of now, you have manipulated one bundle, ``AcmeDemoBundle``. You will learn -more about bundles in the last chapter of this tutorial. - -.. _quick-tour-big-picture-environments: - -Working with Environments -------------------------- - -Now that you have a better understanding of how Symfony2 works, take a closer -look at the bottom of any Symfony2 rendered page. You should notice a small -bar with the Symfony2 logo. This is called the "Web Debug Toolbar" and it -is the developer's best friend. - -.. image:: /images/quick_tour/web_debug_toolbar.png - :align: center - -But what you see initially is only the tip of the iceberg; click on the weird -hexadecimal number to reveal yet another very useful Symfony2 debugging tool: -the profiler. - -.. image:: /images/quick_tour/profiler.png - :align: center - -.. note:: - - You can get more information quickly by hovering over the items on the - Web Debug Toolbar. - -Of course, you won't want to show these tools when you deploy your application -to production. That's why you will find another front controller in the -``web/`` directory (``app.php``), which is optimized for the production environment: - -.. code-block:: text - - http://localhost/app.php/demo/hello/Fabien - -And if you use Apache with ``mod_rewrite`` enabled, you can even omit the -``app.php`` part of the URL: - -.. code-block:: text - - http://localhost/demo/hello/Fabien - -Last but not least, on the production servers, you should point your web root -directory to the ``web/`` directory to secure your installation and have an -even better looking URL: - -.. code-block:: text +Keep reading with :doc:`/quick_tour/flex_recipes`. - http://localhost/demo/hello/Fabien - -.. note:: - - Note that the three URLs above are provided here only as **examples** of - how a URL looks like when the production front controller is used (with or - without mod_rewrite). If you actually try them in an out of the box - installation of *Symfony Standard Edition* you will get a 404 error as - *AcmeDemoBundle* is enabled only in dev environment and its routes imported - in *app/config/routing_dev.yml*. - -To make your application respond faster, Symfony2 maintains a cache under the -``app/cache/`` directory. In the development environment (``app_dev.php``), -this cache is flushed automatically whenever you make changes to any code or -configuration. But that's not the case in the production environment -(``app.php``) where performance is key. That's why you should always use -the development environment when developing your application. - -Different :term:`environments` of a given application differ -only in their configuration. In fact, a configuration can inherit from another -one: - -.. code-block:: yaml - - # app/config/config_dev.yml - imports: - - { resource: config.yml } - - web_profiler: - toolbar: true - intercept_redirects: false - -The ``dev`` environment (which loads the ``config_dev.yml`` configuration file) -imports the global ``config.yml`` file and then modifies it by, in this example, -enabling the web debug toolbar. - -Final Thoughts --------------- - -Congratulations! You've had your first taste of Symfony2 code. That wasn't so -hard, was it? There's a lot more to explore, but you should already see how -Symfony2 makes it really easy to implement web sites better and faster. If you -are eager to learn more about Symfony2, dive into the next section: -":doc:`The View`". - -.. _Symfony2 Standard Edition: http://symfony.com/download -.. _Symfony in 5 minutes: http://symfony.com/symfony-in-five-minutes -.. _Separation of Concerns: http://en.wikipedia.org/wiki/Separation_of_concerns -.. _YAML: http://www.yaml.org/ -.. _annotations in controllers: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html#annotations-for-controllers -.. _Twig: http://twig.sensiolabs.org/ -.. _`Symfony Installation Page`: http://symfony.com/download +.. _`Composer`: https://getcomposer.org/ diff --git a/quick_tour/the_controller.rst b/quick_tour/the_controller.rst deleted file mode 100755 index 51a160204be..00000000000 --- a/quick_tour/the_controller.rst +++ /dev/null @@ -1,283 +0,0 @@ -The Controller -============== - -Still here after the first two parts? You are already becoming a Symfony2 -addict! Without further ado, discover what controllers can do for you. - -Using Formats -------------- - -Nowadays, a web application should be able to deliver more than just HTML -pages. From XML for RSS feeds or Web Services, to JSON for Ajax requests, -there are plenty of different formats to choose from. Supporting those formats -in Symfony2 is straightforward. Tweak the route by adding a default value of -``xml`` for the ``_format`` variable:: - - // src/Acme/DemoBundle/Controller/DemoController.php - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; - - // ... - - /** - * @Route("/hello/{name}", defaults={"_format"="xml"}, name="_demo_hello") - * @Template() - */ - public function helloAction($name) - { - return array('name' => $name); - } - -By using the request format (as defined by the ``_format`` value), Symfony2 -automatically selects the right template, here ``hello.xml.twig``: - -.. code-block:: xml+php - - - - {{ name }} - - -That's all there is to it. For standard formats, Symfony2 will also -automatically choose the best ``Content-Type`` header for the response. If -you want to support different formats for a single action, use the ``{_format}`` -placeholder in the route path instead:: - - // src/Acme/DemoBundle/Controller/DemoController.php - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; - - // ... - - /** - * @Route("/hello/{name}.{_format}", defaults={"_format"="html"}, requirements={"_format"="html|xml|json"}, name="_demo_hello") - * @Template() - */ - public function helloAction($name) - { - return array('name' => $name); - } - -The controller will now be called for URLs like ``/demo/hello/Fabien.xml`` or -``/demo/hello/Fabien.json``. - -The ``requirements`` entry defines regular expressions that placeholders must -match. In this example, if you try to request the ``/demo/hello/Fabien.js`` -resource, you will get a 404 HTTP error, as it does not match the ``_format`` -requirement. - -Redirecting and Forwarding --------------------------- - -If you want to redirect the user to another page, use the ``redirect()`` -method:: - - return $this->redirect($this->generateUrl('_demo_hello', array('name' => 'Lucas'))); - -The ``generateUrl()`` is the same method as the ``path()`` function used in the -templates. It takes the route name and an array of parameters as arguments and -returns the associated friendly URL. - -You can also easily forward the action to another one with the ``forward()`` -method. Internally, Symfony makes a "sub-request", and returns the ``Response`` -object from that sub-request:: - - $response = $this->forward('AcmeDemoBundle:Hello:fancy', array('name' => $name, 'color' => 'green')); - - // ... do something with the response or return it directly - -Getting information from the Request ------------------------------------- - -Besides the values of the routing placeholders, the controller also has access -to the ``Request`` object:: - - $request = $this->getRequest(); - - $request->isXmlHttpRequest(); // is it an Ajax request? - - $request->getPreferredLanguage(array('en', 'fr')); - - $request->query->get('page'); // get a $_GET parameter - - $request->request->get('page'); // get a $_POST parameter - -In a template, you can also access the ``Request`` object via the -``app.request`` variable: - -.. code-block:: html+jinja - - {{ app.request.query.get('page') }} - - {{ app.request.parameter('page') }} - -Persisting Data in the Session ------------------------------- - -Even if the HTTP protocol is stateless, Symfony2 provides a nice session object -that represents the client (be it a real person using a browser, a bot, or a -web service). Between two requests, Symfony2 stores the attributes in a cookie -by using native PHP sessions. - -Storing and retrieving information from the session can be easily achieved -from any controller:: - - $session = $this->getRequest()->getSession(); - - // store an attribute for reuse during a later user request - $session->set('foo', 'bar'); - - // in another controller for another request - $foo = $session->get('foo'); - - // use a default value if the key doesn't exist - $filters = $session->set('filters', array()); - -You can also store small messages that will only be available for the very -next request:: - - // store a message for the very next request (in a controller) - $session->getFlashBag()->add('notice', 'Congratulations, your action succeeded!'); - - // display any messages back in the next request (in a template) - - {% for flashMessage in app.session.flashbag.get('notice') %} -
          {{ flashMessage }}
          - {% endfor %} - -This is useful when you need to set a success message before redirecting -the user to another page (which will then show the message). Please note that -when you use has() instead of get(), the flash message will not be cleared and -thus remains available during the following requests. - -Securing Resources ------------------- - -The Symfony Standard Edition comes with a simple security configuration that -fits most common needs: - -.. code-block:: yaml - - # app/config/security.yml - security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext - - role_hierarchy: - ROLE_ADMIN: ROLE_USER - ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] - - providers: - in_memory: - memory: - users: - user: { password: userpass, roles: [ 'ROLE_USER' ] } - admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] } - - firewalls: - dev: - pattern: ^/(_(profiler|wdt)|css|images|js)/ - security: false - - login: - pattern: ^/demo/secured/login$ - security: false - - secured_area: - pattern: ^/demo/secured/ - form_login: - check_path: /demo/secured/login_check - login_path: /demo/secured/login - logout: - path: /demo/secured/logout - target: /demo/ - -This configuration requires users to log in for any URL starting with -``/demo/secured/`` and defines two valid users: ``user`` and ``admin``. -Moreover, the ``admin`` user has a ``ROLE_ADMIN`` role, which includes the -``ROLE_USER`` role as well (see the ``role_hierarchy`` setting). - -.. tip:: - - For readability, passwords are stored in clear text in this simple - configuration, but you can use any hashing algorithm by tweaking the - ``encoders`` section. - -Going to the ``http://localhost/app_dev.php/demo/secured/hello`` -URL will automatically redirect you to the login form because this resource is -protected by a ``firewall``. - -You can also force the action to require a given role by using the ``@Secure`` -annotation on the controller:: - - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; - use JMS\SecurityExtraBundle\Annotation\Secure; - - /** - * @Route("/hello/admin/{name}", name="_demo_secured_hello_admin") - * @Secure(roles="ROLE_ADMIN") - * @Template() - */ - public function helloAdminAction($name) - { - return array('name' => $name); - } - -Now, log in as ``user`` (who does *not* have the ``ROLE_ADMIN`` role) and -from the secured hello page, click on the "Hello resource secured" link. -Symfony2 should return a 403 HTTP status code, indicating that the user -is "forbidden" from accessing that resource. - -.. note:: - - The Symfony2 security layer is very flexible and comes with many different - user providers (like one for the Doctrine ORM) and authentication providers - (like HTTP basic, HTTP digest, or X509 certificates). Read the - ":doc:`/book/security`" chapter of the book for more information - on how to use and configure them. - -Caching Resources ------------------ - -As soon as your website starts to generate more traffic, you will want to -avoid generating the same resource again and again. Symfony2 uses HTTP cache -headers to manage resources cache. For simple caching strategies, use the -convenient ``@Cache()`` annotation:: - - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; - - /** - * @Route("/hello/{name}", name="_demo_hello") - * @Template() - * @Cache(maxage="86400") - */ - public function helloAction($name) - { - return array('name' => $name); - } - -In this example, the resource will be cached for a day. But you can also use -validation instead of expiration or a combination of both if that fits your -needs better. - -Resource caching is managed by the Symfony2 built-in reverse proxy. But because -caching is managed using regular HTTP cache headers, you can replace the -built-in reverse proxy with Varnish or Squid and easily scale your application. - -.. note:: - - But what if you cannot cache whole pages? Symfony2 still has the solution - via Edge Side Includes (ESI), which are supported natively. Learn more by - reading the ":doc:`/book/http_cache`" chapter of the book. - -Final Thoughts --------------- - -That's all there is to it, and I'm not even sure you'll have spent the full -10 minutes. You were briefly introduced to bundles in the first part, and all the -features you've learned about so far are part of the core framework bundle. -But thanks to bundles, everything in Symfony2 can be extended or replaced. -That's the topic of the :doc:`next part of this tutorial`. diff --git a/quick_tour/the_view.rst b/quick_tour/the_view.rst deleted file mode 100644 index 60788277a0d..00000000000 --- a/quick_tour/the_view.rst +++ /dev/null @@ -1,294 +0,0 @@ -The View -======== - -After reading the first part of this tutorial, you have decided that Symfony2 -was worth another 10 minutes. Great choice! In this second part, you will -learn more about the Symfony2 template engine, `Twig`_. Twig is a flexible, -fast, and secure template engine for PHP. It makes your templates more -readable and concise; it also makes them more friendly for web designers. - -.. note:: - - Instead of Twig, you can also use :doc:`PHP ` - for your templates. Both template engines are supported by Symfony2. - -Getting familiar with Twig --------------------------- - -.. tip:: - - If you want to learn Twig, it's highly recommended you read its official - `documentation`_. This section is just a quick overview of the main - concepts. - -A Twig template is a text file that can generate any type of content (HTML, -XML, CSV, LaTeX, ...). Twig defines two kinds of delimiters: - -* ``{{ ... }}``: Prints a variable or the result of an expression; - -* ``{% ... %}``: Controls the logic of the template; it is used to execute - ``for`` loops and ``if`` statements, for example. - -Below is a minimal template that illustrates a few basics, using two variables -``page_title`` and ``navigation``, which would be passed into the template: - -.. code-block:: html+jinja - - - - - My Webpage - - -

          {{ page_title }}

          - - - - - - -.. tip:: - - Comments can be included inside templates using the ``{# ... #}`` delimiter. - -To render a template in Symfony, use the ``render`` method from within a controller -and pass it any variables needed in the template:: - - $this->render('AcmeDemoBundle:Demo:hello.html.twig', array( - 'name' => $name, - )); - -Variables passed to a template can be strings, arrays, or even objects. Twig -abstracts the difference between them and lets you access "attributes" of a -variable with the dot (``.``) notation: - -.. code-block:: jinja - - {# array('name' => 'Fabien') #} - {{ name }} - - {# array('user' => array('name' => 'Fabien')) #} - {{ user.name }} - - {# force array lookup #} - {{ user['name'] }} - - {# array('user' => new User('Fabien')) #} - {{ user.name }} - {{ user.getName }} - - {# force method name lookup #} - {{ user.name() }} - {{ user.getName() }} - - {# pass arguments to a method #} - {{ user.date('Y-m-d') }} - -.. note:: - - It's important to know that the curly braces are not part of the variable - but the print statement. If you access variables inside tags don't put the - braces around. - -Decorating Templates --------------------- - -More often than not, templates in a project share common elements, like the -well-known header and footer. In Symfony2, you think about this problem -differently: a template can be decorated by another one. This works exactly -the same as PHP classes: template inheritance allows you to build a base -"layout" template that contains all the common elements of your site and -defines "blocks" that child templates can override. - -The ``hello.html.twig`` template inherits from ``layout.html.twig``, thanks to -the ``extends`` tag: - -.. code-block:: html+jinja - - {# src/Acme/DemoBundle/Resources/views/Demo/hello.html.twig #} - {% extends "AcmeDemoBundle::layout.html.twig" %} - - {% block title "Hello " ~ name %} - - {% block content %} -

          Hello {{ name }}!

          - {% endblock %} - -The ``AcmeDemoBundle::layout.html.twig`` notation sounds familiar, doesn't it? -It is the same notation used to reference a regular template. The ``::`` part -simply means that the controller element is empty, so the corresponding file -is directly stored under the ``Resources/views/`` directory. - -Now, let's have a look at a simplified ``layout.html.twig``: - -.. code-block:: jinja - - {# src/Acme/DemoBundle/Resources/views/layout.html.twig #} -
          - {% block content %} - {% endblock %} -
          - -The ``{% block %}`` tags define blocks that child templates can fill in. All -the block tag does is to tell the template engine that a child template may -override those portions of the template. - -In this example, the ``hello.html.twig`` template overrides the ``content`` -block, meaning that the "Hello Fabien" text is rendered inside the ``div.symfony-content`` -element. - -Using Tags, Filters, and Functions ----------------------------------- - -One of the best feature of Twig is its extensibility via tags, filters, and -functions. Symfony2 comes bundled with many of these built-in to ease the -work of the template designer. - -Including other Templates -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The best way to share a snippet of code between several distinct templates is -to create a new template that can then be included from other templates. - -Create an ``embedded.html.twig`` template: - -.. code-block:: jinja - - {# src/Acme/DemoBundle/Resources/views/Demo/embedded.html.twig #} - Hello {{ name }} - -And change the ``index.html.twig`` template to include it: - -.. code-block:: jinja - - {# src/Acme/DemoBundle/Resources/views/Demo/hello.html.twig #} - {% extends "AcmeDemoBundle::layout.html.twig" %} - - {# override the body block from embedded.html.twig #} - {% block content %} - {{ include("AcmeDemoBundle:Demo:embedded.html.twig") }} - {% endblock %} - -Embedding other Controllers -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -And what if you want to embed the result of another controller in a template? -That's very useful when working with Ajax, or when the embedded template needs -some variable not available in the main template. - -Suppose you've created a ``fancyAction`` controller method, and you want to -"render" it inside the ``index`` template, which means including the result -(e.g. ``HTML``) of the controller. To do this, use the ``render`` function: - -.. code-block:: jinja - - {# src/Acme/DemoBundle/Resources/views/Demo/index.html.twig #} - {{ render(controller("AcmeDemoBundle:Demo:fancy", {'name': name, 'color': 'green'})) }} - -Here, the ``AcmeDemoBundle:Demo:fancy`` string refers to the ``fancy`` action -of the ``Demo`` controller. The arguments (``name`` and ``color``) act like -simulated request variables (as if the ``fancyAction`` were handling a whole -new request) and are made available to the controller:: - - // src/Acme/DemoBundle/Controller/DemoController.php - - class DemoController extends Controller - { - public function fancyAction($name, $color) - { - // create some object, based on the $color variable - $object = ...; - - return $this->render('AcmeDemoBundle:Demo:fancy.html.twig', array( - 'name' => $name, - 'object' => $object, - )); - } - - // ... - } - -Creating Links between Pages -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Speaking of web applications, creating links between pages is a must. Instead -of hardcoding URLs in templates, the ``path`` function knows how to generate -URLs based on the routing configuration. That way, all your URLs can be easily -updated by just changing the configuration: - -.. code-block:: html+jinja - - Greet Thomas! - -The ``path`` function takes the route name and an array of parameters as -arguments. The route name is the main key under which routes are referenced -and the parameters are the values of the placeholders defined in the route -pattern:: - - // src/Acme/DemoBundle/Controller/DemoController.php - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; - - // ... - - /** - * @Route("/hello/{name}", name="_demo_hello") - * @Template() - */ - public function helloAction($name) - { - return array('name' => $name); - } - -.. tip:: - - The ``url`` function generates *absolute* URLs: ``{{ url('_demo_hello', { - 'name': 'Thomas'}) }}``. - -Including Assets: images, JavaScripts, and stylesheets -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -What would the Internet be without images, JavaScripts, and stylesheets? -Symfony2 provides the ``asset`` function to deal with them easily: - -.. code-block:: jinja - - - - - -The ``asset`` function's main purpose is to make your application more portable. -Thanks to this function, you can move the application root directory anywhere -under your web root directory without changing anything in your template's -code. - -Escaping Variables ------------------- - -Twig is configured to automatically escape all output by default. Read Twig -`documentation`_ to learn more about output escaping and the Escaper -extension. - -Final Thoughts --------------- - -Twig is simple yet powerful. Thanks to layouts, blocks, templates and action -inclusions, it is very easy to organize your templates in a logical and -extensible way. However, if you're not comfortable with Twig, you can always -use PHP templates inside Symfony without any issues. - -You have only been working with Symfony2 for about 20 minutes, but you can -already do pretty amazing stuff with it. That's the power of Symfony2. Learning -the basics is easy, and you will soon learn that this simplicity is hidden -under a very flexible architecture. - -But I'm getting ahead of myself. First, you need to learn more about the controller -and that's exactly the topic of the :doc:`next part of this tutorial`. -Ready for another 10 minutes with Symfony2? - -.. _Twig: http://twig.sensiolabs.org/ -.. _documentation: http://twig.sensiolabs.org/documentation diff --git a/rate_limiter.rst b/rate_limiter.rst new file mode 100644 index 00000000000..3a517c37bd4 --- /dev/null +++ b/rate_limiter.rst @@ -0,0 +1,673 @@ +Rate Limiter +============ + +A "rate limiter" controls how frequently some event (e.g. an HTTP request or a +login attempt) is allowed to happen. Rate limiting is commonly used as a +defensive measure to protect services from excessive use (intended or not) and +maintain their availability. It's also useful to control your internal or +outbound processes (e.g. limit the number of simultaneously processed messages). + +Symfony uses these rate limiters in built-in features like :ref:`login throttling `, +which limits how many failed login attempts a user can make in a given period of +time, but you can use them for your own features too. + +.. danger:: + + By definition, the Symfony rate limiters require Symfony to be booted + in a PHP process. This makes them not useful to protect against `DoS attacks`_. + Such protections must consume the least resources possible. Consider + using `Apache mod_ratelimit`_, `NGINX rate limiting`_, + `Caddy HTTP rate limit module`_ (also supported by FrankenPHP) + or proxies (like AWS or Cloudflare) to prevent your server from being overwhelmed. + +.. _rate-limiter-policies: + +Rate Limiting Policies +---------------------- + +Symfony's rate limiter implements some of the most common policies to enforce +rate limits: **fixed window**, **sliding window**, **token bucket**. + +Fixed Window Rate Limiter +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is the simplest technique and it's based on setting a limit for a given +interval of time (e.g. 5,000 requests per hour or 3 login attempts every 15 +minutes). + +In the diagram below, the limit is set to "5 tokens per hour". Each window +starts at the first hit (i.e. 10:15, 11:30 and 12:30). As soon as there are +5 hits (the blue squares) in a window, all others will be rejected (red +squares). + +.. raw:: html + + + +Its main drawback is that resource usage is not evenly distributed in time and +it can overload the server at the window edges. In this example, +there were 6 accepted requests between 11:00 and 12:00. + +This is more significant with bigger limits. For instance, with 5,000 requests +per hour, a user could make 4,999 requests in the last minute of some +hour and another 5,000 requests during the first minute of the next hour, +making 9,999 requests in total in two minutes and possibly overloading the +server. These periods of excessive usage are called "bursts". + +Sliding Window Rate Limiter +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The sliding window algorithm is an alternative to the fixed window algorithm +designed to reduce bursts. This is the same example as above, but then +using a 1 hour window that slides over the timeline: + +.. raw:: html + + + +As you can see, this removes the edges of the window and would prevent the +6th request at 11:45. + +To achieve this, the rate limit is approximated based on the current window and +the previous window. + +For example: the limit is 5,000 requests per hour; a user made 4,000 requests +the previous hour and 500 requests this hour. 15 minutes in to the current hour +(25% of the window) the hit count would be calculated as: 75% * 4,000 + 500 = 3,500. +At this point in time the user can only do 1,500 more requests. + +The math shows that the closer the last window is, the more the hit count +of the last window will affect the current limit. This will make sure that a user can +do 5,000 requests per hour but only if they are evenly spread out. + +Token Bucket Rate Limiter +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This technique implements the `token bucket algorithm`_, which defines +continuously updating the budget of resource usage. It roughly works like this: + +#. A bucket is created with an initial set of tokens; +#. A new token is added to the bucket with a predefined frequency (e.g. every second); +#. Allowing an event consumes one or more tokens; +#. If the bucket still contains tokens, the event is allowed; otherwise, it's denied; +#. If the bucket is at full capacity, new tokens are discarded. + +The below diagram shows a token bucket of size 4 that is filled with a rate +of 1 token per 15 minutes: + +.. raw:: html + + + +This algorithm handles more complex back-off burst management. +For instance, it can allow a user to try a password 5 times and then only +allow 1 every 15 minutes (unless the user waits 75 minutes and they will be +allowed 5 tries again). + +Installation +------------ + +Before using a rate limiter for the first time, run the following command to +install the associated Symfony Component in your application: + +.. code-block:: terminal + + $ composer require symfony/rate-limiter + +Configuration +------------- + +The following example creates two different rate limiters for an API service, to +enforce different levels of service (free or paid): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/rate_limiter.yaml + framework: + rate_limiter: + anonymous_api: + # use 'sliding_window' if you prefer that policy + policy: 'fixed_window' + limit: 100 + interval: '60 minutes' + authenticated_api: + policy: 'token_bucket' + limit: 5000 + rate: { interval: '15 minutes', amount: 500 } + + .. code-block:: xml + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/rate_limiter.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->rateLimiter() + ->limiter('anonymous_api') + // use 'sliding_window' if you prefer that policy + ->policy('fixed_window') + ->limit(100) + ->interval('60 minutes') + ; + + $framework->rateLimiter() + ->limiter('authenticated_api') + ->policy('token_bucket') + ->limit(5000) + ->rate() + ->interval('15 minutes') + ->amount(500) + ; + }; + +.. note:: + + The value of the ``interval`` option must be a number followed by any of the + units accepted by the `PHP date relative formats`_ (e.g. ``3 seconds``, + ``10 hours``, ``1 day``, etc.) + +In the ``anonymous_api`` limiter, after making the first HTTP request, you can +make up to 100 requests in the next 60 minutes. After that time, the counter +resets and you have another 100 requests for the following 60 minutes. + +In the ``authenticated_api`` limiter, after making the first HTTP request you +are allowed to make up to 5,000 HTTP requests in total, and this number grows +at a rate of another 500 requests every 15 minutes. If you don't make that +number of requests, the unused ones don't accumulate (the ``limit`` option +prevents that number from being higher than 5,000). + +.. tip:: + + All rate-limiters are tagged with the ``rate_limiter`` tag, so you can + find them with a :doc:`tagged iterator ` or + :doc:`locator `. + + .. versionadded:: 7.1 + + The automatic addition of the ``rate_limiter`` tag was introduced + in Symfony 7.1. + +Rate Limiting in Action +----------------------- + +.. versionadded:: 7.3 + + :class:`Symfony\\Component\\RateLimiter\\RateLimiterFactoryInterface` was + added and should now be used for autowiring instead of + :class:`Symfony\\Component\\RateLimiter\\RateLimiterFactory`. + +After having installed and configured the rate limiter, inject it in any service +or controller and call the ``consume()`` method to try to consume a given number +of tokens. For example, this controller uses the previous rate limiter to control +the number of requests to the API:: + + // src/Controller/ApiController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; + use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; + + class ApiController extends AbstractController + { + // if you're using service autowiring, the variable name must be: + // "rate limiter name" (in camelCase) + "Limiter" suffix + public function index(Request $request, RateLimiterFactoryInterface $anonymousApiLimiter): Response + { + // create a limiter based on a unique identifier of the client + // (e.g. the client's IP address, a username/email, an API key, etc.) + $limiter = $anonymousApiLimiter->create($request->getClientIp()); + + // the argument of consume() is the number of tokens to consume + // and returns an object of type Limit + if (false === $limiter->consume(1)->isAccepted()) { + throw new TooManyRequestsHttpException(); + } + + // you can also use the ensureAccepted() method - which throws a + // RateLimitExceededException if the limit has been reached + // $limiter->consume(1)->ensureAccepted(); + + // to reset the counter + // $limiter->reset(); + + // ... + } + } + +.. note:: + + In a real application, instead of checking the rate limiter in all the API + controller methods, create an :doc:`event listener or subscriber ` + for the :ref:`kernel.request event ` + and check the rate limiter once for all requests. + +Wait until a Token is Available +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Instead of dropping a request or process when the limit has been reached, +you might want to wait until a new token is available. This can be achieved +using the ``reserve()`` method:: + + // src/Controller/ApiController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; + + class ApiController extends AbstractController + { + public function registerUser(Request $request, RateLimiterFactoryInterface $authenticatedApiLimiter): Response + { + $apiKey = $request->headers->get('apikey'); + $limiter = $authenticatedApiLimiter->create($apiKey); + + // this blocks the application until the given number of tokens can be consumed + $limiter->reserve(1)->wait(); + + // optional, pass a maximum wait time (in seconds), a MaxWaitDurationExceededException + // is thrown if the process has to wait longer. E.g. to wait at most 20 seconds: + //$limiter->reserve(1, 20)->wait(); + + // ... + } + + // ... + } + +The ``reserve()`` method is able to reserve a token in the future. Only use +this method if you're planning to wait, otherwise you will block other +processes by reserving unused tokens. + +.. note:: + + Not all strategies allow reserving tokens in the future. These + strategies may throw a ``ReserveNotSupportedException`` when calling + ``reserve()``. + + In these cases, you can use ``consume()`` together with ``wait()``, but + there is no guarantee that a token is available after the wait:: + + // ... + do { + $limit = $limiter->consume(1); + $limit->wait(); + } while (!$limit->isAccepted()); + +Exposing the Rate Limiter Status +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using a rate limiter in APIs, it's common to include some standard HTTP +headers in the response to expose the limit status (e.g. remaining tokens, when +new tokens will be available, etc.) + +Use the :class:`Symfony\\Component\\RateLimiter\\RateLimit` object returned by +the ``consume()`` method (also available via the ``getRateLimit()`` method of +the :class:`Symfony\\Component\\RateLimiter\\Reservation` object returned by the +``reserve()`` method) to get the value of those HTTP headers:: + + // src/Controller/ApiController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; + + class ApiController extends AbstractController + { + public function index(Request $request, RateLimiterFactoryInterface $anonymousApiLimiter): Response + { + $limiter = $anonymousApiLimiter->create($request->getClientIp()); + $limit = $limiter->consume(); + $headers = [ + 'X-RateLimit-Remaining' => $limit->getRemainingTokens(), + 'X-RateLimit-Retry-After' => $limit->getRetryAfter()->getTimestamp() - time(), + 'X-RateLimit-Limit' => $limit->getLimit(), + ]; + + if (false === $limit->isAccepted()) { + return new Response(null, Response::HTTP_TOO_MANY_REQUESTS, $headers); + } + + // ... + + $response = new Response('...'); + $response->headers->add($headers); + + return $response; + } + } + +.. _rate-limiter-storage: + +Storing Rate Limiter State +-------------------------- + +All rate limiter policies require to store their state (e.g. how many hits were +already made in the current time window). By default, all limiters use the +``cache.rate_limiter`` cache pool created with the :doc:`Cache component `. +This means that every time you clear the cache, the rate limiter will be reset. + +You can use the ``cache_pool`` option to override the cache used by a specific limiter +(or even :ref:`create a new cache pool ` for it): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/rate_limiter.yaml + framework: + rate_limiter: + anonymous_api: + # ... + + # use the "cache.anonymous_rate_limiter" cache pool + cache_pool: 'cache.anonymous_rate_limiter' + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/rate_limiter.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->rateLimiter() + ->limiter('anonymous_api') + // ... + + // use the "cache.anonymous_rate_limiter" cache pool + ->cachePool('cache.anonymous_rate_limiter') + ; + }; + +.. note:: + + Instead of using the Cache component, you can also implement a custom + storage. Create a PHP class that implements the + :class:`Symfony\\Component\\RateLimiter\\Storage\\StorageInterface` and + use the ``storage_service`` setting of each limiter to the service ID + of this class. + +Using Locks to Prevent Race Conditions +-------------------------------------- + +`Race conditions`_ can happen when the same rate limiter is used by multiple +simultaneous requests (e.g. three servers of a company hitting your API at the +same time). Rate limiters use :doc:`locks ` to protect their operations +against these race conditions. + +By default, if the :doc:`lock ` component is installed, Symfony uses the +global lock configured by ``framework.lock``, but you can use a specific +:ref:`named lock ` via the ``lock_factory`` option (or none +at all): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/rate_limiter.yaml + framework: + rate_limiter: + anonymous_api: + # ... + + # use the "lock.rate_limiter.factory" for this limiter + lock_factory: 'lock.rate_limiter.factory' + + # or don't use any lock mechanism + lock_factory: null + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/rate_limiter.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->rateLimiter() + ->limiter('anonymous_api') + // ... + + // use the "lock.rate_limiter.factory" for this limiter + ->lockFactory('lock.rate_limiter.factory') + + // or don't use any lock mechanism + ->lockFactory(null) + ; + }; + +.. versionadded:: 7.3 + + Before Symfony 7.3, configuring a rate limiter and using the default configured + lock factory (``lock.factory``) failed if the Symfony Lock component was not + installed in the application. + +Compound Rate Limiter +--------------------- + +.. versionadded:: 7.3 + + Support for configuring compound rate limiters was introduced in Symfony 7.3. + +You can configure multiple rate limiters to work together: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/rate_limiter.yaml + framework: + rate_limiter: + two_per_minute: + policy: 'fixed_window' + limit: 2 + interval: '1 minute' + five_per_hour: + policy: 'fixed_window' + limit: 5 + interval: '1 hour' + contact_form: + policy: 'compound' + limiters: [two_per_minute, five_per_hour] + + .. code-block:: xml + + + + + + + + + + + + + two_per_minute + five_per_hour + + + + + + .. code-block:: php + + // config/packages/rate_limiter.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->rateLimiter() + ->limiter('two_per_minute') + ->policy('fixed_window') + ->limit(2) + ->interval('1 minute') + ; + + $framework->rateLimiter() + ->limiter('two_per_minute') + ->policy('fixed_window') + ->limit(5) + ->interval('1 hour') + ; + + $framework->rateLimiter() + ->limiter('contact_form') + ->policy('compound') + ->limiters(['two_per_minute', 'five_per_hour']) + ; + }; + +Then, inject and use as normal:: + + // src/Controller/ContactController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\RateLimiter\RateLimiterFactory; + + class ContactController extends AbstractController + { + public function registerUser(Request $request, RateLimiterFactoryInterface $contactFormLimiter): Response + { + $limiter = $contactFormLimiter->create($request->getClientIp()); + + if (false === $limiter->consume(1)->isAccepted()) { + // either of the two limiters has been reached + } + + // ... + } + + // ... + } + +.. _`DoS attacks`: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html +.. _`Apache mod_ratelimit`: https://httpd.apache.org/docs/current/mod/mod_ratelimit.html +.. _`NGINX rate limiting`: https://www.nginx.com/blog/rate-limiting-nginx/ +.. _`Caddy HTTP rate limit module`: https://github.com/mholt/caddy-ratelimit +.. _`token bucket algorithm`: https://en.wikipedia.org/wiki/Token_bucket +.. _`PHP date relative formats`: https://www.php.net/manual/en/datetime.formats.php#datetime.formats.relative +.. _`Race conditions`: https://en.wikipedia.org/wiki/Race_condition diff --git a/redirection_map b/redirection_map deleted file mode 100644 index 925a47b6641..00000000000 --- a/redirection_map +++ /dev/null @@ -1,23 +0,0 @@ -/cookbook/doctrine/migrations /bundles/DoctrineFixturesBundle/index -/cookbook/doctrine/doctrine_fixtures /bundles/DoctrineFixturesBundle/index -/cookbook/doctrine/mongodb /bundles/DoctrineMongoDBBundle/index -/cookbook/form/dynamic_form_generation /cookbook/form/dynamic_form_modification -/cookbook/form/simple_signup_form_with_mongodb /bundles/DoctrineMongoDBBundle/form -/cookbook/email /cookbook/email/email -/cookbook/gmail /cookbook/email/gmail -/cookbook/console /components/console -/cookbook/tools/autoloader /components/class_loader -/cookbook/tools/finder /components/finder -/cookbook/service_container/parentservices /components/dependency_injection/parentservices -/cookbook/service_container/factories /components/dependency_injection/factories -/cookbook/service_container/tags /components/dependency_injection/tags -/reference/configuration/mongodb /bundles/DoctrineMongoDBBundle/config -/reference/YAML /components/yaml -/components/dependency_injection /components/dependency_injection/introduction -/components/event_dispatcher /components/event_dispatcher/introduction -/components/http_foundation /components/http_foundation/introduction -/components/console /components/console/introduction -/components/routing /components/routing/introduction -/cookbook/console/generating_urls /cookbook/console/sending_emails -/components/yaml /components/yaml/introduction -/cookbook/form/use_virtuals_forms /cookbook/form/inherit_data_option diff --git a/reference/attributes.rst b/reference/attributes.rst new file mode 100644 index 00000000000..a8399dafe28 --- /dev/null +++ b/reference/attributes.rst @@ -0,0 +1,157 @@ +Symfony Attributes Overview +=========================== + +Attributes are the successor of annotations since PHP 8. Attributes are native +to the language and Symfony takes full advantage of them across the framework +and its different components. + +Doctrine Bridge +~~~~~~~~~~~~~~~ + +* :doc:`UniqueEntity ` +* :ref:`MapEntity ` + +Command +~~~~~~~ + +* :ref:`AsCommand ` + +Contracts +~~~~~~~~~ + +* :ref:`Required ` +* :ref:`SubscribedService ` + +Dependency Injection +~~~~~~~~~~~~~~~~~~~~ + +* :ref:`AsAlias ` +* :doc:`AsDecorator ` +* :ref:`AsTaggedItem ` +* :ref:`Autoconfigure ` +* :ref:`AutoconfigureTag ` +* :ref:`Autowire ` +* :ref:`AutowireCallable ` +* :doc:`AutowireDecorated ` +* :ref:`AutowireIterator ` +* :ref:`AutowireLocator ` +* :ref:`AutowireMethodOf ` +* :ref:`AutowireServiceClosure ` +* :ref:`Exclude ` +* :ref:`Lazy ` +* :ref:`TaggedIterator ` +* :ref:`TaggedLocator ` +* :ref:`Target ` +* :ref:`When ` +* :ref:`WhenNot ` + +.. deprecated:: 7.1 + + The :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedIterator` + and :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedLocator` + attributes were deprecated in Symfony 7.1. + +EventDispatcher +~~~~~~~~~~~~~~~ + +* :ref:`AsEventListener ` + +FrameworkBundle +~~~~~~~~~~~~~~~ + +* :ref:`AsRoutingConditionService ` + +HttpKernel +~~~~~~~~~~ + +* :doc:`AsController ` +* :ref:`AsTargetedValueResolver ` +* :ref:`Cache ` +* :ref:`MapDateTime ` +* :ref:`MapQueryParameter ` +* :ref:`MapQueryString ` +* :ref:`MapRequestPayload ` +* :ref:`MapUploadedFile ` +* :ref:`ValueResolver ` +* :ref:`WithHttpStatus ` +* :ref:`WithLogLevel ` + +Messenger +~~~~~~~~~ + +* :ref:`AsMessage ` +* :ref:`AsMessageHandler ` + +RemoteEvent +~~~~~~~~~~~ + +* :ref:`AsRemoteEventConsumer ` + +Routing +~~~~~~~ + +* :doc:`Route ` + +Scheduler +~~~~~~~~~ + +* :ref:`AsCronTask ` +* :ref:`AsPeriodicTask ` +* :ref:`AsSchedule ` + +Security +~~~~~~~~ + +* :ref:`CurrentUser ` +* :ref:`IsCsrfTokenValid ` +* :ref:`IsGranted ` + +.. _reference-attributes-serializer: + +Serializer +~~~~~~~~~~ + +* :ref:`Context ` +* :ref:`DiscriminatorMap ` +* :ref:`Groups ` +* :ref:`Ignore ` +* :ref:`MaxDepth ` +* :ref:`SerializedName ` +* :ref:`SerializedPath ` + +Twig +~~~~ + +* :ref:`Template ` + +Symfony UX +~~~~~~~~~~ + +* `AsEntityAutocompleteField`_ +* `AsLiveComponent`_ +* `AsTwigComponent`_ +* `Broadcast`_ + +Validator +~~~~~~~~~ + +Each validation constraint comes with a PHP attribute. See +:doc:`/reference/constraints` for a full list of validation constraints. + +* :doc:`HasNamedArguments ` + +Workflow +~~~~~~~~ + +* :ref:`AsAnnounceListener ` +* :ref:`AsCompletedListener ` +* :ref:`AsEnterListener ` +* :ref:`AsEnteredListener ` +* :ref:`AsGuardListener ` +* :ref:`AsLeaveListener ` +* :ref:`AsTransitionListener ` + +.. _`AsEntityAutocompleteField`: https://symfony.com/bundles/ux-autocomplete/current/index.html#usage-in-a-form-with-ajax +.. _`AsLiveComponent`: https://symfony.com/bundles/ux-live-component/current/index.html +.. _`AsTwigComponent`: https://symfony.com/bundles/ux-twig-component/current/index.html +.. _`Broadcast`: https://symfony.com/bundles/ux-turbo/current/index.html#broadcast-conventions-and-configuration diff --git a/reference/configuration/assetic.rst b/reference/configuration/assetic.rst deleted file mode 100644 index e58e5a7686b..00000000000 --- a/reference/configuration/assetic.rst +++ /dev/null @@ -1,103 +0,0 @@ -.. index:: - pair: Assetic; Configuration reference - -AsseticBundle Configuration Reference -===================================== - -Full Default Configuration -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. configuration-block:: - - .. code-block:: yaml - - assetic: - debug: "%kernel.debug%" - use_controller: - enabled: "%kernel.debug%" - profiler: false - read_from: "%kernel.root_dir%/../web" - write_to: "%assetic.read_from%" - java: /usr/bin/java - node: /usr/bin/node - ruby: /usr/bin/ruby - sass: /usr/bin/sass - # An key-value pair of any number of named elements - variables: - some_name: [] - bundles: - - # Defaults (all currently registered bundles): - - FrameworkBundle - - SecurityBundle - - TwigBundle - - MonologBundle - - SwiftmailerBundle - - DoctrineBundle - - AsseticBundle - - ... - assets: - # An array of named assets (e.g. some_asset, some_other_asset) - some_asset: - inputs: [] - filters: [] - options: - # A key-value array of options and values - some_option_name: [] - filters: - - # An array of named filters (e.g. some_filter, some_other_filter) - some_filter: [] - twig: - functions: - # An array of named functions (e.g. some_function, some_other_function) - some_function: [] - - .. code-block:: xml - - - - FrameworkBundle - SecurityBundle - TwigBundle - MonologBundle - SwiftmailerBundle - DoctrineBundle - AsseticBundle - ... - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reference/configuration/debug.rst b/reference/configuration/debug.rst new file mode 100644 index 00000000000..6ca05b49bd7 --- /dev/null +++ b/reference/configuration/debug.rst @@ -0,0 +1,100 @@ +Debug Configuration Reference (DebugBundle) +=========================================== + +The DebugBundle integrates the :doc:`VarDumper component ` +in Symfony applications. All these options are configured under the ``debug`` +key in your application configuration. + +.. code-block:: terminal + + # displays the default config values defined by Symfony + $ php bin/console config:dump-reference debug + + # displays the actual config values used by your application + $ php bin/console debug:config debug + + # displays the config values used by your application and replaces the + # environment variables with their actual values + $ php bin/console debug:config --resolve-env debug + +.. note:: + + When using XML, you must use the ``http://symfony.com/schema/dic/debug`` + namespace and the related XSD schema is available at: + ``https://symfony.com/schema/dic/debug/debug-1.0.xsd`` + +.. _configuration-debug-dump_destination: + +dump_destination +~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +Configures the output destination of the dumps. + +By default, dumps are shown in the WebDebugToolbar when returning HTML. +Since this is not always possible (e.g. when working on a JSON API), +you can have an alternate output destination for dumps. +Typically, you would set this to ``php://stderr``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/debug.yaml + debug: + dump_destination: php://stderr + + .. code-block:: xml + + + + + + + + + .. code-block:: php + + // config/packages/debug.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return static function (ContainerConfigurator $container): void { + $container->extension('debug', [ + 'dump_destination' => 'php://stderr', + ]); + }; + + +Configure it to ``"tcp://%env(VAR_DUMPER_SERVER)%"`` in order to use the :ref:`ServerDumper feature `. + +max_items +~~~~~~~~~ + +**type**: ``integer`` **default**: ``2500`` + +This is the maximum number of items to dump. Setting this option to ``-1`` +disables the limit. + +max_string_length +~~~~~~~~~~~~~~~~~ + +**type**: ``integer`` **default**: ``-1`` + +This option configures the maximum string length before truncating the +string. The default value (``-1``) means that strings are never truncated. + +min_depth +~~~~~~~~~ + +**type**: ``integer`` **default**: ``1`` + +Configures the minimum tree depth until which all items are guaranteed to +be cloned. After this depth is reached, only ``max_items`` items will be +cloned. The default value is ``1``, which is consistent with older Symfony +versions. diff --git a/reference/configuration/doctrine.rst b/reference/configuration/doctrine.rst index c2115238e27..6e5bd12aaea 100644 --- a/reference/configuration/doctrine.rst +++ b/reference/configuration/doctrine.rst @@ -1,9 +1,34 @@ -.. index:: - single: Doctrine; ORM configuration reference - single: Configuration reference; Doctrine ORM +Doctrine Configuration Reference (DoctrineBundle) +================================================= -Doctrine Configuration Reference -================================ +The DoctrineBundle integrates both the :doc:`DBAL ` and +:doc:`ORM ` Doctrine projects in Symfony applications. All these +options are configured under the ``doctrine`` key in your application +configuration. + +.. code-block:: terminal + + # displays the default config values defined by Symfony + $ php bin/console config:dump-reference doctrine + + # displays the actual config values used by your application + $ php bin/console debug:config doctrine + +.. note:: + + When using XML, you must use the ``http://symfony.com/schema/dic/doctrine`` + namespace and the related XSD schema is available at: + ``https://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd`` + +.. _`reference-dbal-configuration`: + +Doctrine DBAL Configuration +--------------------------- + +DoctrineBundle supports all parameters that default Doctrine drivers +accept, converted to the XML or YAML naming standards that Symfony +enforces. See the Doctrine `DBAL documentation`_ for more information. +The following block shows all possible configuration keys: .. configuration-block:: @@ -11,225 +36,141 @@ Doctrine Configuration Reference doctrine: dbal: - default_connection: default + dbname: database + host: localhost + port: 1234 + user: user + password: secret + driver: pdo_mysql + # if the url option is specified, it will override the above config + url: mysql://db_user:db_password@127.0.0.1:3306/db_name + # the DBAL driverClass option + driver_class: App\DBAL\MyDatabaseDriver + # the DBAL driverOptions option + options: + foo: bar + path: '%kernel.project_dir%/var/data/data.sqlite' + memory: true + unix_socket: /tmp/mysql.sock + # the DBAL wrapperClass option + wrapper_class: App\DBAL\MyConnectionWrapper + charset: utf8mb4 + logging: '%kernel.debug%' + platform_service: App\DBAL\MyDatabasePlatformService + server_version: '8.0.37' + mapping_types: + enum: string types: - # A collection of custom types - # Example - some_custom_type: - class: Acme\HelloBundle\MyCustomType - commented: true - - connections: - default: - dbname: database - - # A collection of different named connections (e.g. default, conn2, etc) - default: - dbname: ~ - host: localhost - port: ~ - user: root - password: ~ - charset: ~ - path: ~ - memory: ~ - - # The unix socket to use for MySQL - unix_socket: ~ - - # True to use as persistent connection for the ibm_db2 driver - persistent: ~ - - # The protocol to use for the ibm_db2 driver (default to TCPIP if omitted) - protocol: ~ - - # True to use dbname as service name instead of SID for Oracle - service: ~ - - # The session mode to use for the oci8 driver - sessionMode: ~ - - # True to use a pooled server with the oci8 driver - pooled: ~ - - # Configuring MultipleActiveResultSets for the pdo_sqlsrv driver - MultipleActiveResultSets: ~ - driver: pdo_mysql - platform_service: ~ - logging: %kernel.debug% - profiling: %kernel.debug% - driver_class: ~ - wrapper_class: ~ - options: - # an array of options - key: [] - mapping_types: - # an array of mapping types - name: [] - slaves: - - # a collection of named slave connections (e.g. slave1, slave2) - slave1: - dbname: ~ - host: localhost - port: ~ - user: root - password: ~ - charset: ~ - path: ~ - memory: ~ - - # The unix socket to use for MySQL - unix_socket: ~ - - # True to use as persistent connection for the ibm_db2 driver - persistent: ~ - - # The protocol to use for the ibm_db2 driver (default to TCPIP if omitted) - protocol: ~ - - # True to use dbname as service name instead of SID for Oracle - service: ~ - - # The session mode to use for the oci8 driver - sessionMode: ~ - - # True to use a pooled server with the oci8 driver - pooled: ~ - - # Configuring MultipleActiveResultSets for the pdo_sqlsrv driver - MultipleActiveResultSets: ~ - - orm: - default_entity_manager: ~ - auto_generate_proxy_classes: false - proxy_dir: %kernel.cache_dir%/doctrine/orm/Proxies - proxy_namespace: Proxies - # search for the "ResolveTargetEntityListener" class for a cookbook about this - resolve_target_entities: [] - entity_managers: - # A collection of different named entity managers (e.g. some_em, another_em) - some_em: - query_cache_driver: - type: array # Required - host: ~ - port: ~ - instance_class: ~ - class: ~ - metadata_cache_driver: - type: array # Required - host: ~ - port: ~ - instance_class: ~ - class: ~ - result_cache_driver: - type: array # Required - host: ~ - port: ~ - instance_class: ~ - class: ~ - connection: ~ - class_metadata_factory_name: Doctrine\ORM\Mapping\ClassMetadataFactory - default_repository_class: Doctrine\ORM\EntityRepository - auto_mapping: false - hydrators: - - # An array of hydrator names - hydrator_name: [] - mappings: - # An array of mappings, which may be a bundle name or something else - mapping_name: - mapping: true - type: ~ - dir: ~ - alias: ~ - prefix: ~ - is_bundle: ~ - dql: - # a collection of string functions - string_functions: - # example - # test_string: Acme\HelloBundle\DQL\StringFunction - - # a collection of numeric functions - numeric_functions: - # example - # test_numeric: Acme\HelloBundle\DQL\NumericFunction - - # a collection of datetime functions - datetime_functions: - # example - # test_datetime: Acme\HelloBundle\DQL\DatetimeFunction - - # Register SQL Filters in the entity manager - filters: - # An array of filters - some_filter: - class: ~ # Required - enabled: false + custom: App\DBAL\MyCustomType .. code-block:: xml + + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/doctrine + https://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"> - - - bar - string - - - Acme\HelloBundle\MyCustomType + + + bar + string + App\DBAL\MyCustomType - - - - - - - Acme\HelloBundle\DQL\NumericFunction - - - - -Configuration Overview ----------------------- +.. note:: -This following configuration example shows all the configuration defaults that -the ORM resolves to: + The ``server_version`` option was added in Doctrine DBAL 2.5, which + is used by DoctrineBundle 1.3. The value of this option should match + your database server version (use ``postgres -V`` or ``psql -V`` command + to find your PostgreSQL version and ``mysql -V`` to get your MySQL + version). + + If you are running a MariaDB database, you must prefix the ``server_version`` + value with ``mariadb-`` (e.g. ``server_version: mariadb-10.4.14``). This will + change in Doctrine DBAL 4.x, where you must define the version as output by + the server (e.g. ``10.4.14-MariaDB``). + + Always wrap the server version number with quotes to parse it as a string + instead of a float number. Otherwise, the floating-point representation + issues can make your version be considered a different number (e.g. ``5.7`` + will be rounded as ``5.6999999999999996447286321199499070644378662109375``). + + If you don't define this option and you haven't created your database + yet, you may get ``PDOException`` errors because Doctrine will try to + guess the database server version automatically and none is available. + +If you want to configure multiple connections in YAML, put them under the +``connections`` key and give them a unique name: + +.. code-block:: yaml + + doctrine: + dbal: + default_connection: default + connections: + default: + dbname: Symfony + user: root + password: null + host: localhost + server_version: '8.0.37' + customer: + dbname: customer + user: root + password: null + host: localhost + server_version: '8.2.0' + +The ``database_connection`` service always refers to the *default* connection, +which is the first one defined or the one configured via the +``default_connection`` parameter. + +Each connection is also accessible via the ``doctrine.dbal.[name]_connection`` +service where ``[name]`` is the name of the connection. In a :doc:`controller ` +you can access it using the ``getConnection()`` method and the name of the connection:: + + // src/Controller/SomeController.php + use Doctrine\Persistence\ManagerRegistry; + + class SomeController + { + public function someMethod(ManagerRegistry $doctrine): void + { + $connection = $doctrine->getConnection('customer'); + $result = $connection->fetchAllAssociative('SELECT name FROM customer'); + + // ... + } + } + +Doctrine ORM Configuration +-------------------------- + +This following configuration example shows all the configuration defaults +that the ORM resolves to: .. code-block:: yaml @@ -239,84 +180,298 @@ the ORM resolves to: # the standard distribution overrides this to be true in debug, false otherwise auto_generate_proxy_classes: false proxy_namespace: Proxies - proxy_dir: "%kernel.cache_dir%/doctrine/orm/Proxies" + proxy_dir: '%kernel.cache_dir%/doctrine/orm/Proxies' default_entity_manager: default metadata_cache_driver: array query_cache_driver: array result_cache_driver: array + naming_strategy: doctrine.orm.naming_strategy.default There are lots of other configuration options that you can use to overwrite certain classes, but those are for very advanced use-cases only. +Shortened Configuration Syntax +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you are only using one entity manager, all config options available +can be placed directly under ``doctrine.orm`` config level. + +.. code-block:: yaml + + doctrine: + orm: + # ... + query_cache_driver: + # ... + metadata_cache_driver: + # ... + result_cache_driver: + # ... + connection: ~ + class_metadata_factory_name: Doctrine\ORM\Mapping\ClassMetadataFactory + default_repository_class: Doctrine\ORM\EntityRepository + auto_mapping: false + naming_strategy: doctrine.orm.naming_strategy.default + hydrators: + # ... + mappings: + # ... + dql: + # ... + filters: + # ... + +This shortened version is commonly used in other documentation sections. +Keep in mind that you can't use both syntaxes at the same time. + Caching Drivers ~~~~~~~~~~~~~~~ -For the caching drivers you can specify the values "array", "apc", "memcache", "memcached", -"xcache" or "service". - -The following example shows an overview of the caching configurations: +Use any of the existing :doc:`Symfony Cache ` pools or define new pools +to cache each of Doctrine ORM elements (queries, results, etc.): .. code-block:: yaml + # config/packages/prod/doctrine.yaml + framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system + doctrine: orm: - auto_mapping: true - metadata_cache_driver: apc + # ... + metadata_cache_driver: + type: pool + pool: doctrine.system_cache_pool query_cache_driver: - type: service - id: my_doctrine_common_cache_service + type: pool + pool: doctrine.system_cache_pool result_cache_driver: - type: memcache - host: localhost - port: 11211 - instance_class: Memcache + type: pool + pool: doctrine.result_cache_pool + + # in addition to Symfony Cache pools, you can also use the + # 'type: service' option to use any service as the cache + query_cache_driver: + type: service + id: App\ORM\MyCacheService Mapping Configuration ~~~~~~~~~~~~~~~~~~~~~ Explicit definition of all the mapped entities is the only necessary -configuration for the ORM and there are several configuration options that you -can control. The following configuration options exist for a mapping: - -* ``type`` One of ``annotation``, ``xml``, ``yml``, ``php`` or ``staticphp``. - This specifies which type of metadata type your mapping uses. - -* ``dir`` Path to the mapping or entity files (depending on the driver). If - this path is relative it is assumed to be relative to the bundle root. This - only works if the name of your mapping is a bundle name. If you want to use - this option to specify absolute paths you should prefix the path with the - kernel parameters that exist in the DIC (for example %kernel.root_dir%). - -* ``prefix`` A common namespace prefix that all entities of this mapping - share. This prefix should never conflict with prefixes of other defined - mappings otherwise some of your entities cannot be found by Doctrine. This - option defaults to the bundle namespace + ``Entity``, for example for an - application bundle called ``AcmeHelloBundle`` prefix would be - ``Acme\HelloBundle\Entity``. - -* ``alias`` Doctrine offers a way to alias entity namespaces to simpler, - shorter names to be used in DQL queries or for Repository access. When using - a bundle the alias defaults to the bundle name. - -* ``is_bundle`` This option is a derived value from ``dir`` and by default is - set to true if dir is relative proved by a ``file_exists()`` check that - returns false. It is false if the existence check returns true. In this case - an absolute path was specified and the metadata files are most likely in a - directory outside of a bundle. - -.. index:: - single: Configuration; Doctrine DBAL - single: Doctrine; DBAL configuration +configuration for the ORM and there are several configuration options that +you can control. The following configuration options exist for a mapping: -.. _`reference-dbal-configuration`: +``type`` +........ -Doctrine DBAL Configuration ---------------------------- +One of ``attribute`` (for PHP attributes; it's the default value), +``xml``, ``php`` or ``staticphp``. This specifies which +type of metadata type your mapping uses. -DoctrineBundle supports all parameters that default Doctrine drivers -accept, converted to the XML or YAML naming standards that Symfony -enforces. See the Doctrine `DBAL documentation`_ for more information. -The following block shows all possible configuration keys: +.. versionadded:: 3.0 + + The ``yml`` mapping configuration is deprecated and was removed in Doctrine ORM 3.0. + +See `Doctrine Metadata Drivers`_ for more information about this option. + +``dir`` +....... + +Absolute path to the mapping or entity files (depending on the driver). + +``prefix`` +.......... + +A common namespace prefix that all entities of this mapping share. This prefix +should never conflict with prefixes of other defined mappings otherwise some of +your entities cannot be found by Doctrine. + +``alias`` +......... + +Doctrine offers a way to alias entity namespaces to simpler, shorter names +to be used in DQL queries or for Repository access. + +``is_bundle`` +............. + +This option is ``false`` by default and it's considered a legacy option. It was +only useful in previous Symfony versions, when it was recommended to use bundles +to organize the application code. + +.. _doctrine_auto-mapping: + +Custom Mapping Entities in a Bundle +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Doctrine's ``auto_mapping`` feature loads attribute configuration from +the ``Entity/`` directory of each bundle *and* looks for other formats (e.g. +YAML, XML) in the ``Resources/config/doctrine`` directory. + +If you store metadata somewhere else in your bundle, you can define your +own mappings, where you tell Doctrine exactly *where* to look, along with +some other configurations. + +If you're using the ``auto_mapping`` configuration, you just need to overwrite +the configurations you want. In this case it's important that the key of +the mapping configurations corresponds to the name of the bundle. + +For example, suppose you decide to store your ``XML`` configuration for +``AppBundle`` entities in the ``@AppBundle/SomeResources/config/doctrine`` +directory instead: + +.. configuration-block:: + + .. code-block:: yaml + + doctrine: + # ... + orm: + # ... + auto_mapping: true + mappings: + # ... + AppBundle: + type: xml + dir: SomeResources/config/doctrine + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine): void { + $emDefault = $doctrine->orm()->entityManager('default'); + + $emDefault->autoMapping(true); + $emDefault->mapping('AppBundle') + ->type('xml') + ->dir('SomeResources/config/doctrine') + ; + }; + +Mapping Entities Outside of a Bundle +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For example, the following looks for entity classes in the ``Entity`` +namespace in the ``src/Entity`` directory and gives them an ``App`` alias +(so you can say things like ``App:Post``): + +.. configuration-block:: + + .. code-block:: yaml + + doctrine: + # ... + orm: + # ... + mappings: + # ... + SomeEntityNamespace: + type: attribute + dir: '%kernel.project_dir%/src/Entity' + is_bundle: false + prefix: App\Entity + alias: App + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine): void { + $emDefault = $doctrine->orm()->entityManager('default'); + + $emDefault->autoMapping(true); + $emDefault->mapping('SomeEntityNamespace') + ->type('attribute') + ->dir('%kernel.project_dir%/src/Entity') + ->isBundle(false) + ->prefix('App\Entity') + ->alias('App') + ; + }; + +Detecting a Mapping Configuration Format +........................................ + +If the ``type`` on the bundle configuration isn't set, the DoctrineBundle +will try to detect the correct mapping configuration format for the bundle. + +DoctrineBundle will look for files matching ``*.orm.[FORMAT]`` (e.g. +``Post.orm.yaml``) in the configured ``dir`` of your mapping (if you're mapping +a bundle, then ``dir`` is relative to the bundle's directory). + +The bundle looks for (in this order) XML, YAML and PHP files. +Using the ``auto_mapping`` feature, every bundle can have only one +configuration format. The bundle will stop as soon as it locates one. + +If it wasn't possible to determine a configuration format for a bundle, +the DoctrineBundle will check if there is an ``Entity`` folder in the bundle's +root directory. If the folder exist, Doctrine will fall back to using +attributes. + +Default Value of Dir +.................... + +If ``dir`` is not specified, then its default value depends on which configuration +driver is being used. For drivers that rely on the PHP files (attribute, +``staticphp``) it will be ``[Bundle]/Entity``. For drivers that are using +configuration files (XML, YAML, ...) it will be +``[Bundle]/Resources/config/doctrine``. + +If the ``dir`` configuration is set and the ``is_bundle`` configuration +is ``true``, the DoctrineBundle will prefix the ``dir`` configuration with +the path of the bundle. + +SSL Connection with MySQL +~~~~~~~~~~~~~~~~~~~~~~~~~ + +To securely configure an SSL connection to MySQL in your Symfony application +with Doctrine, you need to specify the SSL certificate options. Here's how to +set up the connection using environment variables for the certificate paths: .. configuration-block:: @@ -324,86 +479,71 @@ The following block shows all possible configuration keys: doctrine: dbal: - dbname: database - host: localhost - port: 1234 - user: user - password: secret - driver: pdo_mysql - # the DBAL driverClass option - driver_class: MyNamespace\MyDriverImpl - # the DBAL driverOptions option + url: '%env(DATABASE_URL)%' + server_version: '8.0.31' + driver: 'pdo_mysql' options: - foo: bar - path: "%kernel.data_dir%/data.sqlite" - memory: true - unix_socket: /tmp/mysql.sock - # the DBAL wrapperClass option - wrapper_class: MyDoctrineDbalConnectionWrapper - charset: UTF8 - logging: "%kernel.debug%" - platform_service: MyOwnDatabasePlatformService - mapping_types: - enum: string - types: - custom: Acme\HelloBundle\MyCustomType - # the DBAL keepSlave option - keep_slave: false + # SSL private key + !php/const 'PDO::MYSQL_ATTR_SSL_KEY': '%env(MYSQL_SSL_KEY)%' + # SSL certificate + !php/const 'PDO::MYSQL_ATTR_SSL_CERT': '%env(MYSQL_SSL_CERT)%' + # SSL CA authority + !php/const 'PDO::MYSQL_ATTR_SSL_CA': '%env(MYSQL_SSL_CA)%' .. code-block:: xml - - - - - - bar - string - Acme\HelloBundle\MyCustomType - - + + -If you want to configure multiple connections in YAML, put them under the -``connections`` key and give them a unique name: + + + + %env(MYSQL_SSL_KEY)% + %env(MYSQL_SSL_CERT)% + %env(MYSQL_SSL_CA)% + + + -.. code-block:: yaml + .. code-block:: php - doctrine: - dbal: - default_connection: default - connections: - default: - dbname: Symfony2 - user: root - password: null - host: localhost - customer: - dbname: customer - user: root - password: null - host: localhost + // config/packages/doctrine.php + use Symfony\Config\DoctrineConfig; -The ``database_connection`` service always refers to the *default* connection, -which is the first one defined or the one configured via the -``default_connection`` parameter. + return static function (DoctrineConfig $doctrine): void { + $doctrine->dbal() + ->connection('default') + ->url(env('DATABASE_URL')->resolve()) + ->serverVersion('8.0.31') + ->driver('pdo_mysql'); + + $doctrine->dbal()->defaultConnection('default'); + + $doctrine->dbal()->option(\PDO::MYSQL_ATTR_SSL_KEY, '%env(MYSQL_SSL_KEY)%'); + $doctrine->dbal()->option(\PDO::MYSQL_SSL_CERT, '%env(MYSQL_ATTR_SSL_CERT)%'); + $doctrine->dbal()->option(\PDO::MYSQL_SSL_CA, '%env(MYSQL_ATTR_SSL_CA)%'); + }; + +Ensure your environment variables are correctly set in the ``.env.local`` or +``.env.local.php`` file as follows: + +.. code-block:: bash + + MYSQL_SSL_KEY=/path/to/your/server-key.pem + MYSQL_SSL_CERT=/path/to/your/server-cert.pem + MYSQL_SSL_CA=/path/to/your/ca-cert.pem + +This configuration secures your MySQL connection with SSL by specifying the paths to the required certificates. -Each connection is also accessible via the ``doctrine.dbal.[name]_connection`` -service where ``[name]`` if the name of the connection. -.. _DBAL documentation: http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html +.. _DBAL documentation: https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/configuration.html +.. _`Doctrine Metadata Drivers`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/metadata-drivers.html diff --git a/reference/configuration/framework.rst b/reference/configuration/framework.rst index 409e02daeb6..56a7dfe54b1 100644 --- a/reference/configuration/framework.rst +++ b/reference/configuration/framework.rst @@ -1,531 +1,3951 @@ -.. index:: - single: Configuration reference; Framework - -FrameworkBundle Configuration ("framework") -=========================================== - -This reference document is a work in progress. It should be accurate, but -all options are not yet fully covered. - -The ``FrameworkBundle`` contains most of the "base" framework functionality -and can be configured under the ``framework`` key in your application configuration. -This includes settings related to sessions, translation, forms, validation, -routing and more. - -Configuration -------------- - -* `secret`_ -* `http_method_override`_ -* `ide`_ -* `test`_ -* `trusted_proxies`_ -* `form`_ - * enabled -* `csrf_protection`_ - * enabled - * field_name -* `session`_ - * `cookie_lifetime`_ - * `cookie_path`_ - * `cookie_domain`_ - * `cookie_secure`_ - * `cookie_httponly`_ - * `gc_divisor`_ - * `gc_probability`_ - * `gc_maxlifetime`_ - * `save_path`_ -* `serializer`_ - * `enabled`_ -* `templating`_ - * `assets_base_urls`_ - * `assets_version`_ - * `assets_version_format`_ +Framework Configuration Reference (FrameworkBundle) +=================================================== -secret -~~~~~~ - -**type**: ``string`` **required** +The FrameworkBundle defines the main framework configuration, from sessions and +translations to forms, validation, routing and more. All these options are +configured under the ``framework`` key in your application configuration. -This is a string that should be unique to your application. In practice, -it's used for generating the CSRF tokens, but it could be used in any other -context where having a unique string is useful. It becomes the service container -parameter named ``kernel.secret``. +.. code-block:: terminal -.. _configuration-framework-http_method_override: + # displays the default config values defined by Symfony + $ php bin/console config:dump-reference framework -http_method_override -~~~~~~~~~~~~~~~~~~~~ + # displays the actual config values used by your application + $ php bin/console debug:config framework -.. versionadded:: 2.3 - The ``http_method_override`` option is new in Symfony 2.3. - -**type**: ``Boolean`` **default**: ``true`` +.. note:: -This determines whether the ``_method`` request parameter is used as the intended -HTTP method on POST requests. If enabled, the -:method:`Request::enableHttpMethodParameterOverride ` -gets called automatically. It becomes the service container parameter named -``kernel.http_method_override``. For more information, see -:doc:`/cookbook/routing/method_parameters`. + When using XML, you must use the ``http://symfony.com/schema/dic/symfony`` + namespace and the related XSD schema is available at: + ``https://symfony.com/schema/dic/symfony/symfony-1.0.xsd`` -ide -~~~ +annotations +~~~~~~~~~~~ -**type**: ``string`` **default**: ``null`` +.. _reference-annotations-cache: -If you're using an IDE like TextMate or Mac Vim, then Symfony can turn all -of the file paths in an exception message into a link, which will open that -file in your IDE. +cache +..... -If you use TextMate or Mac Vim, you can simply use one of the following built-in -values: +**type**: ``string`` **default**: ``php_array`` -* ``textmate`` -* ``macvim`` +This option can be one of the following values: -You can also specify a custom file link string. If you do this, all percentage -signs (``%``) must be doubled to escape that character. For example, the -full TextMate string would look like this: +php_array + Use a PHP array to cache annotations in memory +file + Use the filesystem to cache annotations +none + Disable the caching of annotations -.. code-block:: yaml +debug +..... - framework: - ide: "txmt://open?url=file://%%f&line=%%l" +**type**: ``boolean`` **default**: ``%kernel.debug%`` -Of course, since every developer uses a different IDE, it's better to set -this on a system level. This can be done by setting the ``xdebug.file_link_format`` -PHP.ini value to the file link string. If this configuration value is set, then -the ``ide`` option does not need to be specified. +Whether to enable debug mode for caching. If enabled, the cache will +automatically update when the original file is changed (both with code and +annotation changes). For performance reasons, it is recommended to disable +debug mode in production, which will happen automatically if you use the +default value. -.. _reference-framework-test: +file_cache_dir +.............. -test -~~~~ +**type**: ``string`` **default**: ``%kernel.cache_dir%/annotations`` -**type**: ``Boolean`` +The directory to store cache files for annotations, in case +``annotations.cache`` is set to ``'file'``. -If this configuration parameter is present (and not ``false``), then the -services related to testing your application (e.g. ``test.client``) are loaded. -This setting should be present in your ``test`` environment (usually via -``app/config/config_test.yml``). For more information, see :doc:`/book/testing`. +assets +~~~~~~ -trusted_proxies -~~~~~~~~~~~~~~~ +.. _reference-assets-base-path: -**type**: ``array`` +base_path +......... -Configures the IP addresses that should be trusted as proxies. For more details, -see :doc:`/components/http_foundation/trusting_proxies`. +**type**: ``string`` -.. versionadded:: 2.3 - CIDR notation support was introduced, so you can whitelist whole - subnets (e.g. ``10.0.0.0/8``, ``fc00::/7``). +This option allows you to define a base path to be used for assets: .. configuration-block:: .. code-block:: yaml + # config/packages/framework.yaml framework: - trusted_proxies: [192.0.0.1, 10.0.0.0/8] + # ... + assets: + base_path: '/images' .. code-block:: xml - - - + + + + + + + + .. code-block:: php - $container->loadFromExtension('framework', array( - 'trusted_proxies' => array('192.0.0.1', '10.0.0.0/8'), - )); + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; -.. _reference-framework-form: + return static function (FrameworkConfig $framework): void { + // ... + $framework->assets() + ->basePath('/images'); + }; -form -~~~~ +.. _reference-templating-base-urls: +.. _reference-assets-base-urls: -csrf_protection -~~~~~~~~~~~~~~~ +base_urls +......... -session -~~~~~~~ +**type**: ``array`` -cookie_lifetime -............... +This option allows you to define base URLs to be used for assets. +If multiple base URLs are provided, Symfony will select one from the +collection each time it generates an asset's path: -**type**: ``integer`` **default**: ``0`` +.. configuration-block:: -This determines the lifetime of the session - in seconds. By default it will use -``0``, which means the cookie is valid for the length of the browser session. + .. code-block:: yaml -cookie_path -........... + # config/packages/framework.yaml + framework: + # ... + assets: + base_urls: + - 'http://cdn.example.com/' -**type**: ``string`` **default**: ``/`` + .. code-block:: xml -This determines the path to set in the session cookie. By default it will use ``/``. + + + -cookie_domain -............. + + + + -**type**: ``string`` **default**: ``''`` + .. code-block:: php -This determines the domain to set in the session cookie. By default it's blank, -meaning the host name of the server which generated the cookie according -to the cookie specification. + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; -cookie_secure -............. + return static function (FrameworkConfig $framework): void { + // ... + $framework->assets() + ->baseUrls(['http://cdn.example.com/']); + }; -**type**: ``Boolean`` **default**: ``false`` +.. _reference-assets-json-manifest-path: +.. _reference-templating-json-manifest-path: -This determines whether cookies should only be sent over secure connections. +json_manifest_path +.................. -cookie_httponly -............... +**type**: ``string`` **default**: ``null`` -**type**: ``Boolean`` **default**: ``false`` +The file path or absolute URL to a ``manifest.json`` file containing an +associative array of asset names and their respective compiled names. A common +cache-busting technique using a "manifest" file works by writing out assets with +a "hash" appended to their file names (e.g. ``main.ae433f1cb.css``) during a +front-end compilation routine. -This determines whether cookies should only accessible through the HTTP protocol. -This means that the cookie won't be accessible by scripting languages, such -as JavaScript. This setting can effectively help to reduce identity theft -through XSS attacks. +.. tip:: -gc_probability -.............. + Symfony's :ref:`Webpack Encore ` supports + :ref:`outputting hashed assets `. Moreover, this + can be incorporated into many other workflows, including Webpack and + Gulp using `webpack-manifest-plugin`_ and `gulp-rev`_, respectively. -.. versionadded:: 2.1 - The ``gc_probability`` option is new in version 2.1 +This option can be set globally for all assets and individually for each asset +package: -**type**: ``integer`` **default**: ``1`` +.. configuration-block:: -This defines the probability that the garbage collector (GC) process is started -on every session initialization. The probability is calculated by using -``gc_probability`` / ``gc_divisor``, e.g. 1/100 means there is a 1% chance -that the GC process will start on each request. + .. code-block:: yaml -gc_divisor -.......... + # config/packages/framework.yaml + framework: + assets: + # this manifest is applied to every asset (including packages) + json_manifest_path: "%kernel.project_dir%/public/build/manifest.json" + # you can use absolute URLs too and Symfony will download them automatically + # json_manifest_path: 'https://cdn.example.com/manifest.json' + packages: + foo_package: + # this package uses its own manifest (the default file is ignored) + json_manifest_path: "%kernel.project_dir%/public/build/a_different_manifest.json" + # Throws an exception when an asset is not found in the manifest + strict_mode: %kernel.debug% + bar_package: + # this package uses the global manifest (the default file is used) + base_path: '/images' -.. versionadded:: 2.1 - The ``gc_divisor`` option is new in version 2.1 + .. code-block:: xml -**type**: ``integer`` **default**: ``100`` + + + + + + + + + + + + + + + + + -See `gc_probability`_. + .. code-block:: php -gc_maxlifetime -.............. + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; -.. versionadded:: 2.1 - The ``gc_maxlifetime`` option is new in version 2.1 + return static function (FrameworkConfig $framework): void { + // ... + $framework->assets() + // this manifest is applied to every asset (including packages) + ->jsonManifestPath('%kernel.project_dir%/public/build/manifest.json'); -**type**: ``integer`` **default**: ``14400`` + // you can use absolute URLs too and Symfony will download them automatically + // 'json_manifest_path' => 'https://cdn.example.com/manifest.json', + $framework->assets()->package('foo_package') + // this package uses its own manifest (the default file is ignored) + ->jsonManifestPath('%kernel.project_dir%/public/build/a_different_manifest.json') + // Throws an exception when an asset is not found in the manifest + ->setStrictMode('%kernel.debug%'); -This determines the number of seconds after which data will be seen as "garbage" -and potentially cleaned up. Garbage collection may occur during session start -and depends on `gc_divisor`_ and `gc_probability`_. + $framework->assets()->package('bar_package') + // this package uses the global manifest (the default file is used) + ->basePath('/images'); + }; -save_path -......... +.. note:: -**type**: ``string`` **default**: ``%kernel.cache.dir%/sessions`` + This parameter cannot be set at the same time as ``version`` or ``version_strategy``. + Additionally, this option cannot be nullified at the package scope if a global manifest + file is specified. -This determines the argument to be passed to the save handler. If you choose -the default file handler, this is the path where the files are created. You can -also set this value to the ``save_path`` of your ``php.ini`` by setting the -value to ``null``: +.. tip:: + + If you request an asset that is *not found* in the ``manifest.json`` file, the original - + *unmodified* - asset path will be returned. + You can set ``strict_mode`` to ``true`` to get an exception when an asset is *not found*. + +.. note:: + + If a URL is set, the JSON manifest is downloaded on each request using the `http_client`_. + +.. _reference-framework-assets-packages: + +packages +........ + +You can group assets into packages, to specify different base URLs for them: .. configuration-block:: .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: - session: - save_path: null + # ... + assets: + packages: + avatars: + base_urls: 'http://static_cdn.example.com/avatars' .. code-block:: xml - - - - + + + + + + + + + + .. code-block:: php - // app/config/config.php - $container->loadFromExtension('framework', array( - 'session' => array( - 'save_path' => null, - ), - )); + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; -.. _configuration-framework-serializer: + return static function (FrameworkConfig $framework): void { + // ... + $framework->assets() + ->package('avatars') + ->baseUrls(['http://static_cdn.example.com/avatars']); + }; -serializer -~~~~~~~~~~ +Now you can use the ``avatars`` package in your templates: -enabled -....... +.. code-block:: html+twig -**type**: ``boolean`` **default**: ``false`` + -Whether to enable the ``serializer`` service or not in the service container. +Each package can configure the following options: -For more details, see :doc:`/cookbook/serializer`. +* :ref:`base_path ` +* :ref:`base_urls ` +* :ref:`version_strategy ` +* :ref:`version ` +* :ref:`version_format ` +* :ref:`json_manifest_path ` +* :ref:`strict_mode ` -templating -~~~~~~~~~~ +.. _reference-assets-strict-mode: -assets_base_urls -................ +strict_mode +........... + +**type**: ``boolean`` **default**: ``false`` -**default**: ``{ http: [], ssl: [] }`` - -This option allows you to define base URLs to be used for assets referenced -from ``http`` and ``ssl`` (``https``) pages. A string value may be provided in -lieu of a single-element array. If multiple base URLs are provided, Symfony2 -will select one from the collection each time it generates an asset's path. - -For your convenience, ``assets_base_urls`` can be set directly with a string or -array of strings, which will be automatically organized into collections of base -URLs for ``http`` and ``https`` requests. If a URL starts with ``https://`` or -is `protocol-relative`_ (i.e. starts with `//`) it will be added to both -collections. URLs starting with ``http://`` will only be added to the -``http`` collection. - -.. versionadded:: 2.1 - Unlike most configuration blocks, successive values for ``assets_base_urls`` - will overwrite each other instead of being merged. This behavior was chosen - because developers will typically define base URL's for each environment. - Given that most projects tend to inherit configurations - (e.g. ``config_test.yml`` imports ``config_dev.yml``) and/or share a common - base configuration (i.e. ``config.yml``), merging could yield a set of base - URL's for multiple environments. +When enabled, the strict mode asserts that all requested assets are in the +manifest file. This option is useful to detect typos or missing assets, the +recommended value is ``%kernel.debug%``. +.. _reference-framework-assets-version: .. _ref-framework-assets-version: -assets_version -.............. +version +....... **type**: ``string`` This option is used to *bust* the cache on assets by globally adding a query parameter to all rendered asset paths (e.g. ``/images/logo.png?v2``). This -applies only to assets rendered via the Twig ``asset`` function (or PHP equivalent) -as well as assets rendered with Assetic. +applies only to assets rendered via the Twig ``asset()`` function (or PHP +equivalent). For example, suppose you have the following: -.. configuration-block:: - - .. code-block:: html+jinja - - Symfony! +.. code-block:: html+twig - .. code-block:: php - - Symfony! + Symfony! By default, this will render a path to your image such as ``/images/logo.png``. -Now, activate the ``assets_version`` option: +Now, activate the ``version`` option: .. configuration-block:: .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: # ... - templating: { engines: ['twig'], assets_version: v2 } + assets: + version: 'v2' .. code-block:: xml - - - - + + + + + + + + .. code-block:: php - // app/config/config.php - $container->loadFromExtension('framework', array( - ..., - 'templating' => array( - 'engines' => array('twig'), - 'assets_version' => 'v2', - ), - )); + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + $framework->assets() + ->version('v2'); + }; Now, the same asset will be rendered as ``/images/logo.png?v2`` If you use -this feature, you **must** manually increment the ``assets_version`` value +this feature, you **must** manually increment the ``version`` value before each deployment so that the query parameters change. -You can also control how the query string works via the `assets_version_format`_ +You can also control how the query string works via the `version_format`_ option. -assets_version_format -..................... +.. note:: + + This parameter cannot be set at the same time as ``version_strategy`` or ``json_manifest_path``. + +.. tip:: + + As with all settings, you can use a parameter as value for the + ``version``. This makes it easier to increment the cache on each + deployment. + +.. _reference-templating-version-format: +.. _reference-assets-version-format: + +version_format +.............. **type**: ``string`` **default**: ``%%s?%%s`` -This specifies a :phpfunction:`sprintf` pattern that will be used with the `assets_version`_ -option to construct an asset's path. By default, the pattern adds the asset's -version as a query string. For example, if ``assets_version_format`` is set to -``%%s?version=%%s`` and ``assets_version`` is set to ``5``, the asset's path -would be ``/images/logo.png?version=5``. +This specifies a :phpfunction:`sprintf` pattern that will be used with the +`version`_ option to construct an asset's path. By default, the pattern +adds the asset's version as a query string. For example, if +``version_format`` is set to ``%%s?version=%%s`` and ``version`` +is set to ``5``, the asset's path would be ``/images/logo.png?version=5``. .. note:: - All percentage signs (``%``) in the format string must be doubled to escape - the character. Without escaping, values might inadvertently be interpreted - as :ref:`book-service-container-parameters`. + All percentage signs (``%``) in the format string must be doubled to + escape the character. Without escaping, values might inadvertently be + interpreted as :ref:`service-container-parameters`. .. tip:: - Some CDN's do not support cache-busting via query strings, so injecting the - version into the actual file path is necessary. Thankfully, ``assets_version_format`` - is not limited to producing versioned query strings. + Some CDN's do not support cache-busting via query strings, so injecting + the version into the actual file path is necessary. Thankfully, + ``version_format`` is not limited to producing versioned query + strings. + + The pattern receives the asset's original path and version as its first + and second parameters, respectively. Since the asset's path is one + parameter, you cannot modify it in-place (e.g. ``/images/logo-v5.png``); + however, you can prefix the asset's path using a pattern of + ``version-%%2$s/%%1$s``, which would result in the path + ``version-5/images/logo.png``. + + URL rewrite rules could then be used to disregard the version prefix + before serving the asset. Alternatively, you could copy assets to the + appropriate version path as part of your deployment process and forgot + any URL rewriting. The latter option is useful if you would like older + asset versions to remain accessible at their original URL. + +.. _reference-assets-version-strategy: +.. _reference-templating-version-strategy: - The pattern receives the asset's original path and version as its first and - second parameters, respectively. Since the asset's path is one parameter, you - cannot modify it in-place (e.g. ``/images/logo-v5.png``); however, you can - prefix the asset's path using a pattern of ``version-%%2$s/%%1$s``, which - would result in the path ``version-5/images/logo.png``. +version_strategy +................ - URL rewrite rules could then be used to disregard the version prefix before - serving the asset. Alternatively, you could copy assets to the appropriate - version path as part of your deployment process and forgo any URL rewriting. - The latter option is useful if you would like older asset versions to remain - accessible at their original URL. +**type**: ``string`` **default**: ``null`` -Full Default Configuration --------------------------- +The service id of the :doc:`asset version strategy ` +applied to the assets. This option can be set globally for all assets and +individually for each asset package: .. configuration-block:: .. code-block:: yaml + # config/packages/framework.yaml framework: - secret: ~ - http_method_override: true - trusted_proxies: [] - ide: ~ - test: ~ - default_locale: en - - # form configuration - form: - enabled: false - csrf_protection: - enabled: false - field_name: _token - - # esi configuration - esi: - enabled: false - - # fragments configuration - fragments: - enabled: false - path: /_fragment - - # profiler configuration - profiler: - enabled: false - only_exceptions: false - only_master_requests: false - dsn: file:%kernel.cache_dir%/profiler - username: - password: - lifetime: 86400 - matcher: - ip: ~ - - # use the urldecoded format - path: ~ # Example: ^/path to resource/ - service: ~ - - # router configuration - router: - resource: ~ # Required - type: ~ - http_port: 80 - https_port: 443 - - # set to true to throw an exception when a parameter does not match the requirements - # set to false to disable exceptions when a parameter does not match the requirements (and return null instead) - # set to null to disable parameter checks against requirements - # 'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production - strict_requirements: true - - # session configuration - session: - storage_id: session.storage.native - handler_id: session.handler.native_file - name: ~ - cookie_lifetime: ~ - cookie_path: ~ - cookie_domain: ~ - cookie_secure: ~ - cookie_httponly: ~ - gc_divisor: ~ - gc_probability: ~ - gc_maxlifetime: ~ - save_path: %kernel.cache_dir%/sessions - - # serializer configuration - serializer: - enabled: false - - # templating configuration - templating: - assets_version: ~ - assets_version_format: %%s?%%s - hinclude_default_template: ~ - form: - resources: - - # Default: - - FrameworkBundle:Form - assets_base_urls: - http: [] - ssl: [] - cache: ~ - engines: # Required - - # Example: - - twig - loaders: [] + assets: + # this strategy is applied to every asset (including packages) + version_strategy: 'app.asset.my_versioning_strategy' packages: + foo_package: + # this package removes any versioning (its assets won't be versioned) + version: ~ + bar_package: + # this package uses its own strategy (the default strategy is ignored) + version_strategy: 'app.asset.another_version_strategy' + baz_package: + # this package inherits the default strategy + base_path: '/images' + + .. code-block:: xml + + + + + + + + + + + + + + + + - # Prototype - name: - version: ~ - version_format: %%s?%%s - base_urls: - http: [] - ssl: [] + .. code-block:: php - # translator configuration - translator: - enabled: false - fallback: en + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; - # validation configuration - validation: - enabled: false - cache: ~ - enable_annotations: false - translation_domain: validators - - # annotation configuration - annotations: - cache: file - file_cache_dir: %kernel.cache_dir%/annotations - debug: %kernel.debug% - -.. _`protocol-relative`: http://tools.ietf.org/html/rfc3986#section-4.2 + return static function (FrameworkConfig $framework): void { + // ... + $framework->assets() + ->versionStrategy('app.asset.my_versioning_strategy'); + + $framework->assets()->package('foo_package') + // this package removes any versioning (its assets won't be versioned) + ->version(null); + + $framework->assets()->package('bar_package') + // this package uses its own strategy (the default strategy is ignored) + ->versionStrategy('app.asset.another_version_strategy'); + + $framework->assets()->package('baz_package') + // this package inherits the default strategy + ->basePath('/images'); + }; + +.. note:: + + This parameter cannot be set at the same time as ``version`` or ``json_manifest_path``. + +.. _reference-cache: + +cache +~~~~~ + +.. _reference-cache-app: + +app +... + +**type**: ``string`` **default**: ``cache.adapter.filesystem`` + +The cache adapter used by the ``cache.app`` service. The FrameworkBundle +ships with multiple adapters: ``cache.adapter.apcu``, ``cache.adapter.system``, +``cache.adapter.filesystem``, ``cache.adapter.psr6``, ``cache.adapter.redis``, +``cache.adapter.memcached``, ``cache.adapter.pdo`` and +``cache.adapter.doctrine_dbal``. + +There's also a special adapter called ``cache.adapter.array`` which stores +contents in memory using a PHP array and it's used to disable caching (mostly on +the ``dev`` environment). + +.. tip:: + + It might be tough to understand at the beginning, so to avoid confusion + remember that all pools perform the same actions but on different medium + given the adapter they are based on. Internally, a pool wraps the definition + of an adapter. + +default_doctrine_provider +......................... + +**type**: ``string`` + +The service name to use as your default Doctrine provider. The provider is +available as the ``cache.default_doctrine_provider`` service. + +default_memcached_provider +.......................... + +**type**: ``string`` **default**: ``memcached://localhost`` + +The DSN to use by the Memcached provider. The provider is available as the ``cache.default_memcached_provider`` +service. + +default_pdo_provider +.................... + +**type**: ``string`` **default**: ``doctrine.dbal.default_connection`` + +The service id of the database connection, which should be either a PDO or a +Doctrine DBAL instance. The provider is available as the ``cache.default_pdo_provider`` +service. + +default_psr6_provider +..................... + +**type**: ``string`` + +The service name to use as your default PSR-6 provider. It is available as +the ``cache.default_psr6_provider`` service. + +default_redis_provider +...................... + +**type**: ``string`` **default**: ``redis://localhost`` + +The DSN to use by the Redis provider. The provider is available as the ``cache.default_redis_provider`` +service. + +directory +......... + +**type**: ``string`` **default**: ``%kernel.cache_dir%/pools`` + +The path to the cache directory used by services inheriting from the +``cache.adapter.filesystem`` adapter (including ``cache.app``). + +pools +..... + +**type**: ``array`` + +A list of cache pools to be created by the framework extension. + +.. seealso:: + + For more information about how pools work, see :ref:`cache pools `. + +To configure a Redis cache pool with a default lifetime of 1 hour, do the following: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + cache: + pools: + cache.mycache: + adapter: cache.adapter.redis + default_lifetime: 3600 + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->cache() + ->pool('cache.mycache') + ->adapters(['cache.adapter.redis']) + ->defaultLifetime(3600); + }; + +adapter +""""""" + +**type**: ``string`` **default**: ``cache.app`` + +The service name of the adapter to use. You can specify one of the default +services that follow the pattern ``cache.adapter.[type]``. Alternatively you +can specify another cache pool as base, which will make this pool inherit the +settings from the base pool as defaults. + +.. note:: + + Your service needs to implement the ``Psr\Cache\CacheItemPoolInterface`` interface. + +clearer +""""""" + +**type**: ``string`` + +The cache clearer used to clear your PSR-6 cache. + +.. seealso:: + + For more information, see :class:`Symfony\\Component\\HttpKernel\\CacheClearer\\Psr6CacheClearer`. + +default_lifetime +"""""""""""""""" + +**type**: ``integer`` | ``string`` + +Default lifetime of your cache items. Give an integer value to set the default +lifetime in seconds. A string value could be ISO 8601 time interval, like ``"PT5M"`` +or a PHP date expression that is accepted by ``strtotime()``, like ``"5 minutes"``. + +If no value is provided, the cache adapter will fallback to the default value on +the actual cache storage. + +.. _reference-cache-pools-name: + +name +"""" + +**type**: ``prototype`` + +Name of the pool you want to create. + +.. note:: + + Your pool name must differ from ``cache.app`` or ``cache.system``. + +provider +"""""""" + +**type**: ``string`` + +Overwrite the default service name or DSN respectively, if you do not want to +use what is configured as ``default_X_provider`` under ``cache``. See the +description of the default provider setting above for information on how to +specify your specific provider. + +public +"""""" + +**type**: ``boolean`` **default**: ``false`` + +Whether your service should be public or not. + +tags +"""" + +**type**: ``boolean`` | ``string`` **default**: ``null`` + +Whether your service should be able to handle tags or not. +Can also be the service id of another cache pool where tags will be stored. + +.. _reference-cache-prefix-seed: + +prefix_seed +........... + +**type**: ``string`` **default**: ``_%kernel.project_dir%.%kernel.container_class%`` + +This value is used as part of the "namespace" generated for the +cache item keys. A common practice is to use the unique name of the application +(e.g. ``symfony.com``) because that prevents naming collisions when deploying +multiple applications into the same path (on different servers) that share the +same cache backend. + +It's also useful when using `blue/green deployment`_ strategies and more +generally, when you need to abstract out the actual deployment directory (for +example, when warming caches offline). + +.. note:: + + The ``prefix_seed`` option is used at compile time. This means + that any change made to this value after container's compilation + will have no effect. + +.. _reference-cache-system: + +system +...... + +**type**: ``string`` **default**: ``cache.adapter.system`` + +The cache adapter used by the ``cache.system`` service. It supports the same +adapters available for the ``cache.app`` service. + +.. _reference-framework-csrf-protection: + +csrf_protection +~~~~~~~~~~~~~~~ + +.. seealso:: + + For more information about CSRF protection, see :doc:`/security/csrf`. + +.. _reference-csrf_protection-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation + +This option can be used to disable CSRF protection on *all* forms. But you +can also :ref:`disable CSRF protection on individual forms `. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + csrf_protection: true + + .. code-block:: xml + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + return static function (FrameworkConfig $framework): void { + $framework->csrfProtection() + ->enabled(true) + ; + }; + +If you're using forms, but want to avoid starting your session (e.g. using +forms in an API-only website), ``csrf_protection`` will need to be set to +``false``. + +.. _config-framework-default_locale: + +default_locale +~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``en`` + +The default locale is used if no ``_locale`` routing parameter has been +set. It is available with the +:method:`Request::getDefaultLocale ` +method. + +.. seealso:: + + You can read more information about the default locale in + :ref:`translation-default-locale`. + +.. _reference-translator-enabled-locales: +.. _reference-enabled-locales: + +enabled_locales +............... + +**type**: ``array`` **default**: ``[]`` (empty array = enable all locales) + +Symfony applications generate by default the translation files for validation +and security messages in all locales. If your application only uses some +locales, use this option to restrict the files generated by Symfony and improve +performance a bit: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/translation.yaml + framework: + enabled_locales: ['en', 'es'] + + .. code-block:: xml + + + + + + + en + es + + + + .. code-block:: php + + // config/packages/translation.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->enabledLocales(['en', 'es']); + }; + +An added bonus of defining the enabled locales is that they are automatically +added as a requirement of the :ref:`special _locale parameter `. +For example, if you define this value as ``['ar', 'he', 'ja', 'zh']``, the +``_locale`` routing parameter will have an ``ar|he|ja|zh`` requirement. If some +user makes requests with a locale not included in this option, they'll see a 404 error. + +set_content_language_from_locale +................................ + +**type**: ``boolean`` **default**: ``false`` + +If this option is set to ``true``, the response will have a ``Content-Language`` +HTTP header set with the ``Request`` locale. + +set_locale_from_accept_language +............................... + +**type**: ``boolean`` **default**: ``false`` + +If this option is set to ``true``, the ``Request`` locale will automatically be +set to the value of the ``Accept-Language`` HTTP header. + +When the ``_locale`` request attribute is passed, the ``Accept-Language`` header +is ignored. + +disallow_search_engine_index +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``true`` when the debug mode is enabled, ``false`` otherwise. + +If ``true``, Symfony adds a ``X-Robots-Tag: noindex`` HTTP tag to all responses +(unless your own app adds that header, in which case it's not modified). This +`X-Robots-Tag HTTP header`_ tells search engines to not index your web site. +This option is a protection measure in case you accidentally publish your site +in debug mode. + +.. _config-framework-error_controller: + +error_controller +~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``error_controller`` + +This is the controller that is called when an exception is thrown anywhere in +your application. The default controller +(:class:`Symfony\\Component\\HttpKernel\\Controller\\ErrorController`) +renders specific templates under different error conditions (see +:doc:`/controller/error_pages`). + +esi +~~~ + +.. seealso:: + + You can read more about Edge Side Includes (ESI) in :ref:`edge-side-includes`. + +.. _reference-esi-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``false`` + +Whether to enable the edge side includes support in the framework. + +You can also set ``esi`` to ``true`` to enable it: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + esi: true + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->esi()->enabled(true); + }; + +.. _framework_exceptions: + +exceptions +~~~~~~~~~~ + +**type**: ``array`` + +Defines the :ref:`log level `, :ref:`log channel ` +and HTTP status code applied to the exceptions that match the given exception class: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/exceptions.yaml + framework: + exceptions: + Symfony\Component\HttpKernel\Exception\BadRequestHttpException: + log_level: 'debug' + status_code: 422 + log_channel: 'custom_channel' + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // config/packages/exceptions.php + use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->exception(BadRequestHttpException::class) + ->logLevel('debug') + ->statusCode(422) + ->logChannel('custom_channel') + ; + }; + +.. versionadded:: 7.3 + + The ``log_channel`` option was introduced in Symfony 7.3. + +The order in which you configure exceptions is important because Symfony will +use the configuration of the first exception that matches ``instanceof``: + +.. code-block:: yaml + + # config/packages/exceptions.yaml + framework: + exceptions: + Exception: + log_level: 'debug' + status_code: 404 + # The following configuration will never be used because \RuntimeException extends \Exception + RuntimeException: + log_level: 'debug' + status_code: 422 + +You can map a status code and a set of headers to an exception thanks +to the ``#[WithHttpStatus]`` attribute on the exception class:: + + namespace App\Exception; + + use Symfony\Component\HttpKernel\Attribute\WithHttpStatus; + + #[WithHttpStatus(422, [ + 'Retry-After' => 10, + 'X-Custom-Header' => 'header-value', + ])] + class CustomException extends \Exception + { + } + +It is also possible to map a log level on a custom exception class using +the ``#[WithLogLevel]`` attribute:: + + namespace App\Exception; + + use Psr\Log\LogLevel; + use Symfony\Component\HttpKernel\Attribute\WithLogLevel; + + #[WithLogLevel(LogLevel::WARNING)] + class CustomException extends \Exception + { + } + +The attributes can also be added to interfaces directly:: + + namespace App\Exception; + + use Symfony\Component\HttpKernel\Attribute\WithHttpStatus; + + #[WithHttpStatus(422)] + interface CustomExceptionInterface + { + } + + class CustomException extends \Exception implements CustomExceptionInterface + { + } + +.. versionadded:: 7.1 + + Support to use ``#[WithHttpStatus]`` and ``#[WithLogLevel]`` attributes + on interfaces was introduced in Symfony 7.1. + +.. _reference-framework-form: + +form +~~~~ + +.. _reference-form-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation + +Whether to enable the form services or not in the service container. If +you don't use forms, setting this to ``false`` may increase your application's +performance because less services will be loaded into the container. + +This option will automatically be set to ``true`` when one of the child +settings is configured. + +.. note:: + + This will automatically enable the `validation`_. + +.. seealso:: + + For more details, see :doc:`/forms`. + +.. _reference-form-field-name: + +field_name +.......... + +**type**: ``string`` **default**: ``_token`` + +This is the field name that you should give to the CSRF token field of your forms. + +fragments +~~~~~~~~~ + +.. seealso:: + + Learn more about fragments in the + :ref:`HTTP Cache article `. + +.. _reference-fragments-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``false`` + +Whether to enable the fragment listener or not. The fragment listener is +used to render ESI fragments independently of the rest of the page. + +This setting is automatically set to ``true`` when one of the child settings +is configured. + +hinclude_default_template +......................... + +**type**: ``string`` **default**: ``null`` + +Sets the content shown during the loading of the fragment or when JavaScript +is disabled. This can be either a template name or the content itself. + +.. seealso:: + + See :ref:`templates-hinclude` for more information about hinclude. + +.. _reference-fragments-path: + +path +.... + +**type**: ``string`` **default**: ``/_fragment`` + +The path prefix for fragments. The fragment listener will only be executed +when the request starts with this path. + +handle_all_throwables +~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``true`` + +When set to ``true``, the Symfony kernel will catch all ``\Throwable`` exceptions +thrown by the application and will turn them into HTTP responses. + +html_sanitizer +~~~~~~~~~~~~~~ + +The ``html_sanitizer`` option (and its children) are used to configure +custom HTML sanitizers. Read more about the options in the +:ref:`HTML sanitizer documentation `. + +.. _configuration-framework-http_cache: + +http_cache +~~~~~~~~~~ + +allow_reload +............ + +**type**: ``string`` + +Specifies whether the client can force a cache reload by including a +Cache-Control "no-cache" directive in the request. Set it to ``true`` +for compliance with RFC 2616. (default: false) + +allow_revalidate +................ + +**type**: ``string`` + +Specifies whether the client can force a cache revalidate by including a +Cache-Control "max-age=0" directive in the request. Set it to ``true`` +for compliance with RFC 2616. (default: false) + +debug +..... + +**type**: ``boolean`` **default**: ``%kernel.debug%`` + +If true, exceptions are thrown when things go wrong. Otherwise, the cache will +try to carry on and deliver a meaningful response. + +default_ttl +........... + +**type**: ``integer`` + +The number of seconds that a cache entry should be considered fresh when no +explicit freshness information is provided in a response. Explicit +Cache-Control or Expires headers override this value. (default: 0) + +enabled +....... + +**type**: ``boolean`` **default**: ``false`` + +private_headers +............... + +**type**: ``array`` + +Set of request headers that trigger "private" cache-control behavior on responses +that don't explicitly state whether the response is public or private via a +Cache-Control directive. (default: Authorization and Cookie) + +skip_response_headers +..................... + +**type**: ``array`` **default**: ``Set-Cookie`` + +Set of response headers that will never be cached even when the response is cacheable +and public. + +stale_if_error +.............. + +**type**: ``integer`` + +Specifies the default number of seconds (the granularity is the second) during +which the cache can serve a stale response when an error is encountered +(default: 60). This setting is overridden by the stale-if-error HTTP +Cache-Control extension (see RFC 5861). + +stale_while_revalidate +...................... + +**type**: ``integer`` + +Specifies the default number of seconds (the granularity is the second as the +Response TTL precision is a second) during which the cache can immediately return +a stale response while it revalidates it in the background (default: 2). +This setting is overridden by the stale-while-revalidate HTTP Cache-Control +extension (see RFC 5861). + +trace_header +............ + +**type**: ``string`` + +Header name to use for traces. (default: X-Symfony-Cache) + +trace_level +........... + +**type**: ``string`` **possible values**: ``'none'``, ``'short'`` or ``'full'`` + +For 'short', a concise trace of the main request will be added as an HTTP header. +'full' will add traces for all requests (including ESI subrequests). +(default: 'full' if in debug; 'none' otherwise) + +.. _reference-http-client: + +http_client +~~~~~~~~~~~ + +When the HttpClient component is installed, an HTTP client is available +as a service named ``http_client`` or using the autowiring alias +:class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface`. + +.. _reference-http-client-default-options: + +This service can be configured using ``framework.http_client.default_options``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + http_client: + max_host_connections: 10 + default_options: + headers: { 'X-Powered-By': 'ACME App' } + max_redirects: 7 + + .. code-block:: xml + + + + + + + + + ACME App + + + + + + .. code-block:: php + + // config/packages/framework.php + $container->loadFromExtension('framework', [ + 'http_client' => [ + 'max_host_connections' => 10, + 'default_options' => [ + 'headers' => [ + 'X-Powered-By' => 'ACME App', + ], + 'max_redirects' => 7, + ], + ], + ]); + + .. code-block:: php-standalone + + $client = HttpClient::create([ + 'headers' => [ + 'X-Powered-By' => 'ACME App', + ], + 'max_redirects' => 7, + ], 10); + +.. _reference-http-client-scoped-clients: + +Multiple pre-configured HTTP client services can be defined, each with its +service name defined as a key under ``scoped_clients``. Scoped clients inherit +the default options defined for the ``http_client`` service. You can override +these options and can define a few others: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + http_client: + scoped_clients: + my_api.client: + auth_bearer: secret_bearer_token + # ... + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + $container->loadFromExtension('framework', [ + 'http_client' => [ + 'scoped_clients' => [ + 'my_api.client' => [ + 'auth_bearer' => 'secret_bearer_token', + // ... + ], + ], + ], + ]); + + .. code-block:: php-standalone + + $client = HttpClient::createForBaseUri('https://...', [ + 'auth_bearer' => 'secret_bearer_token', + // ... + ]); + +Options defined for scoped clients apply only to URLs that match either their +`base_uri`_ or the `scope`_ option when it is defined. Non-matching URLs always +use default options. + +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. + +auth_basic +.......... + +**type**: ``string`` + +The username and password used to create the ``Authorization`` HTTP header +used in HTTP Basic authentication. The value of this option must follow the +format ``username:password``. + +auth_bearer +........... + +**type**: ``string`` + +The token used to create the ``Authorization`` HTTP header used in HTTP Bearer +authentication (also called token authentication). + +auth_ntlm +......... + +**type**: ``string`` + +The username and password used to create the ``Authorization`` HTTP header used +in the `Microsoft NTLM authentication protocol`_. The value of this option must +follow the format ``username:password``. This authentication mechanism requires +using the cURL-based transport. + +.. _reference-http-client-base-uri: + +base_uri +........ + +**type**: ``string`` + +URI that is merged into relative URIs, following the rules explained in the +`RFC 3986`_ standard. This is useful when all the requests you make share a +common prefix (e.g. ``https://api.github.com/``) so you can avoid adding it to +every request. + +Here are some common examples of how ``base_uri`` merging works in practice: + +========================== ================== ============================= +``base_uri`` Relative URI Actual Requested URI +========================== ================== ============================= +http://example.org /bar http://example.org/bar +http://example.org/foo /bar http://example.org/bar +http://example.org/foo bar http://example.org/bar +http://example.org/foo/ /bar http://example.org/bar +http://example.org/foo/ bar http://example.org/foo/bar +http://example.org http://symfony.com http://symfony.com +http://example.org/?bar bar http://example.org/bar +http://example.org/api/v4 /bar http://example.org/bar +http://example.org/api/v4/ /bar http://example.org/bar +http://example.org/api/v4 bar http://example.org/api/bar +http://example.org/api/v4/ bar http://example.org/api/v4/bar +========================== ================== ============================= + +bindto +...... + +**type**: ``string`` + +A network interface name, IP address, a host name or a UNIX socket to use as the +outgoing network interface. + +buffer +...... + +**type**: ``boolean`` | ``Closure`` + +Buffering the response means that you can access its content multiple times +without performing the request again. Buffering is enabled by default when the +content type of the response is ``text/*``, ``application/json`` or ``application/xml``. + +If this option is a boolean value, the response is buffered when the value is +``true``. If this option is a closure, the response is buffered when the +returned value is ``true`` (the closure receives as argument an array with the +response headers). + +cafile +...... + +**type**: ``string`` + +The path of the certificate authority file that contains one or more +certificates used to verify the other servers' certificates. + +capath +...... + +**type**: ``string`` + +The path to a directory that contains one or more certificate authority files. + +ciphers +....... + +**type**: ``string`` + +A list of the names of the ciphers allowed for the TLS connections. They +can be separated by colons, commas or spaces (e.g. ``'RC4-SHA:TLS13-AES-128-GCM-SHA256'``). + +crypto_method +............. + +**type**: ``integer`` + +The minimum version of TLS to accept. The value must be one of the +``STREAM_CRYPTO_METHOD_TLSv*_CLIENT`` constants defined by PHP. + +.. _reference-http-client-retry-delay: + +delay +..... + +**type**: ``integer`` **default**: ``1000`` + +The initial delay in milliseconds used to compute the waiting time between retries. + +.. _reference-http-client-retry-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``false`` + +Whether to enable the support for retry failed HTTP request or not. +This setting is automatically set to true when one of the child settings is configured. + +extra +..... + +**type**: ``array`` + +Arbitrary additional data to pass to the HTTP client for further use. +This can be particularly useful when :ref:`decorating an existing client `. + +.. _http-headers: + +headers +....... + +**type**: ``array`` + +An associative array of the HTTP headers added before making the request. This +value must use the format ``['header-name' => 'value0, value1, ...']``. + +.. _reference-http-client-retry-http-codes: + +http_codes +.......... + +**type**: ``array`` **default**: :method:`Symfony\\Component\\HttpClient\\Retry\\GenericRetryStrategy::DEFAULT_RETRY_STATUS_CODES` + +The list of HTTP status codes that triggers a retry of the request. + +http_version +............ + +**type**: ``string`` | ``null`` **default**: ``null`` + +The HTTP version to use, typically ``'1.1'`` or ``'2.0'``. Leave it to ``null`` +to let Symfony select the best version automatically. + +.. _reference-http-client-retry-jitter: + +jitter +...... + +**type**: ``float`` **default**: ``0.1`` (must be between 0.0 and 1.0) + +This option adds some randomness to the delay. It's useful to avoid sending +multiple requests to the server at the exact same time. The randomness is +calculated as ``delay * jitter``. For example: if delay is ``1000ms`` and jitter +is ``0.2``, the actual delay will be a number between ``800`` and ``1200`` (1000 +/- 20%). + +local_cert +.......... + +**type**: ``string`` + +The path to a file that contains the `PEM formatted`_ certificate used by the +HTTP client. This is often combined with the ``local_pk`` and ``passphrase`` +options. + +local_pk +........ + +**type**: ``string`` + +The path of a file that contains the `PEM formatted`_ private key of the +certificate defined in the ``local_cert`` option. + +.. _reference-http-client-retry-max-delay: + +max_delay +......... + +**type**: ``integer`` **default**: ``0`` + +The maximum amount of milliseconds initial to wait between retries. +Use ``0`` to not limit the duration. + +max_duration +............ + +**type**: ``float`` **default**: ``0`` + +The maximum execution time, in seconds, that the request and the response are +allowed to take. A value lower than or equal to 0 means it is unlimited. + +max_host_connections +.................... + +**type**: ``integer`` **default**: ``6`` + +Defines the maximum amount of simultaneously open connections to a single host +(considering a "host" the same as a "host name + port number" pair). This limit +also applies for proxy connections, where the proxy is considered to be the host +for which this limit is applied. + +max_redirects +............. + +**type**: ``integer`` **default**: ``20`` + +The maximum number of redirects to follow. Use ``0`` to not follow any +redirection. + +.. _reference-http-client-retry-max-retries: + +max_retries +........... + +**type**: ``integer`` **default**: ``3`` + +The maximum number of retries for failing requests. When the maximum is reached, +the client returns the last received response. + +.. _reference-http-client-retry-multiplier: + +multiplier +.......... + +**type**: ``float`` **default**: ``2`` + +This value is multiplied to the delay each time a retry occurs, to distribute +retries in time instead of making all of them sequentially. + +no_proxy +........ + +**type**: ``string`` | ``null`` **default**: ``null`` + +A comma separated list of hosts that do not require a proxy to be reached, even +if one is configured. Use the ``'*'`` wildcard to match all hosts and an empty +string to match none (disables the proxy). + +passphrase +.......... + +**type**: ``string`` + +The passphrase used to encrypt the certificate stored in the file defined in the +``local_cert`` option. + +peer_fingerprint +................ + +**type**: ``array`` + +When negotiating a TLS connection, the server sends a certificate +indicating its identity. A public key is extracted from this certificate and if +it does not exactly match any of the public keys provided in this option, the +connection is aborted before sending or receiving any data. + +The value of this option is an associative array of ``algorithm => hash`` +(e.g ``['pin-sha256' => '...']``). + +proxy +..... + +**type**: ``string`` | ``null`` + +The HTTP proxy to use to make the requests. Leave it to ``null`` to detect the +proxy automatically based on your system configuration. + +query +..... + +**type**: ``array`` + +An associative array of the query string values added to the URL before making +the request. This value must use the format ``['parameter-name' => parameter-value, ...]``. + +rate_limiter +............ + +**type**: ``string`` + +The service ID of the rate limiter used to limit the number of HTTP requests +within a certain period. The service must implement the +:class:`Symfony\\Component\\RateLimiter\\LimiterInterface`. + +.. versionadded:: 7.1 + + The ``rate_limiter`` option was introduced in Symfony 7.1. + +resolve +....... + +**type**: ``array`` + +A list of hostnames and their IP addresses to pre-populate the DNS cache used by +the HTTP client in order to avoid a DNS lookup for those hosts. This option is +useful to improve security when IPs are checked before the URL is passed to the +client and to make your tests easier. + +The value of this option is an associative array of ``domain => IP address`` +(e.g ``['symfony.com' => '46.137.106.254', ...]``). + +.. _reference-http-client-retry-failed: + +retry_failed +............ + +**type**: ``array`` + +This option configures the behavior of the HTTP client when some request fails, +including which types of requests to retry and how many times. The behavior is +defined with the following options: + +* :ref:`delay ` +* :ref:`http_codes ` +* :ref:`jitter ` +* :ref:`max_delay ` +* :ref:`max_retries ` +* :ref:`multiplier ` + +.. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + http_client: + # ... + default_options: + retry_failed: + # retry_strategy: app.custom_strategy + http_codes: + 0: ['GET', 'HEAD'] # retry network errors if request method is GET or HEAD + 429: true # retry all responses with 429 status code + 500: ['GET', 'HEAD'] + max_retries: 2 + delay: 1000 + multiplier: 3 + max_delay: 5000 + jitter: 0.3 + + scoped_clients: + my_api.client: + # ... + retry_failed: + max_retries: 4 + +retry_strategy +.............. + +**type**: ``string`` + +The service is used to decide if a request should be retried and to compute the +time to wait between retries. By default, it uses an instance of +:class:`Symfony\\Component\\HttpClient\\Retry\\GenericRetryStrategy` configured +with ``http_codes``, ``delay``, ``max_delay``, ``multiplier`` and ``jitter`` +options. This class has to implement +:class:`Symfony\\Component\\HttpClient\\Retry\\RetryStrategyInterface`. + +scope +..... + +**type**: ``string`` + +For scoped clients only: the regular expression that the URL must match before +applying all other non-default options. By default, the scope is derived from +`base_uri`_. + +timeout +....... + +**type**: ``float`` **default**: depends on your PHP config + +Time, in seconds, to wait for network activity. If the connection is idle for longer, a +:class:`Symfony\\Component\\HttpClient\\Exception\\TransportException` is thrown. +Its default value is the same as the value of PHP's `default_socket_timeout`_ +config option. + +verify_host +........... + +**type**: ``boolean`` **default**: ``true`` + +If ``true``, the certificate sent by other servers is verified to ensure that +their common name matches the host included in the URL. This is usually +combined with ``verify_peer`` to also verify the certificate authenticity. + +verify_peer +........... + +**type**: ``boolean`` **default**: ``true`` + +If ``true``, the certificate sent by other servers when negotiating a TLS +connection is verified for authenticity. Authenticating the certificate is not +enough to be sure about the server, so you should combine this with the +``verify_host`` option. + + .. _configuration-framework-http_method_override: + +http_method_override +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +This determines whether the ``_method`` request parameter is used as the +intended HTTP method on POST requests. If enabled, the +:method:`Request::enableHttpMethodParameterOverride ` +method gets called automatically. It becomes the service container parameter +named ``kernel.http_method_override``. + +.. seealso:: + + :ref:`Changing the Action and HTTP Method ` of + Symfony forms. + +.. warning:: + + If you're using the :ref:`HttpCache Reverse Proxy ` + with this option, the kernel will ignore the ``_method`` parameter, + which could lead to errors. + + To fix this, invoke the ``enableHttpMethodParameterOverride()`` method + before creating the ``Request`` object:: + + // public/index.php + + // ... + $kernel = new CacheKernel($kernel); + + Request::enableHttpMethodParameterOverride(); // <-- add this line + $request = Request::createFromGlobals(); + // ... + +.. _reference-framework-ide: + +ide +~~~ + +**type**: ``string`` **default**: ``%env(default::SYMFONY_IDE)%`` + +Symfony turns file paths seen in variable dumps and exception messages into +links that open those files right inside your browser. If you prefer to open +those files in your favorite IDE or text editor, set this option to any of the +following values: ``phpstorm``, ``sublime``, ``textmate``, ``macvim``, ``emacs``, +``atom`` and ``vscode``. + +.. note:: + + The ``phpstorm`` option is supported natively by PhpStorm on macOS and + Windows; Linux requires installing `phpstorm-url-handler`_. + +If you use another editor, the expected configuration value is a URL template +that contains an ``%f`` placeholder where the file path is expected and ``%l`` +placeholder for the line number (percentage signs (``%``) must be escaped by +doubling them to prevent Symfony from interpreting them as container parameters). + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + ide: 'myide://open?url=file://%%f&line=%%l' + + .. code-block:: xml + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->ide('myide://open?url=file://%%f&line=%%l'); + }; + +Since every developer uses a different IDE, the recommended way to enable this +feature is to configure it on a system level. First, you can define this option +in the ``SYMFONY_IDE`` environment variable, which Symfony reads automatically +when ``framework.ide`` config is not set. + +Another alternative is to set the ``xdebug.file_link_format`` option in your +``php.ini`` configuration file. The format to use is the same as for the +``framework.ide`` option, but without the need to escape the percent signs +(``%``) by doubling them: + +.. code-block:: ini + + // example for PhpStorm + xdebug.file_link_format="phpstorm://open?file=%f&line=%l" + + // example for PhpStorm with Jetbrains Toolbox + xdebug.file_link_format="jetbrains://phpstorm/navigate/reference?project=example&path=%f:%l" + + // example for Sublime Text + xdebug.file_link_format="subl://open?url=file://%f&line=%l" + +.. note:: + + If both ``framework.ide`` and ``xdebug.file_link_format`` are defined, + Symfony uses the value of the ``xdebug.file_link_format`` option. + +.. tip:: + + Setting the ``xdebug.file_link_format`` ini option works even if the Xdebug + extension is not enabled. + +.. tip:: + + When running your app in a container or in a virtual machine, you can tell + Symfony to map files from the guest to the host by changing their prefix. + This map should be specified at the end of the URL template, using ``&`` and + ``>`` as guest-to-host separators: + + .. code-block:: text + + // /path/to/guest/.../file will be opened + // as /path/to/host/.../file on the host + // and /var/www/app/ as /projects/my_project/ also + 'myide://%%f:%%l&/path/to/guest/>/path/to/host/&/var/www/app/>/projects/my_project/&...' + + // example for PhpStorm + 'phpstorm://open?file=%%f&line=%%l&/var/www/app/>/projects/my_project/' + +.. _reference-lock: + +lock +~~~~ + +**type**: ``string`` | ``array`` + +The default lock adapter. If not defined, the value is set to ``semaphore`` when +available, or to ``flock`` otherwise. Store's DSN are also allowed. + +.. _reference-lock-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` + +Whether to enable the support for lock or not. This setting is +automatically set to ``true`` when one of the child settings is configured. + +.. _reference-lock-resources: + +resources +......... + +**type**: ``array`` + +A map of lock stores to be created by the framework extension, with +the name as key and DSN or service id as value: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/lock.yaml + framework: + lock: '%env(LOCK_DSN)%' + + .. code-block:: xml + + + + + + + + %env(LOCK_DSN)% + + + + + .. code-block:: php + + // config/packages/lock.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->lock() + ->resource('default', [env('LOCK_DSN')]); + }; + +.. seealso:: + + For more details, see :doc:`/lock`. + +.. _reference-lock-resources-name: + +name +"""" + +**type**: ``prototype`` + +Name of the lock you want to create. + +mailer +~~~~~~ + +.. _mailer-dsn: + +dsn +... + +**type**: ``string`` **default**: ``null`` + +The DSN used by the mailer. When several DSN may be used, use +``transports`` option (see below) instead. + +envelope +........ + +recipients +"""""""""" + +**type**: ``array`` + +The "envelope recipient" which is used as the value of ``RCPT TO`` during the +the `SMTP session`_. This value overrides any other recipient set in the code. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mailer.yaml + framework: + mailer: + dsn: 'smtp://localhost:25' + envelope: + recipients: ['admin@symfony.com', 'lead@symfony.com'] + + .. code-block:: xml + + + + + + + + admin@symfony.com + lead@symfony.com + + + + + + .. code-block:: php + + // config/packages/mailer.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return static function (ContainerConfigurator $container): void { + $container->extension('framework', [ + 'mailer' => [ + 'dsn' => 'smtp://localhost:25', + 'envelope' => [ + 'recipients' => [ + 'admin@symfony.com', + 'lead@symfony.com', + ], + ], + ], + ]); + }; + +sender +"""""" + +**type**: ``string`` + +The "envelope sender" which is used as the value of ``MAIL FROM`` during the +`SMTP session`_. This value overrides any other sender set in the code. + +.. _mailer-headers: + +headers +....... + +**type**: ``array`` + +Headers to add to emails. The key (``name`` attribute in xml format) is the +header name and value the header value. + +.. seealso:: + + For more information, see :ref:`Configuring Emails Globally ` + +message_bus +........... + +**type**: ``string`` **default**: ``null`` or default bus if Messenger component is installed + +Service identifier of the message bus to use when using the +:doc:`Messenger component ` (e.g. ``messenger.default_bus``). + +transports +.......... + +**type**: ``array`` + +A :ref:`list of DSN ` that can be used by the +mailer. A transport name is the key and the dsn is the value. + +messenger +~~~~~~~~~ + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` + +Whether to enable or not Messenger. + +.. seealso:: + + For more details, see the :doc:`Messenger component ` + documentation. + +php_errors +~~~~~~~~~~ + +log +... + +**type**: ``boolean`` | ``int`` | ``array`` **default**: ``true`` + +Use the application logger instead of the PHP logger for logging PHP errors. +When an integer value is used, it defines a bitmask of PHP errors that will +be logged. Those integer values must be the same used in the +`error_reporting PHP option`_. The default log levels will be used for each +PHP error. +When a boolean value is used, ``true`` enables logging for all PHP errors +while ``false`` disables logging entirely. + +This option also accepts a map of PHP errors to log levels: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + php_errors: + log: + !php/const \E_DEPRECATED: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_USER_DEPRECATED: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_NOTICE: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_USER_NOTICE: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_STRICT: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_WARNING: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_USER_WARNING: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_COMPILE_WARNING: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_CORE_WARNING: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_USER_ERROR: !php/const Psr\Log\LogLevel::CRITICAL + !php/const \E_RECOVERABLE_ERROR: !php/const Psr\Log\LogLevel::CRITICAL + !php/const \E_COMPILE_ERROR: !php/const Psr\Log\LogLevel::CRITICAL + !php/const \E_PARSE: !php/const Psr\Log\LogLevel::CRITICAL + !php/const \E_ERROR: !php/const Psr\Log\LogLevel::CRITICAL + !php/const \E_CORE_ERROR: !php/const Psr\Log\LogLevel::CRITICAL + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + use Psr\Log\LogLevel; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->phpErrors()->log(\E_DEPRECATED, LogLevel::ERROR); + $framework->phpErrors()->log(\E_USER_DEPRECATED, LogLevel::ERROR); + // ... + }; + +throw +..... + +**type**: ``boolean`` **default**: ``%kernel.debug%`` + +Throw PHP errors as ``\ErrorException`` instances. The parameter +``debug.error_handler.throw_at`` controls the threshold. + +profiler +~~~~~~~~ + +collect +....... + +**type**: ``boolean`` **default**: ``true`` + +This option configures the way the profiler behaves when it is enabled. If set +to ``true``, the profiler collects data for all requests. If you want to only +collect information on-demand, you can set the ``collect`` flag to ``false`` and +activate the data collectors manually:: + + $profiler->enable(); + +collect_parameter +................. + +**type**: ``string`` **default**: ``null`` + +This specifies name of a query parameter, a body parameter or a request attribute +used to enable or disable collection of data by the profiler for each request. +Combine it with the ``collect`` option to enable/disable the profiler on demand: + +* If the ``collect`` option is set to ``true`` but this parameter exists in a + request and has any value other than ``true``, ``yes``, ``on`` or ``1``, the + request data will not be collected; +* If the ``collect`` option is set to ``false``, but this parameter exists in a + request and has value of ``true``, ``yes``, ``on`` or ``1``, the request data + will be collected. + +.. _collect_serializer_data: + +collect_serializer_data +....................... + +**type**: ``boolean`` **default**: ``true`` + +When this option is ``true``, all normalizers and encoders are +decorated by traceable implementations that collect profiling information about them. + +.. deprecated:: 7.3 + + Setting the ``collect_serializer_data`` option to ``false`` is deprecated + since Symfony 7.3. + +.. _profiler-dsn: + +dsn +... + +**type**: ``string`` **default**: ``file:%kernel.cache_dir%/profiler`` + +The DSN where to store the profiling information. + +.. _reference-profiler-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``false`` + +The profiler can be enabled by setting this option to ``true``. When you +install it using Symfony Flex, the profiler is enabled in the ``dev`` +and ``test`` environments. + +.. note:: + + The profiler works independently from the Web Developer Toolbar, see + the :doc:`WebProfilerBundle configuration ` + on how to disable/enable the toolbar. + +only_exceptions +............... + +**type**: ``boolean`` **default**: ``false`` + +When this is set to ``true``, the profiler will only be enabled when an +exception is thrown during the handling of the request. + +.. _only_master_requests: + +only_main_requests +.................. + +**type**: ``boolean`` **default**: ``false`` + +When this is set to ``true``, the profiler will only be enabled on the main +requests (and not on the subrequests). + +property_access +~~~~~~~~~~~~~~~ + +magic_call +.......... + +**type**: ``boolean`` **default**: ``false`` + +When enabled, the ``property_accessor`` service uses PHP's +:ref:`magic __call() method ` when +its ``getValue()`` method is called. + +magic_get +......... + +**type**: ``boolean`` **default**: ``true`` + +When enabled, the ``property_accessor`` service uses PHP's +:ref:`magic __get() method ` when +its ``getValue()`` method is called. + +magic_set +......... + +**type**: ``boolean`` **default**: ``true`` + +When enabled, the ``property_accessor`` service uses PHP's +:ref:`magic __set() method ` when +its ``setValue()`` method is called. + +throw_exception_on_invalid_index +................................ + +**type**: ``boolean`` **default**: ``false`` + +When enabled, the ``property_accessor`` service throws an exception when you +try to access an invalid index of an array. + +throw_exception_on_invalid_property_path +........................................ + +**type**: ``boolean`` **default**: ``true`` + +When enabled, the ``property_accessor`` service throws an exception when you +try to access an invalid property path of an object. + +property_info +~~~~~~~~~~~~~ + +.. _reference-property-info-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation + +with_constructor_extractor +.......................... + +**type**: ``boolean`` **default**: ``false`` + +Configures the ``property_info`` service to extract property information from the constructor arguments +using the :ref:`ConstructorExtractor `. + +.. versionadded:: 7.3 + + The ``with_constructor_extractor`` option was introduced in Symfony 7.3. + +rate_limiter +~~~~~~~~~~~~ + +.. _reference-rate-limiter-name: + +name +.... + +**type**: ``prototype`` + +Name of the rate limiter you want to create. + +lock_factory +"""""""""""" + +**type**: ``string`` **default:** ``lock.factory`` + +The service that is used to create a lock. The service has to be an instance of +the :class:`Symfony\\Component\\Lock\\LockFactory` class. + +policy +"""""" + +**type**: ``string`` **required** + +The name of the rate limiting algorithm to use. Example names are ``fixed_window``, +``sliding_window`` and ``no_limit``. See :ref:`Rate Limiter Policies `) +for more information. + +request +~~~~~~~ + +formats +....... + +**type**: ``array`` **default**: ``[]`` + +This setting is used to associate additional request formats (e.g. ``html``) +to one or more mime types (e.g. ``text/html``), which will allow you to use the +format & mime types to call +:method:`Request::getFormat($mimeType) ` or +:method:`Request::getMimeType($format) `. + +In practice, this is important because Symfony uses it to automatically set the +``Content-Type`` header on the ``Response`` (if you don't explicitly set one). +If you pass an array of mime types, the first will be used for the header. + +To configure a ``jsonp`` format: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + request: + formats: + jsonp: 'application/javascript' + + .. code-block:: xml + + + + + + + + + + application/javascript + + + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->request() + ->format('jsonp', 'application/javascript'); + }; + +router +~~~~~~ + +cache_dir +......... + +**type**: ``string`` **default**: ``%kernel.cache_dir%`` + +The directory where routing information will be cached. Can be set to +``~`` (``null``) to disable route caching. + +.. deprecated:: 7.1 + + Setting the ``cache_dir`` option is deprecated since Symfony 7.1. The routes + are now always cached in the ``%kernel.build_dir%`` directory. + +default_uri +........... + +**type**: ``string`` + +The default URI used to generate URLs in a non-HTTP context (see +:ref:`Generating URLs in Commands `). + +http_port +......... + +**type**: ``integer`` **default**: ``80`` + +The port for normal http requests (this is used when matching the scheme). + +https_port +.......... + +**type**: ``integer`` **default**: ``443`` + +The port for https requests (this is used when matching the scheme). + +resource +........ + +**type**: ``string`` **required** + +The path the main routing resource (e.g. a YAML file) that contains the +routes and imports the router should load. + +strict_requirements +................... + +**type**: ``mixed`` **default**: ``true`` + +Determines the routing generator behavior. When generating a route that +has specific :ref:`parameter requirements `, the generator +can behave differently in case the used parameters do not meet these requirements. + +The value can be one of: + +``true`` + Throw an exception when the requirements are not met; +``false`` + Disable exceptions when the requirements are not met and return ``''`` + instead; +``null`` + Disable checking the requirements (thus, match the route even when the + requirements don't match). + +``true`` is recommended in the development environment, while ``false`` +or ``null`` might be preferred in production. + +.. _reference-router-type: + +type +.... + +**type**: ``string`` + +The type of the resource to hint the loaders about the format. This isn't +needed when you use the default routers with the expected file extensions +(``.xml``, ``.yaml``, ``.php``). + +utf8 +.... + +**type**: ``boolean`` **default**: ``true`` + +When this option is set to ``true``, the regular expressions used in the +:ref:`requirements of route parameters ` will be run +using the `utf-8 modifier`_. This will for example match any UTF-8 character +when using ``.``, instead of matching only a single byte. + +If the charset of your application is UTF-8 (as defined in the +:ref:`getCharset() method ` of your kernel) it's +recommended setting it to ``true``. This will make non-UTF8 URLs to generate 404 +errors. + +.. _configuration-framework-secret: + +secret +~~~~~~ + +**type**: ``string`` **required** + +This is a string that should be unique to your application and it's commonly +used to add more entropy to security related operations. Its value should +be a series of characters, numbers and symbols chosen randomly and the +recommended length is around 32 characters. + +In practice, Symfony uses this value for encrypting the cookies used +in the :doc:`remember me functionality ` and for +creating signed URIs when using :ref:`ESI (Edge Side Includes) `. +That's why you should treat this value as if it were a sensitive credential and +**never make it public**. + +This option becomes the service container parameter named ``kernel.secret``, +which you can use whenever the application needs an immutable random string +to add more entropy. + +As with any other security-related parameter, it is a good practice to change +this value from time to time. However, keep in mind that changing this value +will invalidate all signed URIs and Remember Me cookies. That's why, after +changing this value, you should regenerate the application cache and log +out all the application users. + +secrets +~~~~~~~ + +decryption_env_var +.................. + +**type**: ``string`` **default**: ``base64:default::SYMFONY_DECRYPTION_SECRET`` + +The env var name that contains the vault decryption secret. By default, this +value will be decoded from base64. + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` + +Whether to enable or not secrets managements. + +local_dotenv_file +................. + +**type**: ``string`` **default**: ``%kernel.project_dir%/.env.%kernel.environment%.local`` + +The path to the local ``.env`` file. This file must contain the vault +decryption key, given by the ``decryption_env_var`` option. + +vault_directory +............... + +**type**: ``string`` **default**: ``%kernel.project_dir%/config/secrets/%kernel.runtime_environment%`` + +The directory to store the secret vault. By default, the path includes the value +of the :ref:`kernel.runtime_environment ` +parameter. + +semaphore +~~~~~~~~~ + +**type**: ``string`` | ``array`` + +The default semaphore adapter. Store's DSN are also allowed. + +.. _reference-semaphore-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` + +Whether to enable the support for semaphore or not. This setting is +automatically set to ``true`` when one of the child settings is configured. + +.. _reference-semaphore-resources: + +resources +......... + +**type**: ``array`` + +A map of semaphore stores to be created by the framework extension, with +the name as key and DSN or service id as value: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/semaphore.yaml + framework: + semaphore: '%env(SEMAPHORE_DSN)%' + + .. code-block:: xml + + + + + + + + %env(SEMAPHORE_DSN)% + + + + + .. code-block:: php + + // config/packages/semaphore.php + use function Symfony\Component\DependencyInjection\Loader\Configurator\env; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->semaphore() + ->resource('default', [env('SEMAPHORE_DSN')]); + }; + +.. _reference-semaphore-resources-name: + +name +"""" + +**type**: ``prototype`` + +Name of the semaphore you want to create. + +.. _configuration-framework-serializer: + +serializer +~~~~~~~~~~ + +.. _reference-serializer-circular_reference_handler: + +circular_reference_handler +.......................... + +**type** ``string`` + +The service id that is used as the circular reference handler of the default +serializer. The service has to implement the magic ``__invoke($object)`` +method. + +.. seealso:: + + For more information, see + :ref:`component-serializer-handling-circular-references`. + +default_context +............... + +**type**: ``array`` **default**: ``[]`` + +A map with default context options that will be used with each ``serialize`` and ``deserialize`` +call. This can be used for example to set the json encoding behavior by setting ``json_encode_options`` +to a `json_encode flags bitmask`_. + +You can inspect the :ref:`serializer context builders ` +to discover the available settings. + +.. _reference-serializer-enable_annotations: + +enable_attributes +................. + +**type**: ``boolean`` **default**: ``true`` + +Enables support for `PHP attributes`_ in the serializer component. + +.. seealso:: + + See :ref:`the reference ` for a list of supported annotations. + +.. _reference-serializer-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation + +Whether to enable the ``serializer`` service or not in the service container. + +.. _reference-serializer-mapping: + +mapping +....... + +.. _reference-serializer-mapping-paths: + +paths +""""" + +**type**: ``array`` **default**: ``[]`` + +This option allows to define an array of paths with files or directories where +the component will look for additional serialization files. + +.. _reference-serializer-name_converter: + +name_converter +.............. + +**type**: ``string`` + +The name converter to use. +The :class:`Symfony\\Component\\Serializer\\NameConverter\\CamelCaseToSnakeCaseNameConverter` +name converter can enabled by using the ``serializer.name_converter.camel_case_to_snake_case`` +value. + +.. seealso:: + + For more information, see :ref:`serializer-name-conversion`. + +.. _config-framework-session: + +session +~~~~~~~ + +cache_limiter +............. + +**type**: ``string`` **default**: ``0`` + +If set to ``0``, Symfony won't set any particular header related to the cache +and it will rely on ``php.ini``'s `session.cache_limiter`_ directive. + +Unlike the other session options, ``cache_limiter`` is set as a regular +:ref:`container parameter `: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + session.storage.options: + cache_limiter: 0 + + .. code-block:: xml + + + + + + + + 0 + + + + + .. code-block:: php + + // config/services.php + $container->setParameter('session.storage.options', [ + 'cache_limiter' => 0, + ]); + +Be aware that if you configure it, you'll have to set other session-related options +as parameters as well. + +cookie_domain +............. + +**type**: ``string`` + +This determines the domain to set in the session cookie. + +If not set, ``php.ini``'s `session.cookie_domain`_ directive will be relied on. + +cookie_httponly +............... + +**type**: ``boolean`` **default**: ``true`` + +This determines whether cookies should only be accessible through the HTTP +protocol. This means that the cookie won't be accessible by scripting +languages, such as JavaScript. This setting can effectively help to reduce +identity theft through :ref:`XSS attacks `. + +cookie_lifetime +............... + +**type**: ``integer`` + +This determines the lifetime of the session - in seconds. +Setting this value to ``0`` means the cookie is valid for +the length of the browser session. + +If not set, ``php.ini``'s `session.cookie_lifetime`_ directive will be relied on. + +cookie_path +........... + +**type**: ``string`` + +This determines the path to set in the session cookie. + +If not set, ``php.ini``'s `session.cookie_path`_ directive will be relied on. + +cookie_samesite +............... + +**type**: ``string`` or ``null`` **default**: ``null`` + +It controls the way cookies are sent when the HTTP request did not originate +from the same domain that is associated with the cookies. Setting this option is +recommended to mitigate `CSRF security attacks`_. + +By default, browsers send all cookies related to the domain of the HTTP request. +This may be a problem for example when you visit a forum and some malicious +comment includes a link like ``https://some-bank.com/?send_money_to=attacker&amount=1000``. +If you were previously logged into your bank website, the browser will send all +those cookies when making that HTTP request. + +The possible values for this option are: + +* ``null``, use ``php.ini``'s `session.cookie_samesite`_ directive. +* ``'none'`` (or the ``Symfony\Component\HttpFoundation\Cookie::SAMESITE_NONE`` constant), use it to allow + sending of cookies when the HTTP request originated from a different domain + (previously this was the default behavior of null, but in newer browsers ``'lax'`` + would be applied when the header has not been set) +* ``'strict'`` (or the ``Cookie::SAMESITE_STRICT`` constant), use it to never + send any cookie when the HTTP request did not originate from the same domain. +* ``'lax'`` (or the ``Cookie::SAMESITE_LAX`` constant), use it to allow sending + cookies when the request originated from a different domain, but only when the + user consciously made the request (by clicking a link or submitting a form + with the ``GET`` method). + +cookie_secure +............. + +**type**: ``boolean`` or ``'auto'`` + +This determines whether cookies should only be sent over secure connections. In +addition to ``true`` and ``false``, there's a special ``'auto'`` value that +means ``true`` for HTTPS requests and ``false`` for HTTP requests. + +If not set, ``php.ini``'s `session.cookie_secure`_ directive will be relied on. + +.. _reference-session-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` + +Whether to enable the session support in the framework. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + session: + enabled: true + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->session() + ->enabled(true); + }; + +gc_divisor +.......... + +**type**: ``integer`` + +See `gc_probability`_. + +If not set, ``php.ini``'s `session.gc_divisor`_ directive will be relied on. + +gc_maxlifetime +.............. + +**type**: ``integer`` + +This determines the number of seconds after which data will be seen as "garbage" +and potentially cleaned up. Garbage collection may occur during session +start and depends on `gc_divisor`_ and `gc_probability`_. + +If not set, ``php.ini``'s `session.gc_maxlifetime`_ directive will be relied on. + +gc_probability +.............. + +**type**: ``integer`` + +This defines the probability that the garbage collector (GC) process is +started on every session initialization. The probability is calculated by +using ``gc_probability`` / ``gc_divisor``, e.g. 1/100 means there is a 1% +chance that the GC process will start on each request. + +If not set, Symfony will use the value of the `session.gc_probability`_ directive +in the ``php.ini`` configuration file. + +.. versionadded:: 7.2 + + Relying on ``php.ini``'s directive as default for ``gc_probability`` was + introduced in Symfony 7.2. + +.. _config-framework-session-handler-id: + +handler_id +.......... + +**type**: ``string`` | ``null`` **default**: ``null`` + +If ``framework.session.save_path`` is not set, the default value of this option +is ``null``, which means to use the session handler configured in php.ini. If the +``framework.session.save_path`` option is set, then Symfony stores sessions using +the native file session handler. + +It is possible to :ref:`store sessions in a database `, +and also to configure the session handler with a DSN: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + session: + # a few possible examples + handler_id: 'redis://localhost' + handler_id: '%env(REDIS_URL)%' + handler_id: '%env(DATABASE_URL)%' + handler_id: 'file://%kernel.project_dir%/var/sessions' + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + use function Symfony\Component\DependencyInjection\Loader\Configurator\env; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + + $framework->session() + // a few possible examples + ->handlerId('redis://localhost') + ->handlerId(env('REDIS_URL')) + ->handlerId(env('DATABASE_URL')) + ->handlerId('file://%kernel.project_dir%/var/sessions'); + }; + +.. note:: + + Supported DSN protocols are the following: + + * ``file`` + * ``redis`` + * ``rediss`` (Redis over TLS) + * ``memcached`` (requires :doc:`symfony/cache `) + * ``pdo_oci`` (requires :doc:`doctrine/dbal `) + * ``mssql`` + * ``mysql`` + * ``mysql2`` + * ``pgsql`` + * ``postgres`` + * ``postgresql`` + * ``sqlsrv`` + * ``sqlite`` + * ``sqlite3`` + +.. _reference-session-metadata-update-threshold: + +metadata_update_threshold +......................... + +**type**: ``integer`` **default**: ``0`` + +This is how many seconds to wait between updating/writing the session metadata. +This can be useful if, for some reason, you want to limit the frequency at which +the session persists, instead of doing that on every request. + +.. _name: + +name +.... + +**type**: ``string`` + +This specifies the name of the session cookie. + +If not set, ``php.ini``'s `session.name`_ directive will be relied on. + +save_path +......... + +**type**: ``string`` | ``null`` **default**: ``%kernel.cache_dir%/sessions`` + +This determines the argument to be passed to the save handler. If you choose +the default file handler, this is the path where the session files are created. + +If ``null``, ``php.ini``'s `session.save_path`_ directive will be relied on: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + session: + save_path: ~ + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->session() + ->savePath(null); + }; + +sid_bits_per_character +...................... + +**type**: ``integer`` + +This determines the number of bits in the encoded session ID character. The possible +values are ``4`` (0-9, a-f), ``5`` (0-9, a-v), and ``6`` (0-9, a-z, A-Z, "-", ","). +The more bits results in stronger session ID. ``5`` is recommended value for +most environments. + +If not set, ``php.ini``'s `session.sid_bits_per_character`_ directive will be relied on. + +.. deprecated:: 7.2 + + The ``sid_bits_per_character`` option was deprecated in Symfony 7.2. No alternative + is provided as PHP 8.4 has deprecated the related option. + +sid_length +.......... + +**type**: ``integer`` + +This determines the length of session ID string, which can be an integer between +``22`` and ``256`` (both inclusive), ``32`` being the recommended value. Longer +session IDs are harder to guess. + +If not set, ``php.ini``'s `session.sid_length`_ directive will be relied on. + +.. deprecated:: 7.2 + + The ``sid_length`` option was deprecated in Symfony 7.2. No alternative is + provided as PHP 8.4 has deprecated the related option. + +.. _storage_id: + +storage_factory_id +.................. + +**type**: ``string`` **default**: ``session.storage.factory.native`` + +The service ID used for creating the ``SessionStorageInterface`` that stores +the session. This service is available in the Symfony application via the +``session.storage.factory`` service alias. The class has to implement +:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageFactoryInterface`. +To see a list of all available storages, run: + +.. code-block:: terminal + + $ php bin/console debug:container session.storage.factory. + +use_cookies +........... + +**type**: ``boolean`` + +This specifies if the session ID is stored on the client side using cookies or +not. + +If not set, ``php.ini``'s `session.use_cookies`_ directive will be relied on. + +ssi +~~~ + +enabled +....... + +**type**: ``boolean`` **default**: ``false`` + +Whether to enable or not SSI support in your application. + +.. _reference-framework-test: + +test +~~~~ + +**type**: ``boolean`` + +If this configuration setting is present (and not ``false``), then the services +related to testing your application (e.g. ``test.client``) are loaded. This +setting should be present in your ``test`` environment (usually via +``config/packages/test/framework.yaml``). + +.. seealso:: + + For more information, see :doc:`/testing`. + +translator +~~~~~~~~~~ + +cache_dir +......... + +**type**: ``string`` | ``null`` **default**: ``%kernel.cache_dir%/translations`` + +Defines the directory where the translation cache is stored. Use ``null`` to +disable this cache. + +.. _reference-translator-default_path: + +default_path +............ + +**type**: ``string`` **default**: ``%kernel.project_dir%/translations`` + +This option allows to define the path where the application translations files +are stored. + +.. _reference-translator-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation + +Whether or not to enable the ``translator`` service in the service container. + +.. _fallback: + +fallbacks +......... + +**type**: ``string|array`` **default**: value of `default_locale`_ + +This option is used when the translation key for the current locale wasn't +found. + +.. seealso:: + + For more details, see :doc:`/translation`. + +.. _reference-framework-translator-formatter: + +formatter +......... + +**type**: ``string`` **default**: ``translator.formatter.default`` + +The ID of the service used to format translation messages. The service class +must implement the :class:`Symfony\\Component\\Translation\\Formatter\\MessageFormatterInterface`. + +.. _reference-framework-translator-logging: + +logging +....... + +**default**: ``true`` when the debug mode is enabled, ``false`` otherwise. + +When ``true``, a log entry is made whenever the translator cannot find a translation +for a given key. The logs are made to the ``translation`` channel at the +``debug`` level for keys where there is a translation in the fallback +locale, and the ``warning`` level if there is no translation to use at all. + +.. _reference-translator-paths: + +paths +..... + +**type**: ``array`` **default**: ``[]`` + +This option allows to define an array of paths where the component will look +for translation files. The later a path is added, the more priority it has +(translations from later paths overwrite earlier ones). Translations from the +:ref:`default_path ` have more priority than +translations from all these paths. + +.. _reference-translator-providers: + +providers +......... + +**type**: ``array`` **default**: ``[]`` + +This option enables and configures :ref:`translation providers ` +to push and pull your translations to/from third party translation services. + +trust_x_sendfile_type_header +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%`` + +.. versionadded:: 7.2 + + In Symfony 7.2, the default value of this option was changed from ``false`` to the + value stored in the ``SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER`` environment variable. + +``X-Sendfile`` is a special HTTP header that tells web servers to replace the +response contents by the file that is defined in that header. This improves +performance because files are no longer served by your application but directly +by the web server. + +This configuration option determines whether to trust ``x-sendfile`` header for +BinaryFileResponse. If enabled, Symfony calls the +:method:`BinaryFileResponse::trustXSendfileTypeHeader ` +method automatically. It becomes the service container parameter named +``kernel.trust_x_sendfile_type_header``. + +.. _reference-framework-trusted-headers: + +trusted_headers +~~~~~~~~~~~~~~~ + +The ``trusted_headers`` option is needed to configure which client information +should be trusted (e.g. their host) when running Symfony behind a load balancer +or a reverse proxy. See :doc:`/deployment/proxies`. + +.. _configuration-framework-trusted-hosts: + +trusted_hosts +~~~~~~~~~~~~~ + +**type**: ``array`` | ``string`` **default**: ``['%env(default::SYMFONY_TRUSTED_HOSTS)%']`` + +.. versionadded:: 7.2 + + In Symfony 7.2, the default value of this option was changed from ``[]`` to the + value stored in the ``SYMFONY_TRUSTED_HOSTS`` environment variable. + +A lot of different attacks have been discovered relying on inconsistencies +in handling the ``Host`` header by various software (web servers, reverse +proxies, web frameworks, etc.). Basically, every time the framework is +generating an absolute URL (when sending an email to reset a password for +instance), the host might have been manipulated by an attacker. + +.. seealso:: + + You can read `HTTP Host header attacks`_ for more information about + these kinds of attacks. + +The Symfony :method:`Request::getHost() ` +method might be vulnerable to some of these attacks because it depends on +the configuration of your web server. One simple solution to avoid these +attacks is to configure a list of hosts that your Symfony application can respond +to. That's the purpose of this ``trusted_hosts`` option. If the incoming +request's hostname doesn't match one of the regular expressions in this list, +the application won't respond and the user will receive a 400 response. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + trusted_hosts: ['^example\.com$', '^example\.org$'] + + .. code-block:: xml + + + + + + + ^example\.com$ + ^example\.org$ + + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->trustedHosts(['^example\.com$', '^example\.org$']); + }; + +Hosts can also be configured to respond to any subdomain, via +``^(.+\.)?example\.com$`` for instance. + +In addition, you can also set the trusted hosts in the front controller +using the ``Request::setTrustedHosts()`` method:: + + // public/index.php + Request::setTrustedHosts(['^(.+\.)?example\.com$', '^(.+\.)?example\.org$']); + +The default value for this option is an empty array, meaning that the application +can respond to any given host. + +.. seealso:: + + Read more about this in the `Security Advisory Blog post`_. + +.. _reference-framework-trusted-proxies: + +trusted_proxies +~~~~~~~~~~~~~~~ + +The ``trusted_proxies`` option is needed to get precise information about the +client (e.g. their IP address) when running Symfony behind a load balancer or a +reverse proxy. See :doc:`/deployment/proxies`. + +.. _reference-validation: + +validation +~~~~~~~~~~ + +.. _reference-validation-auto-mapping: + +auto_mapping +............ + +**type**: ``array`` **default**: ``[]`` + +Defines the Doctrine entities that will be introspected to add +:ref:`automatic validation constraints ` to them: + +.. configuration-block:: + + .. code-block:: yaml + + framework: + validation: + auto_mapping: + # an empty array means that all entities that belong to that + # namespace will add automatic validation + 'App\Entity\': [] + 'Foo\': ['Foo\Some\Entity', 'Foo\Another\Entity'] + + .. code-block:: xml + + + + + + + + + + + Foo\Some\Entity + Foo\Another\Entity + + + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->validation() + ->autoMapping() + ->paths([ + 'App\\Entity\\' => [], + 'Foo\\' => ['Foo\\Some\\Entity', 'Foo\\Another\\Entity'], + ]); + }; + +.. _reference-validation-email_validation_mode: + +email_validation_mode +..................... + +**type**: ``string`` **default**: ``html5`` + +Sets the default value for the +:ref:`"mode" option of the Email validator `. + +.. _reference-validation-enable_annotations: + +enable_attributes +................. + +**type**: ``boolean`` **default**: ``true`` + +If this option is enabled, validation constraints can be defined using `PHP attributes`_. + +.. _reference-validation-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation + +Whether or not to enable validation support. + +This option will automatically be set to ``true`` when one of the child +settings is configured. + +.. _reference-validation-mapping: + +mapping +....... + +.. _reference-validation-mapping-paths: + +paths +""""" + +**type**: ``array`` **default**: ``['config/validation/']`` + +This option allows to define an array of paths with files or directories where +the component will look for additional validation files: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + validation: + mapping: + paths: + - "%kernel.project_dir%/config/validation/" + + .. code-block:: xml + + + + + + + + + %kernel.project_dir%/config/validation/ + + + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->validation() + ->mapping() + ->paths(['%kernel.project_dir%/config/validation/']); + }; + +.. _reference-validation-not-compromised-password: + +not_compromised_password +........................ + +The :doc:`NotCompromisedPassword ` +constraint makes HTTP requests to a public API to check if the given password +has been compromised in a data breach. + +static_method +............. + +**type**: ``string | array`` **default**: ``['loadValidatorMetadata']`` + +Defines the name of the static method which is called to load the validation +metadata of the class. You can define an array of strings with the names of +several methods. In that case, all of them will be called in that order to load +the metadata. + +translation_domain +.................. + +**type**: ``string | false`` **default**: ``validators`` + +The translation domain that is used when translating validation constraint +error messages. Use false to disable translations. + + +.. _reference-validation-not-compromised-password-enabled: + +enabled +""""""" + +**type**: ``boolean`` **default**: ``true`` + +If you set this option to ``false``, no HTTP requests will be made and the given +password will be considered valid. This is useful when you don't want or can't +make HTTP requests, such as in ``dev`` and ``test`` environments or in +continuous integration servers. + +endpoint +"""""""" + +**type**: ``string`` **default**: ``null`` + +By default, the :doc:`NotCompromisedPassword ` +constraint uses the public API provided by `haveibeenpwned.com`_. This option +allows to define a different, but compatible, API endpoint to make the password +checks. It's useful for example when the Symfony application is run in an +intranet without public access to the internet. + +web_link +~~~~~~~~ + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation + +Adds a `Link HTTP header`_ to the response. + +webhook +~~~~~~~ + +The ``webhook`` option (and its children) are used to configure the webhooks +defined in your application. Read more about the options in the :ref:`Webhook documentation `. + +workflows +~~~~~~~~~ + +**type**: ``array`` + +A list of workflows to be created by the framework extension: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/workflow.yaml + framework: + workflows: + my_workflow: + # ... + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/workflow.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->workflows() + ->workflows('my_workflow') + // ... + ; + }; + +.. seealso:: + + See also the article about :doc:`using workflows in Symfony applications `. + +.. _reference-workflows-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``false`` + +Whether to enable the support for workflows or not. This setting is +automatically set to ``true`` when one of the child settings is configured. + +.. _reference-workflows-name: + +name +.... + +**type**: ``prototype`` + +Name of the workflow you want to create. + +audit_trail +""""""""""" + +**type**: ``boolean`` + +If set to ``true``, the :class:`Symfony\\Component\\Workflow\\EventListener\\AuditTrailListener` +will be enabled. + +initial_marking +""""""""""""""" + +**type**: ``string`` | ``array`` + +One of the ``places`` or ``empty``. If not null and the supported object is not +already initialized via the workflow, this place will be set. + +marking_store +""""""""""""" + +**type**: ``array`` + +Each marking store can define any of these options: + +* ``property`` (**type**: ``string`` **default**: ``marking``) +* ``service`` (**type**: ``string``) +* ``type`` (**type**: ``string`` **allow value**: ``'method'``) + +metadata +"""""""" + +**type**: ``array`` + +Metadata available for the workflow configuration. +Note that ``places`` and ``transitions`` can also have their own +``metadata`` entry. + +places +"""""" + +**type**: ``array`` + +All available places (**type**: ``string``) for the workflow configuration. + +supports +"""""""" + +**type**: ``string`` | ``array`` + +The FQCN (fully-qualified class name) of the object supported by the workflow +configuration or an array of FQCN if multiple objects are supported. + +support_strategy +"""""""""""""""" + +**type**: ``string`` + +transitions +""""""""""" + +**type**: ``array`` + +Each marking store can define any of these options: + +* ``from`` (**type**: ``string`` or ``array``) value from the ``places``, + multiple values are allowed for both ``workflow`` and ``state_machine``; +* ``guard`` (**type**: ``string``) an :doc:`ExpressionLanguage ` + compatible expression to block the transition; +* ``name`` (**type**: ``string``) the name of the transition; +* ``to`` (**type**: ``string`` or ``array``) value from the ``places``, + multiple values are allowed only for ``workflow``. + +.. _reference-workflows-type: + +type +"""" + +**type**: ``string`` **possible values**: ``'workflow'`` or ``'state_machine'`` + +Defines the kind of workflow that is going to be created, which can be either +a normal workflow or a state machine. Read :doc:`this article ` +to know their differences. + +.. _`HTTP Host header attacks`: https://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html +.. _`Security Advisory Blog post`: https://symfony.com/blog/security-releases-symfony-2-0-24-2-1-12-2-2-5-and-2-3-3-released#cve-2013-4752-request-gethost-poisoning +.. _`phpstorm-url-handler`: https://github.com/sanduhrs/phpstorm-url-handler +.. _`blue/green deployment`: https://martinfowler.com/bliki/BlueGreenDeployment.html +.. _`gulp-rev`: https://www.npmjs.com/package/gulp-rev +.. _`webpack-manifest-plugin`: https://www.npmjs.com/package/webpack-manifest-plugin +.. _`json_encode flags bitmask`: https://www.php.net/json_encode +.. _`error_reporting PHP option`: https://www.php.net/manual/en/errorfunc.configuration.php#ini.error-reporting +.. _`CSRF security attacks`: https://en.wikipedia.org/wiki/Cross-site_request_forgery +.. _`X-Robots-Tag HTTP header`: https://developers.google.com/search/reference/robots_meta_tag +.. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt +.. _`default_socket_timeout`: https://www.php.net/manual/en/filesystem.configuration.php#ini.default-socket-timeout +.. _`PEM formatted`: https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail +.. _`haveibeenpwned.com`: https://haveibeenpwned.com/ +.. _`session.name`: https://www.php.net/manual/en/session.configuration.php#ini.session.name +.. _`session.cookie_lifetime`: https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-lifetime +.. _`session.cookie_path`: https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-path +.. _`session.cache_limiter`: https://www.php.net/manual/en/session.configuration.php#ini.session.cache-limiter +.. _`session.cookie_domain`: https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-domain +.. _`session.cookie_samesite`: https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-samesite +.. _`session.cookie_secure`: https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-secure +.. _`session.gc_divisor`: https://www.php.net/manual/en/session.configuration.php#ini.session.gc-divisor +.. _`session.gc_probability`: https://www.php.net/manual/en/session.configuration.php#ini.session.gc-probability +.. _`session.gc_maxlifetime`: https://www.php.net/manual/en/session.configuration.php#ini.session.gc-maxlifetime +.. _`session.sid_length`: https://www.php.net/manual/en/session.configuration.php#ini.session.sid-length +.. _`session.sid_bits_per_character`: https://www.php.net/manual/en/session.configuration.php#ini.session.sid-bits-per-character +.. _`session.save_path`: https://www.php.net/manual/en/session.configuration.php#ini.session.save-path +.. _`session.use_cookies`: https://www.php.net/manual/en/session.configuration.php#ini.session.use-cookies +.. _`Microsoft NTLM authentication protocol`: https://docs.microsoft.com/en-us/windows/win32/secauthn/microsoft-ntlm +.. _`utf-8 modifier`: https://www.php.net/reference.pcre.pattern.modifiers +.. _`Link HTTP header`: https://tools.ietf.org/html/rfc5988 +.. _`SMTP session`: https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_transport_example +.. _`PHP attributes`: https://www.php.net/manual/en/language.attributes.overview.php diff --git a/reference/configuration/kernel.rst b/reference/configuration/kernel.rst index 24566753ebb..b7596182906 100644 --- a/reference/configuration/kernel.rst +++ b/reference/configuration/kernel.rst @@ -1,99 +1,370 @@ -.. index:: - single: Configuration reference; Kernel class +Configuring in the Kernel +========================= -Configuring in the Kernel (e.g. AppKernel) -========================================== +Symfony applications define a kernel class (which is located by default at +``src/Kernel.php``) that includes several configurable options. This article +explains how to configure those options and shows the list of container parameters +created by Symfony based on that configuration. -Some configuration can be done on the kernel class itself (usually called -``app/AppKernel.php``). You can do this by overriding specific methods in -the parent :class:`Symfony\\Component\\HttpKernel\\Kernel` class. +.. _configuration-kernel-build-directory: -Configuration -------------- +``kernel.build_dir`` +-------------------- -* `Charset`_ -* `Kernel Name`_ -* `Root Directory`_ -* `Cache Directory`_ -* `Log Directory`_ +**type**: ``string`` **default**: ``$this->getCacheDir()`` -.. versionadded:: 2.1 - The :method:`Symfony\\Component\\HttpKernel\\Kernel::getCharset` method is new - in Symfony 2.1 +This parameter stores the absolute path of a build directory of your Symfony application. +This directory can be used to separate read-only cache (i.e. the compiled container) +from read-write cache (i.e. :doc:`cache pools `). Specify a non-default +value when the application is deployed in a read-only filesystem like a Docker +container or AWS Lambda. -Charset -~~~~~~~ +This value is also exposed via the :method:`Symfony\\Component\\HttpKernel\\Kernel::getBuildDir` +method of the kernel class, which you can override to return a different value. + +You can also change the build directory by defining an environment variable +named ``APP_BUILD_DIR`` whose value is the absolute path of the build folder. + +``kernel.bundles`` +------------------ + +**type**: ``array`` **default**: ``[]`` + +This parameter stores the list of :doc:`bundles ` registered in the +application and the FQCN of their main bundle class:: + + [ + 'FrameworkBundle' => 'Symfony\Bundle\FrameworkBundle\FrameworkBundle', + 'TwigBundle' => 'Symfony\Bundle\TwigBundle\TwigBundle', + // ... + ] + +This value is also exposed via the :method:`Symfony\\Component\\HttpKernel\\Kernel::getBundles` +method of the kernel class. + +``kernel.bundles_metadata`` +--------------------------- + +**type**: ``array`` **default**: ``[]`` + +This parameter stores the list of :doc:`bundles ` registered in the +application and some metadata about them:: + + [ + 'FrameworkBundle' => [ + 'path' => '//vendor/symfony/framework-bundle', + 'namespace' => 'Symfony\Bundle\FrameworkBundle', + ], + 'TwigBundle' => [ + 'path' => '//vendor/symfony/twig-bundle', + 'namespace' => 'Symfony\Bundle\TwigBundle', + ], + // ... + ] + +This value is not exposed via any method of the kernel class, so you can only +obtain it via the container parameter. + +``kernel.cache_dir`` +-------------------- + +**type**: ``string`` **default**: ``$this->getProjectDir()/var/cache/$this->environment`` + +This parameter stores the absolute path of the cache directory of your Symfony +application. The default value is generated by Symfony based on the current +:ref:`configuration environment `. Your application +can write data to this path at runtime. + +This value is also exposed via the :method:`Symfony\\Component\\HttpKernel\\Kernel::getCacheDir` +method of the kernel class, which you can override to return a different value. + +.. _configuration-kernel-charset: + +``kernel.charset`` +------------------ **type**: ``string`` **default**: ``UTF-8`` -This returns the charset that is used in the application. To change it, override the -:method:`Symfony\\Component\\HttpKernel\\Kernel::getCharset` method and return another -charset, for instance:: +This parameter stores the type of charset or `character encoding`_ that is used +in the application. This value is also exposed via the :method:`Symfony\\Component\\HttpKernel\\Kernel::getCharset` +method of the kernel class, which you can override to return a different value:: - // app/AppKernel.php + // src/Kernel.php + namespace App; + use Symfony\Component\HttpKernel\Kernel as BaseKernel; // ... - class AppKernel extends Kernel + + class Kernel extends BaseKernel { - public function getCharset() + public function getCharset(): string { return 'ISO-8859-1'; } } -Kernel Name -~~~~~~~~~~~ +``kernel.container_build_time`` +------------------------------- + +**type**: ``string`` **default**: the result of executing ``time()`` + +Symfony follows the `reproducible builds`_ philosophy, which ensures that the +result of compiling the exact same source code doesn't produce different +results. This helps checking that a given binary or executable code was compiled +from some trusted source code. + +In practice, the compiled :doc:`service container ` of your +application will always be the same if you don't change its source code. This is +exposed via these container parameters: -**type**: ``string`` **default**: ``app`` (i.e. the directory name holding the kernel class) +* ``container.build_hash``, a hash of the contents of all your source files; +* ``container.build_time``, a timestamp of the moment when the container was + built (the result of executing PHP's :phpfunction:`time` function); +* ``container.build_id``, the result of merging the two previous parameters and + encoding the result using CRC32. -To change this setting, override the :method:`Symfony\\Component\\HttpKernel\\Kernel::getName` -method. Alternatively, move your kernel into a different directory. For example, -if you moved the kernel into a ``foo`` directory (instead of ``app``), the -kernel name will be ``foo``. +Since the ``container.build_time`` value will change every time you compile the +application, the build will not be strictly reproducible. If you care about +this, the solution is to use another container parameter called +``kernel.container_build_time`` and set it to a non-changing build time to +achieve a strict reproducible build: -The name of the kernel isn't usually directly important - it's used in the -generation of cache files. If you have an application with multiple kernels, -the easiest way to make each have a unique name is to duplicate the ``app`` -directory and rename it to something else (e.g. ``foo``). +.. configuration-block:: -Root Directory -~~~~~~~~~~~~~~ + .. code-block:: yaml -**type**: ``string`` **default**: the directory of ``AppKernel`` + # config/services.yaml + parameters: + # ... + kernel.container_build_time: '1234567890' -This returns the root directory of your kernel. If you use the Symfony Standard -edition, the root directory refers to the ``app`` directory. + .. code-block:: xml -To change this setting, override the -:method:`Symfony\\Component\\HttpKernel\\Kernel::getRootDir` method:: + + + - // app/AppKernel.php + + + 1234567890 + + + .. code-block:: php + + // config/services.php + + // ... + $container->setParameter('kernel.container_build_time', '1234567890'); + +``kernel.container_class`` +-------------------------- + +**type**: ``string`` **default**: (see explanation below) + +This parameter stores a unique identifier for the container class. In practice, +this is only important to ensure that each kernel has a unique identifier when +:doc:`using applications with multiple kernels `. + +The default value is generated by Symfony based on the current +:ref:`configuration environment ` and the +:ref:`debug mode `. For example, if your application kernel is +defined in the ``App`` namespace, runs in the ``dev`` environment and the ``debug`` +mode is enabled, the value of this parameter is ``App_KernelDevDebugContainer``. + +This value is also exposed via the :method:`Symfony\\Component\\HttpKernel\\Kernel::getContainerClass` +method of the kernel class, which you can override to return a different value:: + + // src/Kernel.php + namespace App; + + use Symfony\Component\HttpKernel\Kernel as BaseKernel; // ... - class AppKernel extends Kernel + + class Kernel extends BaseKernel + { + public function getContainerClass(): string + { + return sprintf('AcmeKernel%s', random_int(10_000, 99_999)); + } + } + +``kernel.debug`` +---------------- + +**type**: ``boolean`` **default**: (the value is passed as an argument when booting the kernel) + +This parameter stores the value of the current :ref:`debug mode ` +used by the application. + +``kernel.default_locale`` +------------------------- + +This parameter stores the value of +:ref:`the framework.default_locale parameter `. + +``kernel.enabled_locales`` +-------------------------- + +This parameter stores the value of +:ref:`the framework.enabled_locales parameter `. + +.. _configuration-kernel-environment: + +``kernel.environment`` +---------------------- + +**type**: ``string`` **default**: (the value is passed as an argument when booting the kernel) + +This parameter stores the name of the current :ref:`configuration environment ` +used by the application. + +This value defines the configuration options used to run the application, whereas +the :ref:`kernel.runtime_environment ` +option defines the place where the application is deployed. This allows for +example to run an application with the ``prod`` config (``kernel.environment``) +in different scenarios like ``staging`` or ``production`` (``kernel.runtime_environment``). + +``kernel.error_controller`` +--------------------------- + +This parameter stores the value of +:ref:`the framework.error_controller parameter `. + +``kernel.http_method_override`` +------------------------------- + +This parameter stores the value of +:ref:`the framework.http_method_override parameter `. + +``kernel.logs_dir`` +------------------- + +**type**: ``string`` **default**: ``$this->getProjectDir()/var/log`` + +This parameter stores the absolute path of the log directory of your Symfony application. +It's calculated automatically based on the current +:ref:`configuration environment `. + +This value is also exposed via the :method:`Symfony\\Component\\HttpKernel\\Kernel::getLogDir` +method of the kernel class, which you can override to return a different value. + +.. _configuration-kernel-project-directory: + +``kernel.project_dir`` +---------------------- + +**type**: ``string`` **default**: the directory of the project's ``composer.json`` + +This parameter stores the absolute path of the root directory of your Symfony application, +which is used by applications to perform operations with file paths relative to +the project's root directory. + +By default, its value is calculated automatically as the directory where the +main ``composer.json`` file is stored. This value is also exposed via the +:method:`Symfony\\Component\\HttpKernel\\Kernel::getProjectDir` method of the +kernel class. + +If you don't use Composer, or have moved the ``composer.json`` file location or +have deleted it entirely (for example in the production servers), override the +``getProjectDir()`` method to return a different value:: + + // src/Kernel.php + namespace App; + + use Symfony\Component\HttpKernel\Kernel as BaseKernel; + // ... + + class Kernel extends BaseKernel { // ... - public function getRootDir() + public function getProjectDir(): string { - return realpath(parent::getRootDir().'/../'); + // when defining a hardcoded string, don't add the trailing slash to the path + // e.g. '/home/user/my_project', '/app', '/var/www/example.com' + return \dirname(__DIR__); } } -Cache Directory -~~~~~~~~~~~~~~~ +.. _configuration-kernel-runtime-environment: + +``kernel.runtime_environment`` +------------------------------ + +**type**: ``string`` **default**: ``%env(default:kernel.environment:APP_RUNTIME_ENV)%`` + +This parameter stores the name of the current :doc:`runtime environment ` +used by the application. + +This value defines the place where the application is deployed, whereas the +:ref:`kernel.environment ` option defines +the configuration options used to run the application. This allows for example +to run an application with the ``prod`` config (``kernel.environment``) in different +scenarios like ``staging`` or ``production`` (``kernel.runtime_environment``). + +``kernel.runtime_mode`` +----------------------- + +**type**: ``string`` **default**: ``%env(query_string:default:container.runtime_mode:APP_RUNTIME_MODE)%`` + +This parameter stores a query string of the current runtime mode used by the +application. For example, the query string looks like ``web=1&worker=0`` when +the application is running in web mode and ``web=1&worker=1`` when running in +a long-running web server. This parameter can be set by using the +``APP_RUNTIME_MODE`` env var. + +``kernel.runtime_mode.web`` +--------------------------- + +**type**: ``boolean`` **default**: ``%env(bool:default::key:web:default:kernel.runtime_mode:)%`` + +Whether the application is running in a web environment. + +``kernel.runtime_mode.cli`` +--------------------------- + +**type**: ``boolean`` **default**: ``%env(not:default:kernel.runtime_mode.web:)%`` + +Whether the application is running in a CLI environment. By default, +this value is the opposite of the ``kernel.runtime_mode.web`` parameter. + +``kernel.runtime_mode.worker`` +------------------------------ + +**type**: ``boolean`` **default**: ``%env(bool:default::key:worker:default:kernel.runtime_mode:)%`` + +Whether the application is running in a worker/long-running environment. Not all web +servers support it, and you have to use a long-running web server like `FrankenPHP`_. + +``kernel.secret`` +----------------- + +**type**: ``string`` **default**: ``%env(APP_SECRET)%`` + +This parameter stores the value of +:ref:`the framework.secret parameter `. + +``kernel.trust_x_sendfile_type_header`` +--------------------------------------- + +This parameter stores the value of +:ref:`the framework.trust_x_sendfile_type_header parameter `. -**type**: ``string`` **default**: ``$this->rootDir/cache/$this->environment`` +``kernel.trusted_hosts`` +------------------------ -This returns the path to the cache directory. To change it, override the -:method:`Symfony\\Component\\HttpKernel\\Kernel::getCacheDir` method. Read -":ref:`override-cache-dir`" for more information. +This parameter stores the value of +:ref:`the framework.trusted_hosts parameter `. -Log Directory -~~~~~~~~~~~~~ +``kernel.trusted_proxies`` +-------------------------- -**type**: ``string`` **default**: ``$this->rootDir/logs`` +This parameter stores the value of +:ref:`the framework.trusted_proxies parameter `. -This returns the path to the log directory. To change it, override the -:method:`Symfony\\Component\\HttpKernel\\Kernel::getLogDir` method. Read -":ref:`override-logs-dir`" for more information. +.. _`character encoding`: https://en.wikipedia.org/wiki/Character_encoding +.. _`reproducible builds`: https://en.wikipedia.org/wiki/Reproducible_builds +.. _`FrankenPHP`: https://frankenphp.dev diff --git a/reference/configuration/monolog.rst b/reference/configuration/monolog.rst index 266b4e56a8f..acabb02af57 100644 --- a/reference/configuration/monolog.rst +++ b/reference/configuration/monolog.rst @@ -1,98 +1,33 @@ -.. index:: - pair: Monolog; Configuration reference +Logging Configuration Reference (MonologBundle) +=============================================== -Monolog Configuration Reference -=============================== +The MonologBundle integrates the Monolog :doc:`logging ` library in +Symfony applications. All these options are configured under the ``monolog`` key +in your application configuration. -.. configuration-block:: +.. code-block:: terminal - .. code-block:: yaml + # displays the default config values defined by Symfony + $ php bin/console config:dump-reference monolog - monolog: - handlers: + # displays the actual config values used by your application + $ php bin/console debug:config monolog - # Examples: - syslog: - type: stream - path: /var/log/symfony.log - level: ERROR - bubble: false - formatter: my_formatter - processors: - - some_callable - main: - type: fingers_crossed - action_level: WARNING - buffer_size: 30 - handler: custom - custom: - type: service - id: my_handler - - # Default options and values for some "my_custom_handler" - my_custom_handler: - type: ~ # Required - id: ~ - priority: 0 - level: DEBUG - bubble: true - path: "%kernel.logs_dir%/%kernel.environment%.log" - ident: false - facility: user - max_files: 0 - action_level: WARNING - activation_strategy: ~ - stop_buffering: true - buffer_size: 0 - handler: ~ - members: [] - channels: - type: ~ - elements: ~ - from_email: ~ - to_email: ~ - subject: ~ - email_prototype: - id: ~ # Required (when the email_prototype is used) - method: ~ - channels: - type: ~ - elements: [] - formatter: ~ +.. note:: - .. code-block:: xml + When using XML, you must use the ``http://symfony.com/schema/dic/monolog`` + namespace and the related XSD schema is available at: + ``https://symfony.com/schema/dic/monolog/monolog-1.0.xsd`` - +.. tip:: - - - - - - + For a full list of handler types and related configuration options, see + `Monolog Configuration`_. .. note:: When the profiler is enabled, a handler is added to store the logs' messages in the profiler. The profiler uses the name "debug" so it is reserved and cannot be used in the configuration. + +.. _`Monolog Configuration`: https://github.com/symfony/monolog-bundle/blob/master/DependencyInjection/Configuration.php diff --git a/reference/configuration/security.rst b/reference/configuration/security.rst index 9d208d605e2..6f4fcd8db33 100644 --- a/reference/configuration/security.rst +++ b/reference/configuration/security.rst @@ -1,375 +1,875 @@ -.. index:: - single: Security; Configuration reference +Security Configuration Reference (SecurityBundle) +================================================= -Security Configuration Reference -================================ +The SecurityBundle integrates the :doc:`Security component ` +in Symfony applications. All these options are configured under the ``security`` +key in your application configuration. -The security system is one of the most powerful parts of Symfony2, and can -largely be controlled via its configuration. +.. code-block:: terminal -Full Default Configuration --------------------------- + # displays the default config values defined by Symfony + $ php bin/console config:dump-reference security -The following is the full default configuration for the security system. -Each part will be explained in the next section. + # displays the actual config values used by your application + $ php bin/console debug:config security + +.. note:: + + When using XML, you must use the ``http://symfony.com/schema/dic/security`` + namespace and the related XSD schema is available at: + ``https://symfony.com/schema/dic/services/services-1.0.xsd`` + +**Basic Options**: + +* `access_denied_url`_ +* `erase_credentials`_ +* `hide_user_not_found`_ +* `session_fixation_strategy`_ + +**Advanced Options**: + +Some of these options define tens of sub-options and they are explained in +separate articles: + +* `access_control`_ +* :ref:`hashers ` +* `firewalls`_ +* `providers`_ +* `role_hierarchy`_ + +access_denied_url +----------------- + +**type**: ``string`` **default**: ``null`` + +Defines the URL where the user is redirected after a ``403`` HTTP error (unless +you define a custom access denial handler). Example: ``/no-permission`` + +erase_credentials +----------------- + +**type**: ``boolean`` **default**: ``true`` + +If ``true``, the ``eraseCredentials()`` method of the user object is called +after authentication. + +hide_user_not_found +------------------- + +**type**: ``boolean`` **default**: ``true`` + +If ``true``, when a user is not found a generic exception of type +:class:`Symfony\\Component\\Security\\Core\\Exception\\BadCredentialsException` +is thrown with the message "Bad credentials". + +If ``false``, the exception thrown is of type +:class:`Symfony\\Component\\Security\\Core\\Exception\\UserNotFoundException` +and it includes the given not found user identifier. + +session_fixation_strategy +------------------------- + +**type**: ``string`` **default**: ``SessionAuthenticationStrategy::MIGRATE`` + +`Session Fixation`_ is a security attack that permits an attacker to hijack a +valid user session. Applications that don't assign new session IDs when +authenticating users are vulnerable to this attack. + +The possible values of this option are: + +* ``NONE`` constant from :class:`Symfony\\Component\\Security\\Http\\Session\\SessionAuthenticationStrategy` + Don't change the session after authentication. This is **not recommended**. +* ``MIGRATE`` constant from :class:`Symfony\\Component\\Security\\Http\\Session\\SessionAuthenticationStrategy` + The session ID is updated, but the rest of session attributes are kept. +* ``INVALIDATE`` constant from :class:`Symfony\\Component\\Security\\Http\\Session\\SessionAuthenticationStrategy` + The entire session is regenerated, so the session ID is updated but all the + other session attributes are lost. + +access_control +-------------- + +Defines the security protection of the URLs of your application. It's used for +example to trigger the user authentication when trying to access to the backend +and to allow unauthenticated users to the login form page. + +This option is explained in detail in :doc:`/security/access_control`. + +firewalls +--------- + +This is arguably the most important option of the security config file. It +defines the authentication mechanism used for each URL (or URL pattern) of your +application: .. configuration-block:: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: - access_denied_url: ~ # Example: /foo/error403 - - # strategy can be: none, migrate, invalidate - session_fixation_strategy: migrate - hide_user_not_found: true - always_authenticate_before_granting: false - erase_credentials: true - access_decision_manager: - strategy: affirmative - allow_if_all_abstain: false - allow_if_equal_granted_denied: true - acl: - - # any name configured in doctrine.dbal section - connection: ~ - cache: - id: ~ - prefix: sf2_acl_ - provider: ~ - tables: - class: acl_classes - entry: acl_entries - object_identity: acl_object_identities - object_identity_ancestors: acl_object_identity_ancestors - security_identity: acl_security_identities - voter: - allow_if_object_identity_unavailable: true - - encoders: - # Examples: - Acme\DemoBundle\Entity\User1: sha512 - Acme\DemoBundle\Entity\User2: - algorithm: sha512 - encode_as_base64: true - iterations: 5000 - - # PBKDF2 encoder - # see the note about PBKDF2 below for details on security and speed - Acme\Your\Class\Name: - algorithm: pbkdf2 - hash_algorithm: sha512 - encode_as_base64: true - iterations: 1000 - - # Example options/values for what a custom encoder might look like - Acme\DemoBundle\Entity\User3: - id: my.encoder.id - - providers: # Required - # Examples: - my_in_memory_provider: - memory: - users: - foo: - password: foo - roles: ROLE_USER - bar: - password: bar - roles: [ROLE_USER, ROLE_ADMIN] - - my_entity_provider: - entity: - class: SecurityBundle:User - property: username - - # Example custom provider - my_some_custom_provider: - id: ~ - - # Chain some providers - my_chain_provider: - chain: - providers: [ my_in_memory_provider, my_entity_provider ] - - firewalls: # Required - # Examples: - somename: - pattern: .* - request_matcher: some.service.id - access_denied_url: /foo/error403 - access_denied_handler: some.service.id - entry_point: some.service.id - provider: some_key_from_above - # manages where each firewall stores session information - # See "Firewall Context" below for more details - context: context_key - stateless: false + # ... + firewalls: + # 'main' is the name of the firewall (can be chosen freely) + main: + # 'pattern' is a regular expression matched against the incoming + # request URL. If there's a match, authentication is triggered + pattern: ^/admin + # the rest of options depend on the authentication mechanism + # ... + + .. code-block:: xml + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + // 'main' is the name of the firewall (can be chosen freely) + $security->firewall('main') + // 'pattern' is a regular expression matched against the incoming + // request URL. If there's a match, authentication is triggered + ->pattern('^/admin') + // the rest of options depend on the authentication mechanism + // ... + ; + }; + +.. seealso:: + + Read :doc:`this article ` to learn about how + to restrict firewalls by host and HTTP methods. + +In addition to some common config options, the most important firewall options +depend on the authentication mechanism, which can be any of these: + +.. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + firewalls: + main: + # ... x509: - provider: some_key_from_above + # ... + remote_user: + # ... + guard: + # ... + form_login: + # ... + form_login_ldap: + # ... + json_login: + # ... http_basic: - provider: some_key_from_above + # ... + http_basic_ldap: + # ... http_digest: - provider: some_key_from_above - form_login: - # submit the login form here - check_path: /login_check - - # the user is redirected here when he/she needs to login - login_path: /login - - # if true, forward the user to the login form instead of redirecting - use_forward: false - - # login success redirecting options (read further below) - always_use_default_target_path: false - default_target_path: / - target_path_parameter: _target_path - use_referer: false - - # login failure redirecting options (read further below) - failure_path: /foo - failure_forward: false - failure_path_parameter: _failure_path - failure_handler: some.service.id - success_handler: some.service.id - - # field names for the username and password fields - username_parameter: _username - password_parameter: _password - - # csrf token options - csrf_parameter: _csrf_token - intention: authenticate - csrf_provider: my.csrf_provider.id - - # by default, the login form *must* be a POST, not a GET - post_only: true - remember_me: false - remember_me: - token_provider: name - key: someS3cretKey - name: NameOfTheCookie - lifetime: 3600 # in seconds - path: /foo - domain: somedomain.foo - secure: false - httponly: true - always_remember_me: false - remember_me_parameter: _remember_me - logout: - path: /logout - target: / - invalidate_session: false - delete_cookies: - a: { path: null, domain: null } - b: { path: null, domain: null } - handlers: [some.service.id, another.service.id] - success_handler: some.service.id - anonymous: ~ - - # Default values and options for any firewall - some_firewall_listener: - pattern: ~ - security: true - request_matcher: ~ - access_denied_url: ~ - access_denied_handler: ~ - entry_point: ~ - provider: ~ - stateless: false - context: ~ - logout: - csrf_parameter: _csrf_token - csrf_provider: ~ - intention: logout - path: /logout - target: / - success_handler: ~ - invalidate_session: true - delete_cookies: + # ... + +You can view actual information about the firewalls in your application with +the ``debug:firewall`` command: + +.. code-block:: terminal - # Prototype - name: - path: ~ - domain: ~ - handlers: [] - anonymous: - key: 4f954a0667e01 - switch_user: - provider: ~ - parameter: _switch_user - role: ROLE_ALLOWED_TO_SWITCH - - access_control: - requires_channel: ~ - - # use the urldecoded format - path: ~ # Example: ^/path to resource/ - host: ~ - ip: ~ - methods: [] - roles: [] - role_hierarchy: - ROLE_ADMIN: [ROLE_ORGANIZER, ROLE_USER] - ROLE_SUPERADMIN: [ROLE_ADMIN] + # displays a list of firewalls currently configured for your application + $ php bin/console debug:firewall + + # displays the details of a specific firewall + $ php bin/console debug:firewall main + + # displays the details of a specific firewall, including detailed information + # about the event listeners for the firewall + $ php bin/console debug:firewall main --events .. _reference-security-firewall-form-login: -Form Login Configuration ------------------------- +``form_login`` Authentication +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When using the ``form_login`` authentication listener beneath a firewall, there are several common options for configuring the "form login" experience. +For even more details, see :doc:`/security/form_login`. -For even more details, see :doc:`/cookbook/security/form_login`. +login_path +.......... -The Login Form and Process -~~~~~~~~~~~~~~~~~~~~~~~~~~ +**type**: ``string`` **default**: ``/login`` + +This is the route or path that the user will be redirected to (unless ``use_forward`` +is set to ``true``) when they try to access a protected resource but aren't +fully authenticated. + +This path **must** be accessible by a normal, unauthenticated user, else +you might create a redirect loop. -* ``login_path`` (type: ``string``, default: ``/login``) - This is the route or path that the user will be redirected to (unless - ``use_forward`` is set to ``true``) when he/she tries to access a - protected resource but isn't fully authenticated. +check_path +.......... - This path **must** be accessible by a normal, un-authenticated user, else - you may create a redirect loop. For details, see - ":ref:`Avoid Common Pitfalls`". +**type**: ``string`` **default**: ``/login_check`` -* ``check_path`` (type: ``string``, default: ``/login_check``) - This is the route or path that your login form must submit to. The - firewall will intercept any requests (``POST`` requests only, by default) - to this URL and process the submitted login credentials. +This is the route or path that your login form must submit to. The firewall +will intercept any requests (``POST`` requests only, by default) to this +URL and process the submitted login credentials. - Be sure that this URL is covered by your main firewall (i.e. don't create - a separate firewall just for ``check_path`` URL). +Be sure that this URL is covered by your main firewall (i.e. don't create +a separate firewall just for ``check_path`` URL). -* ``use_forward`` (type: ``Boolean``, default: ``false``) - If you'd like the user to be forwarded to the login form instead of being - redirected, set this option to ``true``. +failure_path +............ -* ``username_parameter`` (type: ``string``, default: ``_username``) - This is the field name that you should give to the username field of - your login form. When you submit the form to ``check_path``, the security - system will look for a POST parameter with this name. +**type**: ``string`` **default**: ``/login`` -* ``password_parameter`` (type: ``string``, default: ``_password``) - This is the field name that you should give to the password field of - your login form. When you submit the form to ``check_path``, the security - system will look for a POST parameter with this name. +This is the route or path that the user is redirected to after a failed login attempt. +It can be a relative/absolute URL or a Symfony route name. -* ``post_only`` (type: ``Boolean``, default: ``true``) - By default, you must submit your login form to the ``check_path`` URL - as a POST request. By setting this option to ``false``, you can send a - GET request to the ``check_path`` URL. +form_only +......... -Redirecting after Login -~~~~~~~~~~~~~~~~~~~~~~~ +**type**: ``boolean`` **default**: ``false`` -* ``always_use_default_target_path`` (type: ``Boolean``, default: ``false``) -* ``default_target_path`` (type: ``string``, default: ``/``) -* ``target_path_parameter`` (type: ``string``, default: ``_target_path``) -* ``use_referer`` (type: ``Boolean``, default: ``false``) +Set this option to ``true`` to require that the login data is sent using a form +(it checks that the request content-type is ``application/x-www-form-urlencoded`` +or ``multipart/form-data``). This is useful for example to prevent the +:ref:`form login authenticator ` from responding to +requests that should be handled by the :ref:`JSON login authenticator `. -.. _reference-security-pbkdf2: +use_forward +........... -Using the PBKDF2 encoder: Security and Speed --------------------------------------------- +**type**: ``boolean`` **default**: ``false`` -.. versionadded:: 2.2 - The PBKDF2 password encoder was added in Symfony 2.2. +If you'd like the user to be forwarded to the login form instead of being +redirected, set this option to ``true``. -The `PBKDF2`_ encoder provides a high level of Cryptographic security, as -recommended by the National Institute of Standards and Technology (NIST). +username_parameter +.................. -You can see an example of the ``pbkdf2`` encoder in the YAML block on this page. +**type**: ``string`` **default**: ``_username`` -But using PBKDF2 also warrants a warning: using it (with a high number -of iterations) slows down the process. Thus, PBKDF2 should be used with -caution and care. +This is the name of the username field of your +login form. When you submit the form to ``check_path``, the security system +will look for a POST parameter with this name. -A good configuration lies around at least 1000 iterations and sha512 -for the hash algorithm. +password_parameter +.................. -.. _reference-security-bcrypt: +**type**: ``string`` **default**: ``_password`` -Using the BCrypt Password Encoder ---------------------------------- +This is the name of the password field of your +login form. When you submit the form to ``check_path``, the security system +will look for a POST parameter with this name. -.. versionadded:: 2.2 - The BCrypt password encoder was added in Symfony 2.2. +post_only +......... + +**type**: ``boolean`` **default**: ``true`` + +By default, you must submit your login form to the ``check_path`` URL as +a POST request. By setting this option to ``false``, you can send a GET +request too. + +**Options Related to Redirecting after Login** + +always_use_default_target_path +.............................. + +**type**: ``boolean`` **default**: ``false`` + +If ``true``, users are always redirected to the default target path regardless +of the previous URL that was stored in the session. + +default_target_path +................... + +**type**: ``string`` **default**: ``/`` + +The page users are redirected to when there is no previous page stored in the +session (for example, when the users browse the login page directly). + +target_path_parameter +..................... + +**type**: ``string`` **default**: ``_target_path`` + +When using a login form, if you include an HTML element to set the target path, +this option lets you change the name of the HTML element itself. + +failure_path_parameter +...................... + +**type**: ``string`` **default**: ``_failure_path`` + +When using a login form, if you include an HTML element to set the failure path, +this option lets you change the name of the HTML element itself. + +use_referer +........... + +**type**: ``boolean`` **default**: ``false`` + +If ``true``, the user is redirected to the value stored in the ``HTTP_REFERER`` +header when no previous URL was stored in the session. If the referrer URL is +the same as the one generated with the ``login_path`` route, the user is +redirected to the ``default_target_path`` to avoid a redirection loop. + +.. note:: + + For historical reasons, and to match the misspelling of the HTTP standard, + the option is called ``use_referer`` instead of ``use_referrer``. + +logout +~~~~~~ + +You can configure logout options. + +delete_cookies +.............. + +**type**: ``array`` **default**: ``[]`` + +Lists the names (and other optional features) of the cookies to delete when the +user logs out: .. configuration-block:: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... - encoders: - Symfony\Component\Security\Core\User\User: - algorithm: bcrypt - cost: 15 + firewalls: + main: + # ... + logout: + delete_cookies: + cookie1-name: null + cookie2-name: + path: '/' + cookie3-name: + path: null + domain: example.com .. code-block:: xml - - - - - + + + + + + + + + + + + + + + + + .. code-block:: php - // app/config/security.php - $container->loadFromExtension('security', array( + // config/packages/security.php + + // ... + + return static function (SecurityConfig $securityConfig): void { // ... - 'encoders' => array( - 'Symfony\Component\Security\Core\User\User' => array( - 'algorithm' => 'bcrypt', - 'cost' => 15, - ), - ), - )); -The ``cost`` can be in the range of ``4-31`` and determines how long a password -will be encoded. Each increment of ``cost`` *doubles* the time it takes to -encode a password. + $securityConfig->firewall('main') + ->logout() + ->deleteCookie('cookie1-name') + ->deleteCookie('cookie2-name') + ->path('/') + ->deleteCookie('cookie3-name') + ->path(null) + ->domain('example.com'); + }; -If you don't provide the ``cost`` option, the default cost of ``13`` is used. +clear_site_data +............... -.. note:: +**type**: ``array`` **default**: ``[]`` - You can change the cost at any time — even if you already have some - passwords encoded using a different cost. New passwords will be encoded - using the new cost, while the already encoded ones will be validated - using a cost that was used back when they were encoded. +The ``Clear-Site-Data`` HTTP header clears browsing data (cookies, storage, cache) +associated with the requesting website. It allows web developers to have more +control over the data stored by a client browser for their origins. -A salt for each new password is generated automatically and need not be -persisted. Since an encoded password contains the salt used to encode it, -persisting the encoded password alone is enough. +Allowed values are ``cache``, ``cookies``, ``storage`` and ``executionContexts``. +It's also possible to use ``*`` as a wildcard for all directives: -.. note:: +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + logout: + clear_site_data: + - cookies + - storage + + .. code-block:: xml + + + + + + + + + + + + cookies + storage + + + + + + .. code-block:: php + + // config/packages/security.php + + // ... + + return static function (SecurityConfig $securityConfig): void { + // ... + + $securityConfig->firewall('main') + ->logout() + ->clearSiteData(['cookies', 'storage']); + }; + +invalidate_session +.................. + +**type**: ``boolean`` **default**: ``true`` + +By default, when users log out from any firewall, their sessions are invalidated. +This means that logging out from one firewall automatically logs them out from +all the other firewalls. + +The ``invalidate_session`` option allows to redefine this behavior. Set this +option to ``false`` in every firewall and the user will only be logged out from +the current firewall and not the other ones. + +``path`` +........ + +**type**: ``string`` **default**: ``/logout`` + +The path which triggers logout. You need to set up a route with a matching path. + +target +...... + +**type**: ``string`` **default**: ``/`` + +The relative path (if the value starts with ``/``), or absolute URL (if it +starts with ``http://`` or ``https://``) or the route name (otherwise) to +redirect after logout. + +.. _reference-security-logout-csrf: + +enable_csrf +........... + +**type**: ``boolean`` **default**: ``null`` + +Set this option to ``true`` to enable CSRF protection in the logout process +using Symfony's default CSRF token manager. Set also the ``csrf_token_manager`` +option if you need to use a custom CSRF token manager. + +csrf_parameter +.............. + +**type**: ``string`` **default**: ``_csrf_token`` + +The name of the parameter that stores the CSRF token value. + +csrf_token_manager +.................. + +**type**: ``string`` **default**: ``null`` + +The ``id`` of the service used to generate the CSRF tokens. Symfony provides a +default service whose ID is ``security.csrf.token_manager``. + +csrf_token_id +............. + +**type**: ``string`` **default**: ``logout`` + +An arbitrary string used to identify the token (and check its validity afterwards). + +.. _reference-security-firewall-json-login: + +JSON Login Authentication +~~~~~~~~~~~~~~~~~~~~~~~~~ + +check_path +.......... + +**type**: ``string`` **default**: ``/login_check`` + +This is the URL or route name the system must post to authenticate using +the JSON authenticator. The path must be covered by the firewall to which +the user will authenticate. + +username_path +............. + +**type**: ``string`` **default**: ``username`` + +Use this and ``password_path`` to modify the expected request body +structure of the JSON authenticator. For instance, if the JSON document has +the following structure: + +.. code-block:: json + + { + "security": { + "credentials": { + "login": "dunglas", + "password": "MyPassword" + } + } + } + +The security configuration should be: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + lazy: true + json_login: + check_path: login + username_path: security.credentials.login + password_path: security.credentials.password + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $mainFirewall = $security->firewall('main'); + $mainFirewall->lazy(true); + $mainFirewall->jsonLogin() + ->checkPath('/login') + ->usernamePath('security.credentials.login') + ->passwordPath('security.credentials.password') + ; + }; + +password_path +............. + +**type**: ``string`` **default**: ``password`` + +Use this option to modify the expected request body structure. See +`username_path`_ for more details. + +.. _reference-security-ldap: + +LDAP Authentication +~~~~~~~~~~~~~~~~~~~ + +There are several options for connecting against an LDAP server, +using the ``form_login_ldap``, ``http_basic_ldap`` and ``json_login_ldap`` authentication +providers or the ``ldap`` user provider. + +For even more details, see :doc:`/security/ldap`. + +**Authentication** + +You can authenticate to an LDAP server using the LDAP variants of the +``form_login``, ``http_basic`` and ``json_login`` authentication providers. Use +``form_login_ldap``, ``http_basic_ldap`` and ``json_login_ldap``, which will +attempt to ``bind`` against an LDAP server instead of using password comparison. + +Both authentication providers have the same arguments as their normal +counterparts, with the addition of two configuration keys: + +service +....... + +**type**: ``string`` **default**: ``ldap`` + +This is the name of your configured LDAP client. + +dn_string +......... + +**type**: ``string`` **default**: ``{user_identifier}`` + +This is the string which will be used as the bind DN. The ``{user_identifier}`` +placeholder will be replaced with the user-provided value (their login). +Depending on your LDAP server's configuration, you may need to override +this value. + +query_string +............ + +**type**: ``string`` **default**: ``null`` + +This is the string which will be used to query for the DN. The ``{user_identifier}`` +placeholder will be replaced with the user-provided value (their login). +Depending on your LDAP server's configuration, you will need to override +this value. This setting is only necessary if the user's DN cannot be derived +statically using the ``dn_string`` config option. + +**User provider** + +Users will still be fetched from the configured user provider. If you wish to +fetch your users from an LDAP server, you will need to use the +:doc:`LDAP User Provider ` and any of these authentication +providers: ``form_login_ldap`` or ``http_basic_ldap`` or ``json_login_ldap``. + +.. _reference-security-firewall-x509: + +X.509 Authentication +~~~~~~~~~~~~~~~~~~~~ + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + x509: + provider: your_user_provider + user: SSL_CLIENT_S_DN_Email + credentials: SSL_CLIENT_S_DN + user_identifier: emailAddress + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $mainFirewall = $security->firewall('main'); + $mainFirewall->x509() + ->provider('your_user_provider') + ->user('SSL_CLIENT_S_DN_Email') + ->credentials('SSL_CLIENT_S_DN') + ->userIdentifier('emailAddress') + ; + }; + +user +.... + +**type**: ``string`` **default**: ``SSL_CLIENT_S_DN_Email`` + +The name of the ``$_SERVER`` parameter containing the user identifier used +to load the user in Symfony. The default value is exposed by Apache. + +credentials +........... + +**type**: ``string`` **default**: ``SSL_CLIENT_S_DN`` + +If the ``user`` parameter is not available, the name of the ``$_SERVER`` +parameter containing the full "distinguished name" of the certificate +(exposed by e.g. Nginx). + +By default, Symfony identifies the value following ``emailAddress=`` in this +parameter. This can be changed using the ``user_identifier`` option. + +user_identifier +............... + +**type**: ``string`` **default**: ``emailAddress`` + +The value of this option tells Symfony which parameter to use to find the user +identifier in the "distinguished name". + +For example, if the "distinguished name" is +``Subject: C=FR, O=My Organization, CN=user1, emailAddress=user1@myorg.fr``, +and the value of this option is ``'CN'``, the user identifier will be ``'user1'``. + +.. _reference-security-firewall-remote-user: + +Remote User Authentication +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + # ... + remote_user: + provider: your_user_provider + user: REMOTE_USER + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $mainFirewall = $security->firewall('main'); + $mainFirewall->remoteUser() + ->provider('your_user_provider') + ->user('REMOTE_USER') + ; + }; - All the encoded passwords are ``60`` characters long, so make sure to - allocate enough space for them to be persisted. +provider +........ - .. _reference-security-firewall-context: +**type**: ``string`` + +The service ID of the user provider that should be used by this +authenticator. + +user +.... + +**type**: ``string`` **default**: ``REMOTE_USER`` + +The name of the ``$_SERVER`` parameter holding the user identifier. + +.. _reference-security-firewall-context: Firewall Context ----------------- +~~~~~~~~~~~~~~~~ -Most applications will only need one :ref:`firewall`. -But if your application *does* use multiple firewalls, you'll notice that +If your application uses multiple :ref:`firewalls `, you'll notice that if you're authenticated in one firewall, you're not automatically authenticated -in another. In other words, the systems don't share a common "context": each -firewall acts like a separate security system. +in another. In other words, the systems don't share a common "context": +each firewall acts like a separate security system. However, each firewall has an optional ``context`` key (which defaults to the name of the firewall), which is used when storing and retrieving security @@ -380,7 +880,7 @@ multiple firewalls, the "context" could actually be shared: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -394,70 +894,225 @@ multiple firewalls, the "context" could actually be shared: .. code-block:: xml - - - - - - - - - + + + + + + + + + + + + + .. code-block:: php - // app/config/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'somename' => array( - // ... - 'context' => 'my_context' - ), - 'othername' => array( - // ... - 'context' => 'my_context' - ), - ), - )); - -HTTP-Digest Authentication --------------------------- - -To use HTTP-Digest authentication you need to provide a realm and a key: + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('somename') + // ... + ->context('my_context') + ; + + $security->firewall('othername') + // ... + ->context('my_context') + ; + }; + +.. note:: + + The firewall context key is stored in session, so every firewall using it + must set its ``stateless`` option to ``false``. Otherwise, the context is + ignored and you won't be able to authenticate on multiple firewalls at the + same time. + +.. _reference-security-stateless: + +stateless +~~~~~~~~~ + +Firewalls can configure a ``stateless`` boolean option in order to declare that +the session must not be used when authenticating users: .. configuration-block:: - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - somename: - http_digest: - key: "a_random_string" - realm: "secure-api" - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'somename' => array( - 'http_digest' => array( - 'key' => 'a_random_string', - 'realm' => 'secure-api', - ), - ), - ), - )); - -.. _`PBKDF2`: http://en.wikipedia.org/wiki/PBKDF2 + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + stateless: true + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $mainFirewall = $security->firewall('main'); + $mainFirewall->stateless(true); + // ... + }; + +.. _reference-security-lazy: + +lazy +~~~~ + +Firewalls can configure a ``lazy`` boolean option to load the user and start the +session only if the application actually accesses the User object, (e.g. calling +``is_granted()`` in a template or ``isGranted()`` in a controller or service): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + lazy: true + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('main') + ->lazy(true); + // ... + }; + +User Checkers +~~~~~~~~~~~~~ + +During the authentication of a user, additional checks might be required to +verify if the identified user is allowed to log in. Each firewall can include +a ``user_checker`` option to define the service used to perform those checks. + +Learn more about user checkers in :doc:`/security/user_checkers`. + +Required Badges +~~~~~~~~~~~~~~~ + +Firewalls can configure a list of required badges that must be present on the authenticated passport: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + required_badges: ['CsrfTokenBadge', 'My\Badge'] + + .. code-block:: xml + + + + + + + + + CsrfTokenBadge + My\Badge + + + + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $mainFirewall = $security->firewall('main'); + $mainFirewall->requiredBadges(['CsrfTokenBadge', 'My\Badge']); + // ... + }; + +providers +--------- + +This option defines how the application users are loaded (from a database, +an LDAP server, a configuration file, etc.) Read +:doc:`/security/user_providers` to learn more about each of those +providers. + +role_hierarchy +-------------- + +Instead of associating many roles to users, this option allows you to define +role inheritance rules by creating a role hierarchy, as explained in +:ref:`security-role-hierarchy`. + +.. _`Session Fixation`: https://owasp.org/www-community/attacks/Session_fixation diff --git a/reference/configuration/swiftmailer.rst b/reference/configuration/swiftmailer.rst deleted file mode 100644 index a051f8b7233..00000000000 --- a/reference/configuration/swiftmailer.rst +++ /dev/null @@ -1,222 +0,0 @@ -.. index:: - single: Configuration reference; Swiftmailer - -SwiftmailerBundle Configuration ("swiftmailer") -=============================================== - -This reference document is a work in progress. It should be accurate, but -all options are not yet fully covered. For a full list of the default configuration -options, see `Full Default Configuration`_ - -The ``swiftmailer`` key configures Symfony's integration with Swiftmailer, -which is responsible for creating and delivering email messages. - -Configuration -------------- - -* `transport`_ -* `username`_ -* `password`_ -* `host`_ -* `port`_ -* `encryption`_ -* `auth_mode`_ -* `spool`_ - * `type`_ - * `path`_ -* `sender_address`_ -* `antiflood`_ - * `threshold`_ - * `sleep`_ -* `delivery_address`_ -* `disable_delivery`_ -* `logging`_ - -transport -~~~~~~~~~ - -**type**: ``string`` **default**: ``smtp`` - -The exact transport method to use to deliver emails. Valid values are: - -* smtp -* gmail (see :doc:`/cookbook/email/gmail`) -* mail -* sendmail -* null (same as setting `disable_delivery`_ to ``true``) - -username -~~~~~~~~ - -**type**: ``string`` - -The username when using ``smtp`` as the transport. - -password -~~~~~~~~ - -**type**: ``string`` - -The password when using ``smtp`` as the transport. - -host -~~~~ - -**type**: ``string`` **default**: ``localhost`` - -The host to connect to when using ``smtp`` as the transport. - -port -~~~~ - -**type**: ``string`` **default**: 25 or 465 (depending on `encryption`_) - -The port when using ``smtp`` as the transport. This defaults to 465 if encryption -is ``ssl`` and 25 otherwise. - -encryption -~~~~~~~~~~ - -**type**: ``string`` - -The encryption mode to use when using ``smtp`` as the transport. Valid values -are ``tls``, ``ssl``, or ``null`` (indicating no encryption). - -auth_mode -~~~~~~~~~ - -**type**: ``string`` - -The authentication mode to use when using ``smtp`` as the transport. Valid -values are ``plain``, ``login``, ``cram-md5``, or ``null``. - -spool -~~~~~ - -For details on email spooling, see :doc:`/cookbook/email/spool`. - -type -.... - -**type**: ``string`` **default**: ``file`` - -The method used to store spooled messages. Currently only ``file`` is supported. -However, a custom spool should be possible by creating a service called -``swiftmailer.spool.myspool`` and setting this value to ``myspool``. - -path -.... - -**type**: ``string`` **default**: ``%kernel.cache_dir%/swiftmailer/spool`` - -When using the ``file`` spool, this is the path where the spooled messages -will be stored. - -sender_address -~~~~~~~~~~~~~~ - -**type**: ``string`` - -If set, all messages will be delivered with this address as the "return path" -address, which is where bounced messages should go. This is handled internally -by Swiftmailer's ``Swift_Plugins_ImpersonatePlugin`` class. - -antiflood -~~~~~~~~~ - -threshold -......... - -**type**: ``string`` **default**: ``99`` - -Used with ``Swift_Plugins_AntiFloodPlugin``. This is the number of emails -to send before restarting the transport. - -sleep -..... - -**type**: ``string`` **default**: ``0`` - -Used with ``Swift_Plugins_AntiFloodPlugin``. This is the number of seconds -to sleep for during a transport restart. - -delivery_address -~~~~~~~~~~~~~~~~ - -**type**: ``string`` - -If set, all email messages will be sent to this address instead of being sent -to their actual recipients. This is often useful when developing. For example, -by setting this in the ``config_dev.yml`` file, you can guarantee that all -emails sent during development go to a single account. - -This uses ``Swift_Plugins_RedirectingPlugin``. Original recipients are available -on the ``X-Swift-To``, ``X-Swift-Cc`` and ``X-Swift-Bcc`` headers. - -disable_delivery -~~~~~~~~~~~~~~~~ - -**type**: ``Boolean`` **default**: ``false`` - -If true, the ``transport`` will automatically be set to ``null``, and no -emails will actually be delivered. - -logging -~~~~~~~ - -**type**: ``Boolean`` **default**: ``%kernel.debug%`` - -If true, Symfony's data collector will be activated for Swiftmailer and the -information will be available in the profiler. - -Full Default Configuration --------------------------- - -.. configuration-block:: - - .. code-block:: yaml - - swiftmailer: - transport: smtp - username: ~ - password: ~ - host: localhost - port: false - encryption: ~ - auth_mode: ~ - spool: - type: file - path: "%kernel.cache_dir%/swiftmailer/spool" - sender_address: ~ - antiflood: - threshold: 99 - sleep: 0 - delivery_address: ~ - disable_delivery: ~ - logging: "%kernel.debug%" - - .. code-block:: xml - - - - - - diff --git a/reference/configuration/twig.rst b/reference/configuration/twig.rst index f6f860b5c22..3c4dc1b30ac 100644 --- a/reference/configuration/twig.rst +++ b/reference/configuration/twig.rst @@ -1,103 +1,401 @@ -.. index:: - pair: Twig; Configuration reference +Twig Configuration Reference (TwigBundle) +========================================= -TwigBundle Configuration Reference -================================== +The TwigBundle integrates the Twig library in Symfony applications to +:ref:`render templates `. All these options are configured +under the ``twig`` key in your application configuration. + +.. code-block:: terminal + + # displays the default config values defined by Symfony + $ php bin/console config:dump-reference twig + + # displays the actual config values used by your application + $ php bin/console debug:config twig + +.. note:: + + When using XML, you must use the ``http://symfony.com/schema/dic/twig`` + namespace and the related XSD schema is available at: + ``https://symfony.com/schema/dic/twig/twig-1.0.xsd`` + +auto_reload +~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``%kernel.debug%`` + +If ``true``, whenever a template is rendered, Symfony checks first if its source +code has changed since it was compiled. If it has changed, the template is +compiled again automatically. + +.. _config-twig-autoescape: + +autoescape_service +~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +The escaping strategy applied by default to the template (to prevent :ref:`XSS attacks `) +is determined during compilation time based on the filename of the template. This means for example +that the contents of a ``*.html.twig`` template are escaped for HTML and the +contents of ``*.js.twig`` are escaped for JavaScript. + +This option allows to define the Symfony service which will be used to determine +the default escaping applied to the template. + +autoescape_service_method +~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +If ``autoescape_service`` option is defined, then this option defines the method +called to determine the default escaping applied to the template. + +If the service defined in ``autoescape_service`` is invocable (i.e. it defines +the `__invoke() PHP magic method`_) you can omit this option. + +base_template_class +~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``Twig\Template`` + +.. deprecated:: 7.1 + + The ``base_template_class`` option is deprecated since Symfony 7.1. + +Twig templates are compiled into PHP classes before using them to render +contents. This option defines the base class from which all the template classes +extend. Using a custom base template is discouraged because it will make your +application harder to maintain. + +cache +~~~~~ + +**type**: ``string`` | ``false`` **default**: ``%kernel.cache_dir%/twig`` + +Before using the Twig templates to render some contents, they are compiled into +regular PHP code. Compilation is a costly process, so the result is cached in +the directory defined by this configuration option. + +Set this option to ``false`` to disable Twig template compilation. However, this +is not recommended; not even in the ``dev`` environment, because the +``auto_reload`` option ensures that cached templates which have changed get +compiled again. + +charset +~~~~~~~ + +**type**: ``string`` **default**: ``%kernel.charset%`` + +The charset used by the template files. By default it's the same as the value of +the :ref:`kernel.charset container parameter `, +which is ``UTF-8`` by default in Symfony applications. + +date +~~~~ + +These options define the default values used by the ``date`` filter to format +date and time values. They are useful to avoid passing the same arguments on +every ``date`` filter call. + +format +...... + +**type**: ``string`` **default**: ``F j, Y H:i`` + +The format used by the ``date`` filter to display values when no specific format +is passed as an argument. + +interval_format +............... + +**type**: ``string`` **default**: ``%d days`` + +The format used by the ``date`` filter to display ``DateInterval`` instances +when no specific format is passed as argument. + +timezone +........ + +**type**: ``string`` **default**: (the value returned by ``date_default_timezone_get()``) + +The timezone used when formatting date values with the ``date`` filter and no +specific timezone is passed as an argument. + +debug +~~~~~ + +**type**: ``boolean`` **default**: ``%kernel.debug%`` + +If ``true``, the compiled templates include a ``__toString()`` method that can +be used to display their nodes. + +This option also controls the behavior of :ref:`the Twig dump utilities `. +If this option is ``false``, the ``dump()`` function doesn't output any contents. + +.. _config-twig-default-path: + +default_path +~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``%kernel.project_dir%/templates`` + +The path to the directory where Symfony will look for the application Twig +templates by default. If you store the templates in more than one directory, use +the :ref:`paths ` option too. + +.. _config-twig-file-name-pattern: + +file_name_pattern +~~~~~~~~~~~~~~~~~ + +**type**: ``string`` or ``array`` of ``string`` **default**: ``[]`` + +Some applications store their front-end assets in the same directory as Twig +templates. The ``lint:twig`` command filters those files to only lint the ones +that match the ``*.twig`` filename pattern. + +However, the ``cache:warmup`` command tries to compile all files, including +non-Twig templates (and it ignores compilation errors). The result is an +unnecessary consumption of CPU and disk resources. + +In those cases, use this option to define the filename pattern(s) of the files +that are Twig templates (the rest of files will be ignored by ``cache:warmup``). +The value of this option can be a regular expression, a glob, or a string: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/twig.yaml + twig: + file_name_pattern: ['*.twig', 'specific_file.html'] + # ... + + .. code-block:: xml + + + + + + + *.twig + specific_file.html + + + + + .. code-block:: php + + // config/packages/twig.php + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig): void { + $twig->fileNamePattern([ + '*.twig', + 'specific_file.html', + ]); + + // ... + }; + +.. _config-twig-form-themes: + +form_themes +~~~~~~~~~~~ + +**type**: ``array`` of ``string`` **default**: ``['form_div_layout.html.twig']`` + +Defines one or more :doc:`form themes ` which are applied to +all the forms of the application: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/twig.yaml + twig: + form_themes: ['bootstrap_5_layout.html.twig', 'form/my_theme.html.twig'] + # ... + + .. code-block:: xml + + + + + + + bootstrap_5_layout.html.twig + form/my_theme.html.twig + + + + + .. code-block:: php + + // config/packages/twig.php + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig): void { + $twig->formThemes([ + 'bootstrap_5_layout.html.twig', + 'form/my_theme.html.twig', + ]); + + // ... + }; + +The order in which themes are defined is important because each theme overrides +all the previous one. When rendering a form field whose block is not defined in +the form theme, Symfony falls back to the previous themes until the first one. + +These global themes are applied to all forms, even those which use the +:ref:`form_theme Twig tag `, but you can +:ref:`disable global themes for specific forms `. + +globals +~~~~~~~ + +**type**: ``array`` **default**: ``[]`` + +It defines the global variables injected automatically into all Twig templates. +Learn more about :ref:`Twig global variables `. + +mailer +~~~~~~ + +.. _config-twig-html-to-text-converter: + +html_to_text_converter +...................... + +**type**: ``string`` **default**: ``null`` + +The service implementing +:class:`Symfony\\Component\\Mime\\HtmlToTextConverter\\HtmlToTextConverterInterface` +that will be used to automatically create the text part of an email from its +HTML contents when not explicitly defined. + +number_format +~~~~~~~~~~~~~ + +These options define the default values used by the ``number_format`` filter to +format numeric values. They are useful to avoid passing the same arguments on +every ``number_format`` filter call. + +decimals +........ + +**type**: ``integer`` **default**: ``0`` + +The number of decimals used to format numeric values when no specific number is +passed as argument to the ``number_format`` filter. + +decimal_point +............. + +**type**: ``string`` **default**: ``.`` + +The character used to separate the decimals from the integer part of numeric +values when no specific character is passed as argument to the ``number_format`` +filter. + +thousands_separator +................... + +**type**: ``string`` **default**: ``,`` + +The character used to separate every group of thousands in numeric values when +no specific character is passed as argument to the ``number_format`` filter. + +optimizations +~~~~~~~~~~~~~ + +**type**: ``integer`` **default**: ``-1`` + +Twig includes an extension called ``optimizer`` which is enabled by default in +Symfony applications. This extension analyzes the templates to optimize them +when being compiled. For example, if your template doesn't use the special +``loop`` variable inside a ``for`` tag, this extension removes the initialization +of that unused variable. + +By default, this option is ``-1``, which means that all optimizations are turned +on. Set it to ``0`` to disable all the optimizations. You can even enable or +disable these optimizations selectively, as explained in the Twig documentation +about `the optimizer extension`_. + +.. _config-twig-paths: + +paths +~~~~~ + +**type**: ``array`` **default**: ``null`` + +Defines the directories where application templates are stored in addition to +the directory defined in the :ref:`default_path option `: .. configuration-block:: .. code-block:: yaml + # config/packages/twig.yaml twig: - exception_controller: twig.controller.exception:showAction - form: - resources: - - # Default: - - form_div_layout.html.twig - - # Example: - - MyBundle::form.html.twig - globals: - - # Examples: - foo: "@bar" - pi: 3.14 - - # Example options, but the easiest use is as seen above - some_variable_name: - # a service id that should be the value - id: ~ - # set to service or leave blank - type: ~ - value: ~ - autoescape: ~ - - # The following were added in Symfony 2.3. - # See http://twig.sensiolabs.org/doc/recipes.html#using-the-template-name-to-set-the-default-escaping-strategy - autoescape_service: ~ # Example: @my_service - autoescape_service_method: ~ # use in combination with autoescape_service option - base_template_class: ~ # Example: Twig_Template - cache: "%kernel.cache_dir%/twig" - charset: "%kernel.charset%" - debug: "%kernel.debug%" - strict_variables: ~ - auto_reload: ~ - optimizations: ~ + # ... + paths: + 'email/default/templates': ~ + 'backend/templates': 'admin' .. code-block:: xml + - - - - MyBundle::form.html.twig - - - 3.14 + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd"> + + + + email/default/templates + backend/templates .. code-block:: php - $container->loadFromExtension('twig', array( - 'form' => array( - 'resources' => array( - 'MyBundle::form.html.twig', - ) - ), - 'globals' => array( - 'foo' => '@bar', - 'pi' => 3.14, - ), - 'auto_reload' => '%kernel.debug%', - 'autoescape' => true, - 'base_template_class' => 'Twig_Template', - 'cache' => '%kernel.cache_dir%/twig', - 'charset' => '%kernel.charset%', - 'debug' => '%kernel.debug%', - 'strict_variables' => false, - )); - -Configuration -------------- - -.. _config-twig-exception-controller: - -exception_controller -.................... - -**type**: ``string`` **default**: ``twig.controller.exception:showAction`` - -This is the controller that is activated after an exception is thrown anywhere -in your application. The default controller -(:class:`Symfony\\Bundle\\TwigBundle\\Controller\\ExceptionController`) -is what's responsible for rendering specific templates under different error -conditions (see :doc:`/cookbook/controller/error_pages`). Modifying this -option is advanced. If you need to customize an error page you should use -the previous link. If you need to perform some behavior on an exception, -you should add a listener to the ``kernel.exception`` event (see :ref:`dic-tags-kernel-event-listener`). + // config/packages/twig.php + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig): void { + // ... + + $twig->path('email/default/templates', null); + $twig->path('backend/templates', 'admin'); + }; + +Read more about :ref:`template directories and namespaces `. + +.. _config-twig-strict-variables: + +strict_variables +~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``%kernel.debug%`` + +If set to ``true``, Symfony shows an exception whenever a Twig variable, +attribute or method doesn't exist. If set to ``false`` these errors are ignored +and the non-existing values are replaced by ``null``. + +.. _`the optimizer extension`: https://twig.symfony.com/doc/3.x/api.html#optimizer-extension +.. _`__invoke() PHP magic method`: https://www.php.net/manual/en/language.oop5.magic.php#object.invoke diff --git a/reference/configuration/web_profiler.rst b/reference/configuration/web_profiler.rst index 5253692a6be..c3b57d37c55 100644 --- a/reference/configuration/web_profiler.rst +++ b/reference/configuration/web_profiler.rst @@ -1,32 +1,73 @@ -.. index:: - single: Configuration reference; WebProfiler +Profiler Configuration Reference (WebProfilerBundle) +==================================================== -WebProfilerBundle Configuration -=============================== +The WebProfilerBundle is a **development tool** that provides detailed technical +information about each request execution and displays it in both the web debug +toolbar and the :doc:`profiler `. All these options are configured +under the ``web_profiler`` key in your application configuration. -Full Default Configuration --------------------------- +.. code-block:: terminal -.. configuration-block:: + # displays the default config values defined by Symfony + $ php bin/console config:dump-reference web_profiler - .. code-block:: yaml + # displays the actual config values used by your application + $ php bin/console debug:config web_profiler - web_profiler: +.. note:: - # DEPRECATED, it is not useful anymore and can be removed safely from your configuration - verbose: true + When using XML, you must use the ``http://symfony.com/schema/dic/webprofiler`` + namespace and the related XSD schema is available at: + ``https://symfony.com/schema/dic/webprofiler/webprofiler-1.0.xsd`` - # display the web debug toolbar at the bottom of pages with a summary of profiler info - toolbar: false - position: bottom +.. warning:: - # gives you the opportunity to look at the collected data before following the redirect - intercept_redirects: false + The web debug toolbar is not available for responses of type ``StreamedResponse``. - .. code-block:: xml +excluded_ajax_paths +~~~~~~~~~~~~~~~~~~~ - +**type**: ``string`` **default**: ``^/((index|app(_[\w]+)?)\.php/)?_wdt`` + +When the toolbar logs AJAX requests, it matches their URLs against this regular +expression. If the URL matches, the request is not displayed in the toolbar. This +is useful when the application makes lots of AJAX requests, or if they are heavy +and you want to exclude some of them. + +.. _intercept_redirects: + +intercept_redirects +~~~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +If a redirect occurs during an HTTP response, the browser follows it automatically +and you won't see the toolbar or the profiler of the original URL, only the +redirected URL. + +When setting this option to ``true``, the browser *stops* before making any +redirection and shows you the URL which is going to redirect to, its toolbar, +and its profiler. Once you've inspected the toolbar/profiler data, you can click +on the given link to perform the redirect. + +toolbar +~~~~~~~ + +enabled +....... +**type**: ``boolean`` **default**: ``false`` + +It enables and disables the toolbar entirely. Usually you set this to ``true`` +in the ``dev`` and ``test`` environments and to ``false`` in the ``prod`` +environment. + +ajax_replace +............ +**type**: ``boolean`` **default**: ``false`` + +If you set this option to ``true``, the toolbar is replaced on AJAX requests. +This only works in combination with an enabled toolbar. + +.. versionadded:: 7.3 + + The ``ajax_replace`` configuration option was introduced in Symfony 7.3. diff --git a/reference/constraints.rst b/reference/constraints.rst index 1ed9b35649b..bb506bf4576 100644 --- a/reference/constraints.rst +++ b/reference/constraints.rst @@ -1,57 +1,14 @@ Validation Constraints Reference ================================ -.. toctree:: - :maxdepth: 1 - :hidden: - - constraints/NotBlank - constraints/Blank - constraints/NotNull - constraints/Null - constraints/True - constraints/False - constraints/Type - - constraints/Email - constraints/Length - constraints/Url - constraints/Regex - constraints/Ip - - constraints/Range - - constraints/Date - constraints/DateTime - constraints/Time - - constraints/Choice - constraints/Collection - constraints/Count - constraints/UniqueEntity - constraints/Language - constraints/Locale - constraints/Country - - constraints/File - constraints/Image - - constraints/CardScheme - constraints/Luhn - - constraints/Callback - constraints/All - constraints/UserPassword - constraints/Valid - The Validator is designed to validate objects against *constraints*. In real life, a constraint could be: "The cake must not be burned". In -Symfony2, constraints are similar: They are assertions that a condition is +Symfony, constraints are similar: They are assertions that a condition is true. Supported Constraints --------------------- -The following constraints are natively available in Symfony2: +The following constraints are natively available in Symfony: .. include:: /reference/constraints/map.rst.inc diff --git a/reference/constraints/All.rst b/reference/constraints/All.rst index d270fa0d4d6..43ff4d6ac9d 100644 --- a/reference/constraints/All.rst +++ b/reference/constraints/All.rst @@ -4,28 +4,40 @@ All When applied to an array (or Traversable object), this constraint allows you to apply a collection of constraints to each element of the array. -+----------------+------------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+------------------------------------------------------------------------+ -| Options | - `constraints`_ | -+----------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\All` | -+----------------+------------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\AllValidator` | -+----------------+------------------------------------------------------------------------+ +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\All` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\AllValidator` +========== =================================================================== Basic Usage ----------- -Suppose that you have an array of strings, and you want to validate each +Suppose that you have an array of strings and you want to validate each entry in that array: .. configuration-block:: + .. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class User + { + #[Assert\All([ + new Assert\NotBlank, + new Assert\Length(min: 5), + ])] + protected array $favoriteColors = []; + } + .. code-block:: yaml - # src/UserBundle/Resources/config/validation.yml - Acme\UserBundle\Entity\User: + # config/validator/validation.yaml + App\Entity\User: properties: favoriteColors: - All: @@ -33,58 +45,46 @@ entry in that array: - Length: min: 5 - .. code-block:: php-annotations - - // src/Acme/UserBundle/Entity/User.php - namespace Acme\UserBundle\Entity; - - use Symfony\Component\Validator\Constraints as Assert; - - class User - { - /** - * @Assert\All({ - * @Assert\NotBlank - * @Assert\Length(min = "5"), - * }) - */ - protected $favoriteColors = array(); - } - .. code-block:: xml - - - - - - - - + + + + + + + + + + + + .. code-block:: php - // src/Acme/UserBundle/Entity/User.php - namespace Acme\UserBundle\Entity; - - use Symfony\Component\Validator\Mapping\ClassMetadata; + // src/Entity/User.php + namespace App\Entity; + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class User { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('favoriteColors', new Assert\All(array( - 'constraints' => array( + $metadata->addPropertyConstraint('favoriteColors', new Assert\All( + constraints: [ new Assert\NotBlank(), - new Assert\Length(array('min' => 5)), - ), - ))); + new Assert\Length(min: 5), + ], + )); } } @@ -94,10 +94,14 @@ be blank and to be at least 5 characters long. Options ------- -constraints -~~~~~~~~~~~ +``constraints`` +~~~~~~~~~~~~~~~ -**type**: ``array`` [:ref:`default option`] +**type**: ``array`` This required option is the array of validation constraints that you want to apply to each element of the underlying array. + +.. include:: /reference/constraints/_groups-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/AtLeastOneOf.rst b/reference/constraints/AtLeastOneOf.rst new file mode 100644 index 00000000000..fecbe617f5a --- /dev/null +++ b/reference/constraints/AtLeastOneOf.rst @@ -0,0 +1,182 @@ +AtLeastOneOf +============ + +This constraint checks that the value satisfies at least one of the given +constraints. The validation stops as soon as one constraint is satisfied. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\AtLeastOneOf` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\AtLeastOneOfValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraints ensure that: + +* the ``password`` of a ``Student`` either contains ``#`` or is at least ``10`` + characters long; +* the ``grades`` of a ``Student`` is an array which contains at least ``3`` + elements or that each element is greater than or equal to ``5``. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Student.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Student + { + #[Assert\AtLeastOneOf([ + new Assert\Regex('/#/'), + new Assert\Length(min: 10), + ])] + protected string $plainPassword; + + #[Assert\AtLeastOneOf([ + new Assert\Count(min: 3), + new Assert\All( + new Assert\GreaterThanOrEqual(5) + ), + ])] + protected array $grades; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Student: + properties: + password: + - AtLeastOneOf: + - Regex: '/#/' + - Length: + min: 10 + grades: + - AtLeastOneOf: + - Count: + min: 3 + - All: + - GreaterThanOrEqual: 5 + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Student.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Student + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('password', new Assert\AtLeastOneOf( + constraints: [ + new Assert\Regex(pattern: '/#/'), + new Assert\Length(min: 10), + ], + )); + + $metadata->addPropertyConstraint('grades', new Assert\AtLeastOneOf( + constraints: [ + new Assert\Count(min: 3), + new Assert\All( + constraints: [ + new Assert\GreaterThanOrEqual(5), + ], + ), + ], + )); + } + } + +Options +------- + +constraints +~~~~~~~~~~~ + +**type**: ``array`` + +This required option is the array of validation constraints from which at least one of +has to be satisfied in order for the validation to succeed. + +includeInternalMessages +~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``true`` + +If set to ``true``, the message that is shown if the validation fails, +will include the list of messages for the internal constraints. See option +`message`_ for an example. + +message +~~~~~~~ + +**type**: ``string`` **default**: ``This value should satisfy at least one of the following constraints:`` + +This is the intro of the message that will be shown if the validation fails. By default, +it will be followed by the list of messages for the internal constraints +(configurable by `includeInternalMessages`_ option) . For example, +if the above ``grades`` property fails to validate, the message will be +``This value should satisfy at least one of the following constraints: +[1] This collection should contain 3 elements or more. +[2] Each element of this collection should satisfy its own set of constraints.`` + +messageCollection +~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``Each element of this collection should satisfy its own set of constraints.`` + +This is the message that will be shown if the validation fails +and the internal constraint is either :doc:`/reference/constraints/All` +or :doc:`/reference/constraints/Collection`. See option `message`_ for an example. + +.. include:: /reference/constraints/_groups-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Bic.rst b/reference/constraints/Bic.rst new file mode 100644 index 00000000000..6cde4a11bac --- /dev/null +++ b/reference/constraints/Bic.rst @@ -0,0 +1,139 @@ +Bic +=== + +This constraint is used to ensure that a value has the proper format of a +`Business Identifier Code (BIC)`_. BIC is an internationally agreed means to +uniquely identify both financial and non-financial institutions. You may also +check that the BIC's country code is the same as a given IBAN's one. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Bic` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\BicValidator` +========== =================================================================== + +Basic Usage +----------- + +To use the Bic validator, apply it to a property on an object that +will contain a Business Identifier Code (BIC). + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Transaction.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Transaction + { + #[Assert\Bic] + protected string $businessIdentifierCode; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Transaction: + properties: + businessIdentifierCode: + - Bic: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Transaction.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Transaction + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('businessIdentifierCode', new Assert\Bic()); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Available Options +----------------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``iban`` +~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +An IBAN value to validate that its country code is the same as the BIC's one. + +``ibanMessage`` +~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}.`` + +The default message supplied when the value does not pass the combined BIC/IBAN check. + +``ibanPropertyPath`` +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +It defines the object property whose value stores the IBAN used to check the BIC with. + +For example, if you want to compare the ``$bic`` property of some object +with regard to the ``$iban`` property of the same object, use +``ibanPropertyPath="iban"`` in the comparison constraint of ``$bic``. + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This is not a valid Business Identifier Code (BIC).`` + +The default message supplied when the value does not pass the BIC check. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) BIC value +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +``mode`` +~~~~~~~~ + +**type**: ``string`` **default**: ``Bic::VALIDATION_MODE_STRICT`` + +This option defines how the BIC is validated. The possible values are available +as constants in the :class:`Symfony\\Component\\Validator\\Constraints\\Bic` class: + +* ``Bic::VALIDATION_MODE_STRICT`` validates the given value without any modification; +* ``Bic::VALIDATION_MODE_CASE_INSENSITIVE`` converts the given value to uppercase before validating it. + +.. versionadded:: 7.2 + + The ``mode`` option was introduced in Symfony 7.2. + +.. _`Business Identifier Code (BIC)`: https://en.wikipedia.org/wiki/Business_Identifier_Code diff --git a/reference/constraints/Blank.rst b/reference/constraints/Blank.rst index 34a841858b5..485d25319ac 100644 --- a/reference/constraints/Blank.rst +++ b/reference/constraints/Blank.rst @@ -1,20 +1,23 @@ Blank ===== -Validates that a value is blank, defined as equal to a blank string or equal -to ``null``. To force that a value strictly be equal to ``null``, see the -:doc:`/reference/constraints/Null` constraint. To force that a value is *not* -blank, see :doc:`/reference/constraints/NotBlank`. - -+----------------+-----------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+-----------------------------------------------------------------------+ -| Options | - `message`_ | -+----------------+-----------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Blank` | -+----------------+-----------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\BlankValidator` | -+----------------+-----------------------------------------------------------------------+ +Validates that a value is blank - meaning equal to an empty string or ``null``:: + + if ('' !== $value && null !== $value) { + // validation will fail + } + +To force that a value strictly be equal to ``null``, see the +:doc:`/reference/constraints/IsNull` constraint. + +To force that a value is *not* blank, see :doc:`/reference/constraints/NotBlank`. +But be careful as ``NotBlank`` is *not* strictly the opposite of ``Blank``. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Blank` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\BlankValidator` +========== =================================================================== Basic Usage ----------- @@ -24,49 +27,53 @@ of an ``Author`` class were blank, you could do the following: .. configuration-block:: - .. code-block:: yaml - - # src/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - firstName: - - Blank: ~ + .. code-block:: php-attributes - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\Blank() - */ - protected $firstName; + #[Assert\Blank] + protected string $firstName; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + firstName: + - Blank: ~ + .. code-block:: xml - - - - - - + + + + + + + + + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', new Assert\Blank()); } @@ -75,9 +82,22 @@ of an ``Author`` class were blank, you could do the following: Options ------- -message -~~~~~~~ +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ -**type**: ``string`` **default**: ``This value should be blank`` +**type**: ``string`` **default**: ``This value should be blank.`` This is the message that will be shown if the value is not blank. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Callback.rst b/reference/constraints/Callback.rst index 93539cb7eb2..017b9435cff 100644 --- a/reference/constraints/Callback.rst +++ b/reference/constraints/Callback.rst @@ -1,11 +1,11 @@ Callback ======== -The purpose of the Callback assertion is to let you create completely custom -validation rules and to assign any validation errors to specific fields on -your object. If you're using validation with forms, this means that you can -make these custom errors display next to a specific field, instead of simply -at the top of your form. +The purpose of the Callback constraint is to create completely custom +validation rules and to assign any validation errors to specific fields +on your object. If you're using validation with forms, this means that +instead of displaying custom errors at the top of the form, you can +display them next to the field they apply to. This process works by specifying one or more *callback* methods, each of which will be called during the validation process. Each of those methods @@ -17,198 +17,270 @@ can do anything, including creating and assigning validation errors. as you'll see in the example, a callback method has the ability to directly add validator "violations". -+----------------+------------------------------------------------------------------------+ -| Applies to | :ref:`class` | -+----------------+------------------------------------------------------------------------+ -| Options | - `methods`_ | -+----------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Callback` | -+----------------+------------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\CallbackValidator` | -+----------------+------------------------------------------------------------------------+ +========== =================================================================== +Applies to :ref:`class ` or :ref:`property/method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Callback` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\CallbackValidator` +========== =================================================================== -Setup ------ +Configuration +------------- .. configuration-block:: - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - constraints: - - Callback: - methods: [isAuthorValid] + .. code-block:: php-attributes - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Context\ExecutionContextInterface; - /** - * @Assert\Callback(methods={"isAuthorValid"}) - */ class Author { + #[Assert\Callback] + public function validate(ExecutionContextInterface $context, mixed $payload): void + { + // ... + } } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + constraints: + - Callback: validate + .. code-block:: xml - - - - - - + + + + + + validate + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addConstraint(new Assert\Callback(array( - 'methods' => array('isAuthorValid'), - ))); + $metadata->addConstraint(new Assert\Callback('validate')); + } + + public function validate(ExecutionContextInterface $context, mixed $payload): void + { + // ... } } The Callback Method ------------------- -The callback method is passed a special ``ExecutionContextInterface`` object. You -can set "violations" directly on this object and determine to which field -those errors should be attributed:: +The callback method is passed a special ``ExecutionContextInterface`` object. +You can set "violations" directly on this object and determine to which +field those errors should be attributed:: // ... - use Symfony\Component\Validator\ExecutionContextInterface; - + use Symfony\Component\Validator\Context\ExecutionContextInterface; + class Author { // ... - private $firstName; - - public function isAuthorValid(ExecutionContextInterface $context) + private string $firstName; + + public function validate(ExecutionContextInterface $context, mixed $payload): void { // somehow you have an array of "fake names" - $fakeNames = array(); - + $fakeNames = [/* ... */]; + // check if the name is actually a fake name if (in_array($this->getFirstName(), $fakeNames)) { - $context->addViolationAt('firstname', 'This name sounds totally fake!', array(), null); + $context->buildViolation('This name sounds totally fake!') + ->atPath('firstName') + ->addViolation(); } } } -Options -------- +Static Callbacks +---------------- -methods -~~~~~~~ +You can also use the constraint with static methods. Since static methods don't +have access to the object instance, they receive the object as the first argument:: -**type**: ``array`` **default**: ``array()`` [:ref:`default option`] + public static function validate(mixed $value, ExecutionContextInterface $context, mixed $payload): void + { + // somehow you have an array of "fake names" + $fakeNames = [/* ... */]; + + // check if the name is actually a fake name + if (in_array($value->getFirstName(), $fakeNames)) { + $context->buildViolation('This name sounds totally fake!') + ->atPath('firstName') + ->addViolation() + ; + } + } -This is an array of the methods that should be executed during the validation -process. Each method can be one of the following formats: +External Callbacks and Closures +------------------------------- -1) **String method name** +If you want to execute a static callback method that is not located in the +class of the validated object, you can configure the constraint to invoke +an array callable as supported by PHP's :phpfunction:`call_user_func` function. +Suppose your validation function is ``Acme\Validator::validate()``:: - If the name of a method is a simple string (e.g. ``isAuthorValid``), that - method will be called on the same object that's being validated and the - ``ExecutionContextInterface`` will be the only argument (see the above example). + namespace Acme; -2) **Static array callback** + use Symfony\Component\Validator\Context\ExecutionContextInterface; - Each method can also be specified as a standard array callback: + class Validator + { + public static function validate(mixed $value, ExecutionContextInterface $context, mixed $payload): void + { + // ... + } + } - .. configuration-block:: +You can then use the following configuration to invoke this validator: - .. code-block:: yaml +.. configuration-block:: - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - constraints: - - Callback: - methods: - - [Acme\BlogBundle\MyStaticValidatorClass, isAuthorValid] + .. code-block:: php-attributes - .. code-block:: php-annotations + // src/Entity/Author.php + namespace App\Entity; - // src/Acme/BlogBundle/Entity/Author.php - use Symfony\Component\Validator\Constraints as Assert; + use Acme\Validator; + use Symfony\Component\Validator\Constraints as Assert; - /** - * @Assert\Callback(methods={ - * { "Acme\BlogBundle\MyStaticValidatorClass", "isAuthorValid"} - * }) - */ - class Author - { - } + #[Assert\Callback([Validator::class, 'validate'])] + class Author + { + } - .. code-block:: xml + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + constraints: + - Callback: [Acme\Validator, validate] + + .. code-block:: xml - - + + + + + - + Acme\Validator + validate + - .. code-block:: php + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php + // src/Entity/Author.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\Callback; + use Acme\Validator; + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; - class Author + class Author + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - public $name; - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addConstraint(new Callback(array( - 'methods' => array('isAuthorValid'), - ))); - } + $metadata->addConstraint(new Assert\Callback([ + Validator::class, + 'validate', + ])); } + } + +.. note:: + + The Callback constraint does *not* support global callback functions + nor is it possible to specify a global function or a service method + as a callback. To validate using a service, you should + :doc:`create a custom validation constraint ` + and add that new constraint to your class. - In this case, the static method ``isAuthorValid`` will be called on the - ``Acme\BlogBundle\MyStaticValidatorClass`` class. It's passed both the original - object being validated (e.g. ``Author``) as well as the ``ExecutionContextInterface``:: +When configuring the constraint via PHP, you can also pass a closure to the +constructor of the Callback constraint:: - namespace Acme\BlogBundle; - - use Symfony\Component\Validator\ExecutionContextInterface; - use Acme\BlogBundle\Entity\Author; - - class MyStaticValidatorClass + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Context\ExecutionContextInterface; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Author + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - public static function isAuthorValid(Author $author, ExecutionContextInterface $context) - { + $callback = function (mixed $value, ExecutionContextInterface $context, mixed $payload): void { // ... - } + }; + + $metadata->addConstraint(new Assert\Callback($callback)); } + } + +.. warning:: + + Using a ``Closure`` together with attribute configuration will disable the + attribute cache for that class/property/method because ``Closure`` cannot + be cached. For best performance, it's recommended to use a static callback method. + +Options +------- + +.. _callback-option: + +``callback`` +~~~~~~~~~~~~ + +**type**: ``string``, ``array`` or ``Closure`` + +The callback option accepts three different formats for specifying the +callback method: + +* A **string** containing the name of a concrete or static method; + +* An array callable with the format ``['', '']``; + +* A closure. + +Concrete callbacks receive an :class:`Symfony\\Component\\Validator\\Context\\ExecutionContextInterface` +instance as the first argument and the :ref:`payload option ` +as the second argument. + +Static or closure callbacks receive the validated object as the first argument, +the :class:`Symfony\\Component\\Validator\\Context\\ExecutionContextInterface` +instance as the second argument and the :ref:`payload option ` +as the third argument. + +.. include:: /reference/constraints/_groups-option.rst.inc - .. tip:: +.. _reference-constraints-callback-payload: - If you specify your ``Callback`` constraint via PHP, then you also have - the option to make your callback either a PHP closure or a non-static - callback. It is *not* currently possible, however, to specify a :term:`service` - as a constraint. To validate using a service, you should - :doc:`create a custom validation constraint` - and add that new constraint to your class. +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/CardScheme.rst b/reference/constraints/CardScheme.rst index 35b63aacb02..a2ed9c568c3 100644 --- a/reference/constraints/CardScheme.rst +++ b/reference/constraints/CardScheme.rst @@ -1,101 +1,124 @@ CardScheme ========== -.. versionadded:: 2.2 - The CardScheme validation is new in Symfony 2.2. - -This constraint ensures that a credit card number is valid for a given credit card -company. It can be used to validate the number before trying to initiate a payment -through a payment gateway. - -+----------------+--------------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+--------------------------------------------------------------------------+ -| Options | - `schemes`_ | -| | - `message`_ | -+----------------+--------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\CardScheme` | -+----------------+--------------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\CardSchemeValidator` | -+----------------+--------------------------------------------------------------------------+ +This constraint ensures that a credit card number is valid for a given credit +card company. It can be used to validate the number before trying to initiate +a payment through a payment gateway. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\CardScheme` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\CardSchemeValidator` +========== =================================================================== Basic Usage ----------- -To use the ``CardScheme`` validator, simply apply it to a property or method -on an object that will contain a credit card number. +To use the ``CardScheme`` validator, apply it to a property or method +on an object that will contain a credit card number. .. configuration-block:: + .. code-block:: php-attributes + + // src/Entity/Transaction.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Transaction + { + #[Assert\CardScheme( + schemes: [Assert\CardScheme::VISA], + message: 'Your credit card number is invalid.', + )] + protected string $cardNumber; + } + .. code-block:: yaml - # src/Acme/SubscriptionBundle/Resources/config/validation.yml - Acme\SubscriptionBundle\Entity\Transaction: + # config/validator/validation.yaml + App\Entity\Transaction: properties: cardNumber: - CardScheme: schemes: [VISA] - message: You credit card number is invalid. + message: Your credit card number is invalid. .. code-block:: xml - - - - - - - - - - - .. code-block:: php-annotations - - // src/Acme/SubscriptionBundle/Entity/Transaction.php - use Symfony\Component\Validator\Constraints as Assert; - - class Transaction - { - /** - * @Assert\CardScheme(schemes = {"VISA"}, message = "You credit card number is invalid.") - */ - protected $cardNumber; - } + + + + + + + + + + + + + .. code-block:: php - // src/Acme/SubscriptionBundle/Entity/Transaction.php + // src/Entity/Transaction.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\CardScheme; class Transaction { - protected $cardNumber; - - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('cardSchema', new CardScheme(array( - 'schemes' => array( - 'VISA' - ), - 'message' => 'You credit card number is invalid.', - ))); + $metadata->addPropertyConstraint('cardNumber', new Assert\CardScheme( + schemes: [ + Assert\CardScheme::VISA, + ], + message: 'Your credit card number is invalid.', + )); } } +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + Available Options ----------------- -schemes -------- +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``Unsupported card type or invalid card number.`` + +The message shown when the value does not pass the ``CardScheme`` check. + +You can use the following parameters in this message: -**type**: ``mixed`` [:ref:`default option`] +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== -This option is required and represents the name of the number scheme used to -validate the credit card number, it can either be a string or an array. Valid -values are: +.. include:: /reference/constraints/_payload-option.rst.inc + +``schemes`` +~~~~~~~~~~~ + +**type**: ``mixed`` + +This option is required and represents the name of the number scheme used +to validate the credit card number, it can either be a string or an array. +Valid values are: * ``AMEX`` * ``CHINA_UNIONPAY`` @@ -106,15 +129,11 @@ values are: * ``LASER`` * ``MAESTRO`` * ``MASTERCARD`` +* ``MIR`` +* ``UATP`` * ``VISA`` -For more information about the used schemes, see `Wikipedia: Issuer identification number (IIN)`_. - -message -~~~~~~~ - -**type**: ``string`` **default**: ``Unsupported card type or invalid card number`` - -The message shown when the value does not pass the ``CardScheme`` check. +For more information about the used schemes, see +`Wikipedia: Issuer identification number (IIN)`_. -.. _`Wikipedia: Issuer identification number (IIN)`: http://en.wikipedia.org/wiki/Bank_card_number#Issuer_identification_number_.28IIN.29 \ No newline at end of file +.. _`Wikipedia: Issuer identification number (IIN)`: https://en.wikipedia.org/wiki/Bank_card_number#Issuer_identification_number_.28IIN.29 diff --git a/reference/constraints/Cascade.rst b/reference/constraints/Cascade.rst new file mode 100644 index 00000000000..3c99f423b0f --- /dev/null +++ b/reference/constraints/Cascade.rst @@ -0,0 +1,98 @@ +Cascade +======= + +The Cascade constraint is used to validate a whole class, including all the +objects that might be stored in its properties. Thanks to this constraint, +you don't need to add the :doc:`/reference/constraints/Valid` constraint on +every child object that you want to validate in your class. + +========== =================================================================== +Applies to :ref:`class ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Cascade` +========== =================================================================== + +Basic Usage +----------- + +In the following example, the +:class:`Symfony\\Component\\Validator\\Constraints\\Cascade` constraint +will tell the validator to validate all properties of the class, including +constraints that are set in the child classes ``BookMetadata`` and +``Author``: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/BookCollection.php + namespace App\Model; + + use App\Model\Author; + use App\Model\BookMetadata; + use Symfony\Component\Validator\Constraints as Assert; + + #[Assert\Cascade] + class BookCollection + { + #[Assert\NotBlank] + protected string $name = ''; + + public BookMetadata $metadata; + + public Author $author; + + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\BookCollection: + constraints: + - Cascade: ~ + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // src/Entity/BookCollection.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class BookCollection + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addConstraint(new Assert\Cascade()); + } + } + +Options +------- + +The ``groups`` option is not available for this constraint. + +``exclude`` +~~~~~~~~~~~ + +**type**: ``array`` | ``string`` **default**: ``null`` + +This option can be used to exclude one or more properties from the +cascade validation. + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Charset.rst b/reference/constraints/Charset.rst new file mode 100644 index 00000000000..084f24cdf76 --- /dev/null +++ b/reference/constraints/Charset.rst @@ -0,0 +1,113 @@ +Charset +======= + +.. versionadded:: 7.1 + + The ``Charset`` constraint was introduced in Symfony 7.1. + +Validates that a string (or an object implementing the ``Stringable`` PHP interface) +is encoded in a given charset. + +========== ===================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Charset` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\CharsetValidator` +========== ===================================================================== + +Basic Usage +----------- + +If you wanted to ensure that the ``content`` property of a ``FileDTO`` +class uses UTF-8, you could do the following: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/FileDTO.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class FileDTO + { + #[Assert\Charset('UTF-8')] + protected string $content; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\FileDTO: + properties: + content: + - Charset: 'UTF-8' + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/FileDTO.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class FileDTO + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('content', new Assert\Charset('UTF-8')); + } + } + +Options +------- + +``encodings`` +~~~~~~~~~~~~~ + +**type**: ``array`` | ``string`` **default**: ``[]`` + +An encoding or a set of encodings to check against. If you pass an array of +encodings, the validator will check if the value is encoded in *any* of the +encodings. This option accepts any value that can be passed to the +:phpfunction:`mb_detect_encoding` PHP function. + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The detected character encoding is invalid ({{ detected }}). Allowed encodings are {{ encodings }}.`` + +This is the message that will be shown if the value does not match any of the +accepted encodings. + +You can use the following parameters in this message: + +=================== ============================================================== +Parameter Description +=================== ============================================================== +``{{ detected }}`` The detected encoding +``{{ encodings }}`` The accepted encodings +=================== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Choice.rst b/reference/constraints/Choice.rst index 00504eec65e..cdf6b6e47fd 100644 --- a/reference/constraints/Choice.rst +++ b/reference/constraints/Choice.rst @@ -5,24 +5,11 @@ This constraint is used to ensure that the given value is one of a given set of *valid* choices. It can also be used to validate that each item in an array of items is one of those valid choices. -+----------------+-----------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+-----------------------------------------------------------------------+ -| Options | - `choices`_ | -| | - `callback`_ | -| | - `multiple`_ | -| | - `min`_ | -| | - `max`_ | -| | - `message`_ | -| | - `multipleMessage`_ | -| | - `minMessage`_ | -| | - `maxMessage`_ | -| | - `strict`_ | -+----------------+-----------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Choice` | -+----------------+-----------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\ChoiceValidator` | -+----------------+-----------------------------------------------------------------------+ +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Choice` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\ChoiceValidator` +========== =================================================================== Basic Usage ----------- @@ -36,64 +23,87 @@ If your valid choice list is simple, you can pass them in directly via the .. configuration-block:: - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - gender: - - Choice: - choices: [male, female] - message: Choose a valid gender. - - .. code-block:: php-annotations + .. code-block:: php-attributes - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\Choice(choices = {"male", "female"}, message = "Choose a valid gender.") - */ - protected $gender; + public const GENRES = ['fiction', 'non-fiction']; + + #[Assert\Choice(['New York', 'Berlin', 'Tokyo'])] + protected string $city; + + #[Assert\Choice(choices: Author::GENRES, message: 'Choose a valid genre.')] + protected string $genre; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + city: + - Choice: [New York, Berlin, Tokyo] + genre: + - Choice: + choices: [fiction, non-fiction] + message: Choose a valid genre. + .. code-block:: xml - - - - - - - - - + + + + + + + + New York + Berlin + Tokyo + + + + + + + + + + .. code-block:: php - // src/Acme/BlogBundle/EntityAuthor.php - namespace Acme\BlogBundle\Entity; + // src/EntityAuthor.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; - + use Symfony\Component\Validator\Mapping\ClassMetadata; + class Author { - protected $gender; - - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('gender', new Assert\Choice(array( - 'choices' => array('male', 'female'), - 'message' => 'Choose a valid gender.', - ))); + $metadata->addPropertyConstraint( + 'city', + new Assert\Choice(['New York', 'Berlin', 'Tokyo']) + ); + + $metadata->addPropertyConstraint('genre', new Assert\Choice( + choices: ['fiction', 'non-fiction'], + message: 'Choose a valid genre.', + )); } } @@ -102,232 +112,280 @@ Supplying the Choices with a Callback Function You can also use a callback function to specify your options. This is useful if you want to keep your choices in some central location so that, for example, -you can easily access those choices for validation or for building a select -form element. - -.. code-block:: php +you can access those choices for validation or for building a select form element:: - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; class Author { - public static function getGenders() + public static function getGenres(): array { - return array('male', 'female'); + return ['fiction', 'non-fiction']; } } -You can pass the name of this method to the `callback_` option of the ``Choice`` +You can pass the name of this method to the `callback`_ option of the ``Choice`` constraint. .. configuration-block:: - .. code-block:: yaml + .. code-block:: php-attributes - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - gender: - - Choice: { callback: getGenders } - - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\Choice(callback = "getGenders") - */ - protected $gender; + #[Assert\Choice(callback: 'getGenres')] + protected string $genre; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + genre: + - Choice: { callback: getGenres } + .. code-block:: xml - - - - - - - - + + + + + + + + + + + + .. code-block:: php - // src/Acme/BlogBundle/EntityAuthor.php - namespace Acme\BlogBundle\Entity; + // src/EntityAuthor.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; - + use Symfony\Component\Validator\Mapping\ClassMetadata; + class Author { - protected $gender; - - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('gender', new Assert\Choice(array( - 'callback' => 'getGenders', - ))); + $metadata->addPropertyConstraint('genre', new Assert\Choice( + callback: 'getGenres', + )); } } -If the static callback is stored in a different class, for example ``Util``, +If the callback is defined in a different class and is static, for example ``App\Entity\Genre``, you can pass the class name and the method as an array. .. configuration-block:: - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - gender: - - Choice: { callback: [Util, getGenders] } + .. code-block:: php-attributes - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; + use App\Entity\Genre; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\Choice(callback = {"Util", "getGenders"}) - */ - protected $gender; + #[Assert\Choice(callback: [Genre::class, 'getGenres'])] + protected string $genre; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + genre: + - Choice: { callback: [App\Entity\Genre, getGenres] } + .. code-block:: xml - - - - - - - - + + + + + + + + + + + + .. code-block:: php - // src/Acme/BlogBundle/EntityAuthor.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; + use App\Entity\Genre; use Symfony\Component\Validator\Constraints as Assert; - + use Symfony\Component\Validator\Mapping\ClassMetadata; + class Author { - protected $gender; - - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('gender', new Assert\Choice(array( - 'callback' => array('Util', 'getGenders'), - ))); + $metadata->addPropertyConstraint('genre', new Assert\Choice( + callback: [Genre::class, 'getGenres'], + )); } } Available Options ----------------- -choices -~~~~~~~ +``callback`` +~~~~~~~~~~~~ + +**type**: ``callable|string|null`` **default**: ``null`` + +This is a callback method that can be used instead of the `choices`_ option +to return the choices array. See +`Supplying the Choices with a Callback Function`_ for details on its usage. + +``choices`` +~~~~~~~~~~~ -**type**: ``array`` [:ref:`default option`] +**type**: ``array`` A required option (unless `callback`_ is specified) - this is the array of options that should be considered in the valid set. The input value will be matched against this array. -callback -~~~~~~~~ +.. include:: /reference/constraints/_groups-option.rst.inc -**type**: ``string|array|Closure`` +``max`` +~~~~~~~ -This is a callback method that can be used instead of the `choices`_ option -to return the choices array. See `Supplying the Choices with a Callback Function`_ -for details on its usage. +**type**: ``integer`` -multiple -~~~~~~~~ +If the ``multiple`` option is true, then you can use the ``max`` option +to force no more than XX number of values to be selected. For example, if +``max`` is 3, but the input array contains 4 valid items, the validation +will fail. -**type**: ``Boolean`` **default**: ``false`` +``maxMessage`` +~~~~~~~~~~~~~~ -If this option is true, the input value is expected to be an array instead -of a single, scalar value. The constraint will check that each value of -the input array can be found in the array of valid choices. If even one -of the input values cannot be found, the validation will fail. +**type**: ``string`` **default**: ``You must select at most {{ limit }} choices.`` -min -~~~ +This is the validation error message that's displayed when the user chooses +too many options per the `max`_ option. -**type**: ``integer`` +You can use the following parameters in this message: -If the ``multiple`` option is true, then you can use the ``min`` option -to force at least XX number of values to be selected. For example, if -``min`` is 3, but the input array only contains 2 valid items, the validation -will fail. +================= ============================================================ +Parameter Description +================= ============================================================ +``{{ choices }}`` A comma-separated list of available choices +``{{ value }}`` The current (invalid) value +================= ============================================================ -max -~~~ +match +~~~~~ -**type**: ``integer`` +**type**: ``boolean`` **default**: ``true`` -If the ``multiple`` option is true, then you can use the ``max`` option -to force no more than XX number of values to be selected. For example, if -``max`` is 3, but the input array contains 4 valid items, the validation -will fail. +When this option is ``false``, the constraint checks that the given value is +not one of the values defined in the ``choices`` option. In practice, it makes +the ``Choice`` constraint behave like a ``NotChoice`` constraint. -message -~~~~~~~ +``message`` +~~~~~~~~~~~ -**type**: ``string`` **default**: ``The value you selected is not a valid choice`` +**type**: ``string`` **default**: ``The value you selected is not a valid choice.`` -This is the message that you will receive if the ``multiple`` option is set -to ``false``, and the underlying value is not in the valid array of choices. +This is the message that you will receive if the ``multiple`` option is +set to ``false`` and the underlying value is not in the valid array of +choices. -multipleMessage -~~~~~~~~~~~~~~~ +You can use the following parameters in this message: -**type**: ``string`` **default**: ``One or more of the given values is invalid`` +================= ============================================================ +Parameter Description +================= ============================================================ +``{{ choices }}`` A comma-separated list of available choices +``{{ value }}`` The current (invalid) value +================= ============================================================ -This is the message that you will receive if the ``multiple`` option is set -to ``true``, and one of the values on the underlying array being checked -is not in the array of valid choices. +``min`` +~~~~~~~ -minMessage -~~~~~~~~~~ +**type**: ``integer`` -**type**: ``string`` **default**: ``You must select at least {{ limit }} choices`` +If the ``multiple`` option is true, then you can use the ``min`` option +to force at least XX number of values to be selected. For example, if +``min`` is 3, but the input array only contains 2 valid items, the validation +will fail. + +``minMessage`` +~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``You must select at least {{ limit }} choices.`` This is the validation error message that's displayed when the user chooses too few choices per the `min`_ option. -maxMessage -~~~~~~~~~~ +You can use the following parameters in this message: -**type**: ``string`` **default**: ``You must select at most {{ limit }} choices`` +================= ============================================================ +Parameter Description +================= ============================================================ +``{{ choices }}`` A comma-separated list of available choices +``{{ value }}`` The current (invalid) value +================= ============================================================ -This is the validation error message that's displayed when the user chooses -too many options per the `max`_ option. +``multiple`` +~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +If this option is true, the input value is expected to be an array instead +of a single, scalar value. The constraint will check that each value of +the input array can be found in the array of valid choices. If even one +of the input values cannot be found, the validation will fail. + +``multipleMessage`` +~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``One or more of the given values is invalid.`` + +This is the message that you will receive if the ``multiple`` option is +set to ``true`` and one of the values on the underlying array being checked +is not in the array of valid choices. -strict -~~~~~~ +You can use the following parameters in this message: -**type**: ``Boolean`` **default**: ``false`` +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== -If true, the validator will also check the type of the input value. Specifically, -this value is passed to as the third argument to the PHP :phpfunction:`in_array` method -when checking to see if a value is in the valid choices array. +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Cidr.rst b/reference/constraints/Cidr.rst new file mode 100644 index 00000000000..78a5b6c7167 --- /dev/null +++ b/reference/constraints/Cidr.rst @@ -0,0 +1,141 @@ +Cidr +==== + +Validates that a value is a valid `CIDR`_ (Classless Inter-Domain Routing) notation. +By default, this will validate the CIDR's IP and netmask both for version 4 and 6, +with the option of allowing only one type of IP version to be valid. It also supports +a minimum and maximum range constraint in which the value of the netmask is valid. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Cidr` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\CidrValidator` +========== =================================================================== + +Basic Usage +----------- + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/NetworkSettings.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class NetworkSettings + { + #[Assert\Cidr] + protected string $cidrNotation; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\NetworkSettings: + properties: + cidrNotation: + - Cidr: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/NetworkSettings.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class NetworkSettings + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('cidrNotation', new Assert\Cidr()); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid CIDR notation.`` + +This message is shown if the string is not a valid CIDR notation. + +``netmaskMin`` +~~~~~~~~~~~~~~ + +**type**: ``integer`` **default**: ``0`` + +It's a constraint for the lowest value a valid netmask may have. + +``netmaskMax`` +~~~~~~~~~~~~~~ + +**type**: ``integer`` **default**: ``32`` for IPv4 or ``128`` for IPv6 + +It's a constraint for the biggest value a valid netmask may have. + +``netmaskRangeViolationMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The value of the netmask should be between {{ min }} and {{ max }}.`` + +This message is shown if the value of the CIDR's netmask is bigger than the +``netmaskMax`` value or lower than the ``netmaskMin`` value. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ min }}`` The minimum value a CIDR netmask may have +``{{ max }}`` The maximum value a CIDR netmask may have +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +``version`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``all`` + +This determines exactly *how* the CIDR notation is validated and can take one +of :ref:`IP version ranges `. + +.. note:: + + The IP range checks (e.g., ``*_private``, ``*_reserved``) validate only the + IP address, not the entire netmask. To improve validation, you can set the + ``{{ min }}`` value for the netmask. For example, the range ``9.0.0.0/6`` is + considered ``*_public``, but it also includes the ``10.0.0.0/8`` range, which + is categorized as ``*_private``. + +.. versionadded:: 7.1 + + The support of all IP version ranges was introduced in Symfony 7.1. + +.. _`CIDR`: https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing diff --git a/reference/constraints/Collection.rst b/reference/constraints/Collection.rst index 9fd0c1160c0..c35a0103581 100644 --- a/reference/constraints/Collection.rst +++ b/reference/constraints/Collection.rst @@ -5,273 +5,383 @@ This constraint is used when the underlying data is a collection (i.e. an array or an object that implements ``Traversable`` and ``ArrayAccess``), but you'd like to validate different keys of that collection in different ways. For example, you might validate the ``email`` key using the ``Email`` -constraint and the ``inventory`` key of the collection with the ``Range`` constraint. +constraint and the ``inventory`` key of the collection with the ``Range`` +constraint. This constraint can also make sure that certain collection keys are present and that extra keys are not present. -+----------------+--------------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+--------------------------------------------------------------------------+ -| Options | - `fields`_ | -| | - `allowExtraFields`_ | -| | - `extraFieldsMessage`_ | -| | - `allowMissingFields`_ | -| | - `missingFieldsMessage`_ | -+----------------+--------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Collection` | -+----------------+--------------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\CollectionValidator` | -+----------------+--------------------------------------------------------------------------+ +.. seealso:: + + If you want to validate that all the elements of the collection are unique + use the :doc:`Unique constraint `. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Collection` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\CollectionValidator` +========== =================================================================== Basic Usage ----------- -The ``Collection`` constraint allows you to validate the different keys of -a collection individually. Take the following example:: +The ``Collection`` constraint allows you to validate the different keys +of a collection individually. Take the following example:: - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; class Author { - protected $profileData = array( - 'personal_email', - 'short_bio', - ); + protected array $profileData = [ + 'personal_email' => '...', + 'short_bio' => '...', + ]; - public function setProfileData($key, $value) + public function setProfileData($key, $value): void { $this->profileData[$key] = $value; } } To validate that the ``personal_email`` element of the ``profileData`` array -property is a valid email address and that the ``short_bio`` element is not -blank but is no longer than 100 characters in length, you would do the following: +property is a valid email address and that the ``short_bio`` element is +not blank but is no longer than 100 characters in length, you would do the +following: .. configuration-block:: + .. code-block:: php-attributes + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + #[Assert\Collection( + fields: [ + 'personal_email' => new Assert\Email, + 'short_bio' => [ + new Assert\NotBlank, + new Assert\Length( + max: 100, + maxMessage: 'Your short bio is too long!' + ) + ] + ], + allowMissingFields: true, + )] + protected array $profileData = [ + 'personal_email' => '...', + 'short_bio' => '...', + ]; + } + .. code-block:: yaml - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: + # config/validator/validation.yaml + App\Entity\Author: properties: profileData: - Collection: fields: - personal_email: Email + personal_email: + - Email: ~ short_bio: - - NotBlank + - NotBlank: ~ - Length: max: 100 maxMessage: Your short bio is too long! allowMissingFields: true - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\Collection( - * fields = { - * "personal_email" = @Assert\Email, - * "short_bio" = { - * @Assert\NotBlank(), - * @Assert\Length( - * max = 100, - * maxMessage = "Your bio is too long!" - * ) - * } - * }, - * allowMissingFields = true - * ) - */ - protected $profileData = array( - 'personal_email', - 'short_bio', - ); - } - .. code-block:: xml - - - - - - - - - + + + + + + + + + + + + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - private $options = array(); + // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('profileData', new Assert\Collection(array( - 'fields' => array( + $metadata->addPropertyConstraint('profileData', new Assert\Collection( + fields: [ 'personal_email' => new Assert\Email(), - 'lastName' => array( + 'short_bio' => [ new Assert\NotBlank(), - new Assert\Length(array("max" => 100)), - ), - ), - 'allowMissingFields' => true, - ))); + new Assert\Length([ + 'max' => 100, + 'maxMessage' => 'Your short bio is too long!', + ]), + ], + ], + allowMissingFields: true, + )); } } Presence and Absence of Fields ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -By default, this constraint validates more than simply whether or not the -individual fields in the collection pass their assigned constraints. In fact, -if any keys of a collection are missing or if there are any unrecognized +By default, this constraint validates more than whether or not the +individual fields in the collection pass their assigned constraints. In +fact, if any keys of a collection are missing or if there are any unrecognized keys in the collection, validation errors will be thrown. -If you would like to allow for keys to be absent from the collection or if -you would like "extra" keys to be allowed in the collection, you can modify -the `allowMissingFields`_ and `allowExtraFields`_ options respectively. In -the above example, the ``allowMissingFields`` option was set to true, meaning -that if either of the ``personal_email`` or ``short_bio`` elements were missing -from the ``$personalData`` property, no validation error would occur. - -.. versionadded:: 2.1 - The ``Required`` and ``Optional`` constraints are new to Symfony 2.1. +If you would like to allow for keys to be absent from the collection or +if you would like "extra" keys to be allowed in the collection, you can +modify the `allowMissingFields`_ and `allowExtraFields`_ options respectively. +In the above example, the ``allowMissingFields`` option was set to true, +meaning that if either of the ``personal_email`` or ``short_bio`` elements +were missing from the ``$personalData`` property, no validation error would +occur. Required and Optional Field Constraints ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Constraints for fields within a collection can be wrapped in the ``Required`` or -``Optional`` constraint to control whether they should always be applied (``Required``) -or only applied when the field is present (``Optional``). +Constraints for fields within a collection can be wrapped in the ``Required`` +or ``Optional`` constraint to control whether they should always be applied +(``Required``) or only applied when the field is present (``Optional``). -For instance, if you want to require that the ``personal_email`` field of the -``profileData`` array is not blank and is a valid email but the ``alternate_email`` -field is optional but must be a valid email if supplied, you can do the following: +For instance, if you want to require that the ``personal_email`` field of +the ``profileData`` array is not blank and is a valid email but the +``alternate_email`` field is optional but must be a valid email if supplied, +you can do the following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\Collection( - * fields={ - * "personal_email" = @Assert\Collection\Required({@Assert\NotBlank, @Assert\Email}), - * "alternate_email" = @Assert\Collection\Optional({@Assert\Email}), - * } - * ) - */ - protected $profileData = array( - 'personal_email', - ); + #[Assert\Collection( + fields: [ + 'personal_email' => new Assert\Required([ + new Assert\NotBlank, + new Assert\Email, + ]), + 'alternate_email' => new Assert\Optional( + new Assert\Email + ), + ], + )] + protected array $profileData = ['personal_email' => 'email@example.com']; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + profile_data: + - Collection: + fields: + personal_email: + - Required: + - NotBlank: ~ + - Email: ~ + alternate_email: + - Optional: + - Email: ~ + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - protected $profileData = array('personal_email'); + // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('profileData', new Assert\Collection(array( - 'fields' => array( - 'personal_email' => new Assert\Collection\Required(array(new Assert\NotBlank(), new Assert\Email())), - 'alternate_email' => new Assert\Collection\Optional(array(new Assert\Email())), - ), - ))); + $metadata->addPropertyConstraint('profileData', new Assert\Collection( + fields: [ + 'personal_email' => new Assert\Required([ + new Assert\NotBlank(), + new Assert\Email(), + ]), + 'alternate_email' => new Assert\Optional(new Assert\Email()), + ], + )); } } Even without ``allowMissingFields`` set to true, you can now omit the ``alternate_email`` property completely from the ``profileData`` array, since it is ``Optional``. -However, if the the ``personal_email`` field does not exist in the array, +However, if the ``personal_email`` field does not exist in the array, the ``NotBlank`` constraint will still be applied (since it is wrapped in ``Required``) and you will receive a constraint violation. +When you define groups in nested constraints they are automatically added to +the ``Collection`` constraint itself so it can be traversed for all nested +groups. Take the following example:: + + use Symfony\Component\Validator\Constraints as Assert; + + $constraint = new Assert\Collection( + fields: [ + 'name' => new Assert\NotBlank(['groups' => 'basic']), + 'email' => new Assert\NotBlank(['groups' => 'contact']), + ], + ); + +This will result in the following configuration:: + + $constraint = new Assert\Collection( + fields: [ + 'name' => new Assert\Required( + constraints: new Assert\NotBlank(groups: ['basic']), + groups: ['basic', 'strict'], + ), + 'email' => new Assert\Required( + constraints: new Assert\NotBlank(groups: ['contact']), + groups: ['basic', 'strict'], + ), + ], + groups: ['basic', 'strict'], + ); + +The default ``allowMissingFields`` option requires the fields in all groups. +So when validating in ``contact`` group, ``$name`` can be empty but the key is +still required. If this is not the intended behavior, use the ``Optional`` +constraint explicitly instead of ``Required``. + Options ------- -fields -~~~~~~ +``allowExtraFields`` +~~~~~~~~~~~~~~~~~~~~ -**type**: ``array`` [:ref:`default option`] +**type**: ``boolean`` **default**: ``false`` -This option is required, and is an associative array defining all of the -keys in the collection and, for each key, exactly which validator(s) should -be executed against that element of the collection. +If this option is set to ``false`` and the underlying collection contains +one or more elements that are not included in the `fields`_ option, a validation +error will be returned. If set to ``true``, extra fields are OK. -allowExtraFields -~~~~~~~~~~~~~~~~ +``allowMissingFields`` +~~~~~~~~~~~~~~~~~~~~~~ -**type**: ``Boolean`` **default**: false +**type**: ``boolean`` **default**: ``false`` -If this option is set to ``false`` and the underlying collection contains -one or more elements that are not included in the `fields`_ option, a validation -error will be returned. If set to ``true``, extra fields are ok. +If this option is set to ``false`` and one or more fields from the `fields`_ +option are not present in the underlying collection, a validation error +will be returned. If set to ``true``, it's OK if some fields in the `fields`_ +option are not present in the underlying collection. -extraFieldsMessage -~~~~~~~~~~~~~~~~~~ +``extraFieldsMessage`` +~~~~~~~~~~~~~~~~~~~~~~ -**type**: ``Boolean`` **default**: ``The fields {{ fields }} were not expected`` +**type**: ``string`` **default**: ``This field was not expected.`` -The message shown if `allowExtraFields`_ is false and an extra field is detected. +The message shown if `allowExtraFields`_ is false and an extra field is +detected. -allowMissingFields -~~~~~~~~~~~~~~~~~~ +You can use the following parameters in this message: -**type**: ``Boolean`` **default**: false +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ field }}`` The key of the extra field detected +=============== ============================================================== -If this option is set to ``false`` and one or more fields from the `fields`_ -option are not present in the underlying collection, a validation error will -be returned. If set to ``true``, it's ok if some fields in the `fields_` -option are not present in the underlying collection. +``fields`` +~~~~~~~~~~ -missingFieldsMessage -~~~~~~~~~~~~~~~~~~~~ +**type**: ``array`` + +This option is required and is an associative array defining all of the +keys in the collection and, for each key, exactly which validator(s) should +be executed against that element of the collection. + +.. include:: /reference/constraints/_groups-option.rst.inc + +``missingFieldsMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~ -**type**: ``Boolean`` **default**: ``The fields {{ fields }} are missing`` +**type**: ``string`` **default**: ``This field is missing.`` The message shown if `allowMissingFields`_ is false and one or more fields are missing from the underlying collection. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ field }}`` The key of the missing field defined in ``fields`` +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Compound.rst b/reference/constraints/Compound.rst new file mode 100644 index 00000000000..4d2c7743176 --- /dev/null +++ b/reference/constraints/Compound.rst @@ -0,0 +1,154 @@ +Compound +======== + +To the contrary to the other constraints, this constraint cannot be used on its own. +Instead, it allows you to create your own set of reusable constraints, representing +rules to use consistently across your application, by extending the constraint. + +========== =================================================================== +Applies to :ref:`class ` or :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Compound` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\CompoundValidator` +========== =================================================================== + +Basic Usage +----------- + +Suppose that you have different places where a user password must be validated, +you can create your own named set or requirements to be reused consistently everywhere: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Validator/Constraints/PasswordRequirements.php + namespace App\Validator\Constraints; + + use Symfony\Component\Validator\Constraints\Compound; + use Symfony\Component\Validator\Constraints as Assert; + + #[\Attribute] + class PasswordRequirements extends Compound + { + protected function getConstraints(array $options): array + { + return [ + new Assert\NotBlank(), + new Assert\Type('string'), + new Assert\Length(min: 12), + new Assert\NotCompromisedPassword(), + new Assert\PasswordStrength(minScore: 4), + ]; + } + } + +Add ``#[\Attribute]`` to the constraint class if you want to +use it as an attribute in other classes. If the constraint has +configuration options, define them as public properties on the constraint class. + +You can now use it anywhere you need it: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity\User; + + use App\Validator\Constraints as Assert; + + class User + { + #[Assert\PasswordRequirements] + public string $plainPassword; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\User: + properties: + plainPassword: + - App\Validator\Constraints\PasswordRequirements: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/User.php + namespace App\Entity\User; + + use App\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class User + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('plainPassword', new Assert\PasswordRequirements()); + } + } + +Validation groups and payload can be passed via constructor: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity\User; + + use App\Validator\Constraints as Assert; + + class User + { + #[Assert\PasswordRequirements( + groups: ['registration'], + payload: ['severity' => 'error'], + )] + public string $plainPassword; + } + + .. code-block:: php + + // src/Entity/User.php + namespace App\Entity\User; + + use App\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class User + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('plainPassword', new Assert\PasswordRequirements( + groups: ['registration'], + payload: ['severity' => 'error'], + )); + } + } + +.. versionadded:: 7.2 + + Support for passing validation groups and the payload to the constructor + of the ``Compound`` class was introduced in Symfony 7.2. + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Count.rst b/reference/constraints/Count.rst index 9c808e9576d..d33c54c0812 100644 --- a/reference/constraints/Count.rst +++ b/reference/constraints/Count.rst @@ -1,25 +1,14 @@ Count ===== -Validates that a given collection's (i.e. an array or an object that implements Countable) -element count is *between* some minimum and maximum value. - -.. versionadded:: 2.1 - The Count constraint was added in Symfony 2.1. - -+----------------+---------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+---------------------------------------------------------------------+ -| Options | - `min`_ | -| | - `max`_ | -| | - `minMessage`_ | -| | - `maxMessage`_ | -| | - `exactMessage`_ | -+----------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Count` | -+----------------+---------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\CountValidator` | -+----------------+---------------------------------------------------------------------+ +Validates that a given collection's (i.e. an array or an object that implements +Countable) element count is *between* some minimum and maximum value. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Count` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\CountValidator` +========== =================================================================== Basic Usage ----------- @@ -29,110 +18,183 @@ you might add the following: .. configuration-block:: - .. code-block:: yaml + .. code-block:: php-attributes - # src/Acme/EventBundle/Resources/config/validation.yml - Acme\EventBundle\Entity\Participant: - properties: - emails: - - Count: - min: 1 - max: 5 - minMessage: "You must specify at least one email" - maxMessage: "You cannot specify more than {{ limit }} emails" - - .. code-block:: php-annotations - - // src/Acme/EventBundle/Entity/Participant.php - namespace Acme\EventBundle\Entity; + // src/Entity/Participant.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; class Participant { - /** - * @Assert\Count( - * min = "1", - * max = "5", - * minMessage = "You must specify at least one email", - * maxMessage = "You cannot specify more than {{ limit }} emails" - * ) - */ - protected $emails = array(); + #[Assert\Count( + min: 1, + max: 5, + minMessage: 'You must specify at least one email', + maxMessage: 'You cannot specify more than {{ limit }} emails', + )] + protected array $emails = []; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Participant: + properties: + emails: + - Count: + min: 1 + max: 5 + minMessage: 'You must specify at least one email' + maxMessage: 'You cannot specify more than {{ limit }} emails' + .. code-block:: xml - - - - - - - - - - - + + + + + + + + + + + + + + + .. code-block:: php - // src/Acme/EventBundle/Entity/Participant.php - namespace Acme\EventBundle\Entity; + // src/Entity/Participant.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Participant { - public static function loadValidatorMetadata(ClassMetadata $data) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('emails', new Assert\Count(array( - 'min' => 1, - 'max' => 5, - 'minMessage' => 'You must specify at least one email', - 'maxMessage' => 'You cannot specify more than {{ limit }} emails', - ))); + $metadata->addPropertyConstraint('emails', new Assert\Count( + min: 1, + max: 5, + minMessage: 'You must specify at least one email', + maxMessage: 'You cannot specify more than {{ limit }} emails', + )); } } Options ------- -min -~~~ +``divisibleBy`` +~~~~~~~~~~~~~~~ -**type**: ``integer`` [:ref:`default option`] +**type**: ``integer`` **default**: ``null`` -This required option is the "min" count value. Validation will fail if the given -collection elements count is **less** than this min value. +Validates that the number of elements of the given collection is divisible by +a certain number. -max -~~~ +.. seealso:: -**type**: ``integer`` [:ref:`default option`] + If you need to validate that other types of data different from collections + are divisible by a certain number, use the + :doc:`DivisibleBy ` constraint. -This required option is the "max" count value. Validation will fail if the given +``divisibleByMessage`` +~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The number of elements in this collection should be a multiple of {{ compared_value }}.`` + +The message that will be shown if the number of elements of the given collection +is not divisible by the number defined in the ``divisibleBy`` option. + +You can use the following parameters in this message: + +======================== =================================================== +Parameter Description +======================== =================================================== +``{{ compared_value }}`` The number configured in the ``divisibleBy`` option +======================== =================================================== + +``exactMessage`` +~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This collection should contain exactly {{ limit }} elements.`` + +The message that will be shown if min and max values are equal and the underlying +collection elements count is not exactly this value. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ count }}`` The current collection size +``{{ limit }}`` The exact expected collection size +=============== ============================================================== + +.. include:: /reference/constraints/_groups-option.rst.inc + +``max`` +~~~~~~~ + +**type**: ``integer`` + +This option is the "max" count value. Validation will fail if the given collection elements count is **greater** than this max value. -minMessage -~~~~~~~~~~ +This option is required when the ``min`` option is not defined. + +``maxMessage`` +~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This collection should contain {{ limit }} elements or less.`` + +The message that will be shown if the underlying collection elements count +is more than the `max`_ option. -**type**: ``string`` **default**: ``This collection should contain {{ limit }} elements or more.``. +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ count }}`` The current collection size +``{{ limit }}`` The upper limit +=============== ============================================================== + +``min`` +~~~~~~~ + +**type**: ``integer`` + +This option is the "min" count value. Validation will fail if the given +collection elements count is **less** than this min value. -The message that will be shown if the underlying collection elements count is less than the `min`_ option. +This option is required when the ``max`` option is not defined. -maxMessage -~~~~~~~~~~ +``minMessage`` +~~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``This collection should contain {{ limit }} elements or less.``. +**type**: ``string`` **default**: ``This collection should contain {{ limit }} elements or more.`` -The message that will be shown if the underlying collection elements count is more than the `max`_ option. +The message that will be shown if the underlying collection elements count +is less than the `min`_ option. -exactMessage -~~~~~~~~~~~~ +You can use the following parameters in this message: -**type**: ``string`` **default**: ``This collection should contain exactly {{ limit }} elements.``. +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ count }}`` The current collection size +``{{ limit }}`` The lower limit +=============== ============================================================== -The message that will be shown if min and max values are equal and the underlying collection elements -count is not exactly this value. +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Country.rst b/reference/constraints/Country.rst index cc5ec176fb0..2f75b1c1354 100644 --- a/reference/constraints/Country.rst +++ b/reference/constraints/Country.rst @@ -1,77 +1,105 @@ Country ======= -Validates that a value is a valid two-letter country code. - -+----------------+------------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+------------------------------------------------------------------------+ -| Options | - `message`_ | -+----------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Country` | -+----------------+------------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\CountryValidator` | -+----------------+------------------------------------------------------------------------+ +Validates that a value is a valid `ISO 3166-1 alpha-2`_ country code. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Country` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\CountryValidator` +========== =================================================================== Basic Usage ----------- .. configuration-block:: - .. code-block:: yaml - - # src/UserBundle/Resources/config/validation.yml - Acme\UserBundle\Entity\User: - properties: - country: - - Country: - - .. code-block:: php-annotations + .. code-block:: php-attributes - // src/Acme/UserBundle/Entity/User.php - namespace Acme\UserBundle\Entity; + // src/Entity/User.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; class User { - /** - * @Assert\Country - */ - protected $country; + #[Assert\Country] + protected string $country; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\User: + properties: + country: + - Country: ~ + .. code-block:: xml - - - - - - + + + + + + + + + + .. code-block:: php - // src/Acme/UserBundle/Entity/User.php - namespace Acme\UserBundle\Entity; + // src/Entity/User.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class User { - public static function loadValidationMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidationMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('country', new Assert\Country()); } } +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + Options ------- -message -~~~~~~~ +alpha3 +~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +If this option is ``true``, the constraint checks that the value is a +`ISO 3166-1 alpha-3`_ three-letter code (e.g. France = ``FRA``) instead +of the default `ISO 3166-1 alpha-2`_ two-letter code (e.g. France = ``FR``). -**type**: ``string`` **default**: ``This value is not a valid country`` +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid country.`` This message is shown if the string is not a valid country code. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) country code +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`ISO 3166-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes +.. _`ISO 3166-1 alpha-3`: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3#Current_codes diff --git a/reference/constraints/CssColor.rst b/reference/constraints/CssColor.rst new file mode 100644 index 00000000000..b9c78ec25ac --- /dev/null +++ b/reference/constraints/CssColor.rst @@ -0,0 +1,274 @@ +CssColor +======== + +Validates that a value is a valid CSS color. The underlying value is +cast to a string before being validated. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\CssColor` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\CssColorValidator` +========== =================================================================== + +Basic Usage +----------- + +In the following example, the ``$defaultColor`` value must be a CSS color +defined in any of the valid CSS formats (e.g. ``red``, ``#369``, +``hsla(0, 0%, 20%, 0.4)``); the ``$accentColor`` must be a CSS color defined in +hexadecimal format; and ``$currentColor`` must be a CSS color defined as any of +the named CSS colors: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Bulb.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Bulb + { + #[Assert\CssColor] + protected string $defaultColor; + + #[Assert\CssColor( + formats: Assert\CssColor::HEX_LONG, + message: 'The accent color must be a 6-character hexadecimal color.', + )] + protected string $accentColor; + + #[Assert\CssColor( + formats: [Assert\CssColor::BASIC_NAMED_COLORS, Assert\CssColor::EXTENDED_NAMED_COLORS], + message: 'The color '{{ value }}' is not a valid CSS color name.', + )] + protected string $currentColor; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Bulb: + properties: + defaultColor: + - CssColor: ~ + accentColor: + - CssColor: + formats: !php/const Symfony\Component\Validator\Constraints\CssColor::HEX_LONG + message: The accent color must be a 6-character hexadecimal color. + currentColor: + - CssColor: + formats: + - !php/const Symfony\Component\Validator\Constraints\CssColor::BASIC_NAMED_COLORS + - !php/const Symfony\Component\Validator\Constraints\CssColor::EXTENDED_NAMED_COLORS + message: The color "{{ value }}" is not a valid CSS color name. + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Bulb.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Bulb + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('defaultColor', new Assert\CssColor()); + + $metadata->addPropertyConstraint('accentColor', new Assert\CssColor( + formats: Assert\CssColor::HEX_LONG, + message: 'The accent color must be a 6-character hexadecimal color.', + )); + + $metadata->addPropertyConstraint('currentColor', new Assert\CssColor( + formats: [Assert\CssColor::BASIC_NAMED_COLORS, Assert\CssColor::EXTENDED_NAMED_COLORS], + message: 'The color "{{ value }}" is not a valid CSS color name.', + )); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +message +~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid CSS color.`` + +This message is shown if the underlying data is not a valid CSS color. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +=============== ============================================================== + +formats +~~~~~~~ + +**type**: ``string`` | ``array`` + +By default, this constraint considers valid any of the many ways of defining +CSS colors. Use the ``formats`` option to restrict which CSS formats are allowed. +These are the available formats (which are also defined as PHP constants; e.g. +``Assert\CssColor::HEX_LONG``): + +* ``hex_long`` +* ``hex_long_with_alpha`` +* ``hex_short`` +* ``hex_short_with_alpha`` +* ``basic_named_colors`` +* ``extended_named_colors`` +* ``system_colors`` +* ``keywords`` +* ``rgb`` +* ``rgba`` +* ``hsl`` +* ``hsla`` + +hex_long +........ + +A regular expression. Allows all values which represent a CSS color of 6 +characters (in addition of the leading ``#``) and contained in ranges: ``0`` to +``9`` and ``A`` to ``F`` (case insensitive). + +Examples: ``#2F2F2F``, ``#2f2f2f`` + +hex_long_with_alpha +................... + +A regular expression. Allows all values which represent a CSS color with alpha +part of 8 characters (in addition of the leading ``#``) and contained in +ranges: ``0`` to ``9`` and ``A`` to ``F`` (case insensitive). + +Examples: ``#2F2F2F80``, ``#2f2f2f80`` + +hex_short +......... + +A regular expression. Allows all values which represent a CSS color of strictly +3 characters (in addition of the leading ``#``) and contained in ranges: ``0`` +to ``9`` and ``A`` to ``F`` (case insensitive). + +Examples: ``#CCC``, ``#ccc`` + +hex_short_with_alpha +.................... + +A regular expression. Allows all values which represent a CSS color with alpha +part of strictly 4 characters (in addition of the leading ``#``) and contained +in ranges: ``0`` to ``9`` and ``A`` to ``F`` (case insensitive). + +Examples: ``#CCC8``, ``#ccc8`` + +basic_named_colors +.................. + +Any of the valid color names defined in the `W3C list of basic named colors`_ +(case insensitive). + +Examples: ``black``, ``red``, ``green`` + +extended_named_colors +..................... + +Any of the valid color names defined in the `W3C list of extended named colors`_ +(case insensitive). + +Examples: ``aqua``, ``brown``, ``chocolate`` + +system_colors +............. + +Any of the valid color names defined in the `CSS WG list of system colors`_ +(case insensitive). + +Examples: ``LinkText``, ``VisitedText``, ``ActiveText``, ``ButtonFace``, ``ButtonText`` + +keywords +........ + +Any of the valid keywords defined in the `CSS WG list of keywords`_ (case insensitive). + +Examples: ``transparent``, ``currentColor`` + +rgb +... + +A regular expression. Allows all values which represent a CSS color following +the RGB notation, with or without space between values. + +Examples: ``rgb(255, 255, 255)``, ``rgb(255,255,255)`` + +rgba +.... + +A regular expression. Allows all values which represent a CSS color with alpha +part following the RGB notation, with or without space between values. + +Examples: ``rgba(255, 255, 255, 0.3)``, ``rgba(255,255,255,0.3)`` + +hsl +... + +A regular expression. Allows all values which represent a CSS color following +the HSL notation, with or without space between values. + +Examples: ``hsl(0, 0%, 20%)``, ``hsl(0,0%,20%)`` + +hsla +.... + +A regular expression. Allows all values which represent a CSS color with alpha +part following the HSLA notation, with or without space between values. + +Examples: ``hsla(0, 0%, 20%, 0.4)``, ``hsla(0,0%,20%,0.4)`` + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`W3C list of basic named colors`: https://www.w3.org/wiki/CSS/Properties/color/keywords#Basic_Colors +.. _`W3C list of extended named colors`: https://www.w3.org/wiki/CSS/Properties/color/keywords#Extended_colors +.. _`CSS WG list of system colors`: https://drafts.csswg.org/css-color/#css-system-colors +.. _`CSS WG list of keywords`: https://drafts.csswg.org/css-color/#transparent-color diff --git a/reference/constraints/Currency.rst b/reference/constraints/Currency.rst new file mode 100644 index 00000000000..cf074d4b069 --- /dev/null +++ b/reference/constraints/Currency.rst @@ -0,0 +1,99 @@ +Currency +======== + +Validates that a value is a valid `3-letter ISO 4217`_ currency name. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Currency` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\CurrencyValidator` +========== =================================================================== + +Basic Usage +----------- + +If you want to ensure that the ``currency`` property of an ``Order`` is +a valid currency, you could do the following: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Order + { + #[Assert\Currency] + protected string $currency; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Order: + properties: + currency: + - Currency: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Order + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('currency', new Assert\Currency()); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid currency.`` + +This is the message that will be shown if the value is not a valid currency. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`3-letter ISO 4217`: https://en.wikipedia.org/wiki/ISO_4217 diff --git a/reference/constraints/Date.rst b/reference/constraints/Date.rst index 293ed88bc35..93bd401cff6 100644 --- a/reference/constraints/Date.rst +++ b/reference/constraints/Date.rst @@ -1,79 +1,98 @@ Date ==== -Validates that a value is a valid date, meaning either a ``DateTime`` object -or a string (or an object that can be cast into a string) that follows a -valid YYYY-MM-DD format. - -+----------------+--------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+--------------------------------------------------------------------+ -| Options | - `message`_ | -+----------------+--------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Date` | -+----------------+--------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\DateValidator` | -+----------------+--------------------------------------------------------------------+ +Validates that a value is a valid date, meaning a string (or an object that can +be cast into a string) that follows a valid ``Y-m-d`` format (e.g. ``'2023-10-18'``). + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Date` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\DateValidator` +========== =================================================================== Basic Usage ----------- .. configuration-block:: - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - birthday: - - Date: ~ + .. code-block:: php-attributes - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\Date() - */ - protected $birthday; + #[Assert\Date] + protected string $birthday; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + birthday: + - Date: ~ + .. code-block:: xml - - - - - - + + + + + + + + + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + /** + * @var string A "Y-m-d" formatted value + */ + protected string $birthday; + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('birthday', new Assert\Date()); } } +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + Options ------- -message -~~~~~~~ +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ -**type**: ``string`` **default**: ``This value is not a valid date`` +**type**: ``string`` **default**: ``This value is not a valid date.`` This message is shown if the underlying data is not a valid date. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/DateTime.rst b/reference/constraints/DateTime.rst index 11d3a237677..ffcfbf55dda 100644 --- a/reference/constraints/DateTime.rst +++ b/reference/constraints/DateTime.rst @@ -1,79 +1,114 @@ DateTime ======== -Validates that a value is a valid "datetime", meaning either a ``DateTime`` -object or a string (or an object that can be cast into a string) that follows -a valid YYYY-MM-DD HH:MM:SS format. - -+----------------+------------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+------------------------------------------------------------------------+ -| Options | - `message`_ | -+----------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\DateTime` | -+----------------+------------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\DateTimeValidator` | -+----------------+------------------------------------------------------------------------+ +Validates that a value is a valid "datetime", meaning a string (or an object +that can be cast into a string) that follows a specific format. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\DateTime` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\DateTimeValidator` +========== =================================================================== Basic Usage ----------- .. configuration-block:: - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - createdAt: - - DateTime: ~ - - .. code-block:: php-annotations + .. code-block:: php-attributes - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; class Author { /** - * @Assert\DateTime() + * @var string A "Y-m-d H:i:s" formatted value */ - protected $createdAt; + #[Assert\DateTime] + protected string $createdAt; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + createdAt: + - DateTime: ~ + .. code-block:: xml - - - - - - + + + + + + + + + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + /** + * @var string A "Y-m-d H:i:s" formatted value + */ + protected string $createdAt; + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('createdAt', new Assert\DateTime()); } } +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + Options ------- -message -~~~~~~~ +``format`` +~~~~~~~~~~ + +**type**: ``string`` **default**: ``Y-m-d H:i:s`` + +This option allows you to validate a custom date format. See +:phpmethod:`DateTime::createFromFormat` for formatting options. + +.. include:: /reference/constraints/_groups-option.rst.inc -**type**: ``string`` **default**: ``This value is not a valid datetime`` +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid datetime.`` This message is shown if the underlying data is not a valid datetime. + +You can use the following parameters in this message: + +================ ============================================================== +Parameter Description +================ ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +``{{ format }}`` The date format defined in ``format`` +================ ============================================================== + +.. versionadded:: 7.3 + + The ``{{ format }}`` parameter was introduced in Symfony 7.3. + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/DisableAutoMapping.rst b/reference/constraints/DisableAutoMapping.rst new file mode 100644 index 00000000000..e5cec52db2d --- /dev/null +++ b/reference/constraints/DisableAutoMapping.rst @@ -0,0 +1,90 @@ +DisableAutoMapping +================== + +This constraint allows to disable :ref:`Doctrine's auto mapping ` +on a class or a property. Automapping allows to determine validation rules based +on Doctrine's attributes. You may use this constraint when +automapping is globally enabled, but you still want to disable this feature for +a class or a property specifically. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\DisableAutoMapping` +========== =================================================================== + +Basic Usage +----------- + +In the following example, the +:class:`Symfony\\Component\\Validator\\Constraints\\DisableAutoMapping` +constraint will tell the validator to not gather constraints from Doctrine's +metadata: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/BookCollection.php + namespace App\Model; + + use App\Model\Author; + use App\Model\BookMetadata; + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Validator\Constraints as Assert; + + #[Assert\DisableAutoMapping] + class BookCollection + { + #[ORM\Column(nullable: false)] + protected string $name = ''; + + #[ORM\ManyToOne(targetEntity: Author::class)] + public Author $author; + + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\BookCollection: + constraints: + - DisableAutoMapping: ~ + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // src/Entity/BookCollection.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class BookCollection + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addConstraint(new Assert\DisableAutoMapping()); + } + } + +Options +------- + +The ``groups`` option is not available for this constraint. + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/DivisibleBy.rst b/reference/constraints/DivisibleBy.rst new file mode 100644 index 00000000000..23b36023cff --- /dev/null +++ b/reference/constraints/DivisibleBy.rst @@ -0,0 +1,118 @@ +DivisibleBy +=========== + +Validates that a value is divisible by another value, defined in the options. + +.. seealso:: + + If you need to validate that the number of elements in a collection is + divisible by a certain number, use the :doc:`Count ` + constraint with the ``divisibleBy`` option. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\DivisibleBy` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\DivisibleByValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraints ensure that: + +* the ``weight`` of the ``Item`` is provided in increments of ``0.25`` +* the ``quantity`` of the ``Item`` must be divisible by ``5`` + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Item.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Item + { + #[Assert\DivisibleBy(0.25)] + protected float $weight; + + #[Assert\DivisibleBy( + value: 5, + )] + protected int $quantity; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Item: + properties: + weight: + - DivisibleBy: 0.25 + quantity: + - DivisibleBy: + value: 5 + + .. code-block:: xml + + + + + + + + + 0.25 + + + + + + + + + + + .. code-block:: php + + // src/Entity/Item.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Item + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('weight', new Assert\DivisibleBy(0.25)); + + $metadata->addPropertyConstraint('quantity', new Assert\DivisibleBy( + value: 5, + )); + } + } + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be a multiple of {{ compared_value }}.`` + +This is the message that will be shown if the value is not divisible by the +comparison value. + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. include:: /reference/constraints/_comparison-propertypath-option.rst.inc + +.. include:: /reference/constraints/_comparison-value-option.rst.inc diff --git a/reference/constraints/Email.rst b/reference/constraints/Email.rst index f0ea3098655..41012e5e935 100644 --- a/reference/constraints/Email.rst +++ b/reference/constraints/Email.rst @@ -4,64 +4,53 @@ Email Validates that a value is a valid email address. The underlying value is cast to a string before being validated. -+----------------+---------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+---------------------------------------------------------------------+ -| Options | - `message`_ | -| | - `checkMX`_ | -| | - `checkHost`_ | -+----------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Email` | -+----------------+---------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\EmailValidator` | -+----------------+---------------------------------------------------------------------+ +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Email` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\EmailValidator` +========== =================================================================== Basic Usage ----------- .. configuration-block:: - .. code-block:: yaml - - # src/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - email: - - Email: - message: The email "{{ value }}" is not a valid email. - checkMX: true - - .. code-block:: php-annotations + .. code-block:: php-attributes - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\Email( - * message = "The email '{{ value }}' is not a valid email.", - * checkMX = true - * ) - */ - protected $email; + #[Assert\Email( + message: 'The email {{ value }} is not a valid email.', + )] + protected string $email; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + email: + - Email: + message: The email "{{ value }}" is not a valid email. + .. code-block:: xml - + + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> - + - @@ -69,49 +58,78 @@ Basic Usage .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - - use Symfony\Component\Validator\Mapping\ClassMetadata; + // src/Entity/Author.php + namespace App\Entity; + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('email', new Assert\Email(array( - 'message' => 'The email "{{ value }}" is not a valid email.', - 'checkMX' => true, - ))); + $metadata->addPropertyConstraint('email', new Assert\Email( + message: 'The email "{{ value }}" is not a valid email.', + )); } } +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + Options ------- -message -~~~~~~~ +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ -**type**: ``string`` **default**: ``This value is not a valid email address`` +**type**: ``string`` **default**: ``This value is not a valid email address.`` This message is shown if the underlying data is not a valid email address. -checkMX -~~~~~~~ +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. _reference-constraint-email-mode: + +``mode`` +~~~~~~~~ + +**type**: ``string`` **default**: ``html5`` + +This option defines the pattern used to validate the email address. Valid values are: + +* ``html5`` uses the regular expression of the `HTML5 email input element`_, + except it enforces a tld to be present. +* ``html5-allow-no-tld`` uses exactly the same regular expression as the `HTML5 email input element`_, + making the backend validation consistent with the one provided by browsers. +* ``strict`` validates the address according to `RFC 5322`_ using the + `egulias/email-validator`_ library (which is already installed when using + :doc:`Symfony Mailer `; otherwise, you must install it separately). -**type**: ``Boolean`` **default**: ``false`` +.. tip:: -If true, then the :phpfunction:`checkdnsrr` PHP function will be used to -check the validity of the MX record of the host of the given email. + The possible values of this option are also defined as PHP constants of + :class:`Symfony\\Component\\Validator\\Constraints\\Email` + (e.g. ``Email::VALIDATION_MODE_STRICT``). -checkHost -~~~~~~~~~ +The default value used by this option is set in the +:ref:`framework.validation.email_validation_mode ` +configuration option. -.. versionadded:: 2.1 - The ``checkHost`` option was added in Symfony 2.1 +.. include:: /reference/constraints/_normalizer-option.rst.inc -**type**: ``Boolean`` **default**: ``false`` +.. include:: /reference/constraints/_payload-option.rst.inc -If true, then the :phpfunction:`checkdnsrr` PHP function will be used to -check the validity of the MX *or* the A *or* the AAAA record of the host -of the given email. +.. _egulias/email-validator: https://packagist.org/packages/egulias/email-validator +.. _HTML5 email input element: https://www.w3.org/TR/html5/sec-forms.html#valid-e-mail-address +.. _RFC 5322: https://datatracker.ietf.org/doc/html/rfc5322 diff --git a/reference/constraints/EnableAutoMapping.rst b/reference/constraints/EnableAutoMapping.rst new file mode 100644 index 00000000000..e221b7c07d0 --- /dev/null +++ b/reference/constraints/EnableAutoMapping.rst @@ -0,0 +1,90 @@ +EnableAutoMapping +================= + +This constraint allows to enable :ref:`Doctrine's auto mapping ` +on a class or a property. Automapping allows to determine validation rules based +on Doctrine's attributes. You may use this constraint when +automapping is globally disabled, but you still want to enable this feature for +a class or a property specifically. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\EnableAutoMapping` +========== =================================================================== + +Basic Usage +----------- + +In the following example, the +:class:`Symfony\\Component\\Validator\\Constraints\\EnableAutoMapping` +constraint will tell the validator to gather constraints from Doctrine's +metadata: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/BookCollection.php + namespace App\Model; + + use App\Model\Author; + use App\Model\BookMetadata; + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Validator\Constraints as Assert; + + #[Assert\EnableAutoMapping] + class BookCollection + { + #[ORM\Column(nullable: false)] + protected string $name = ''; + + #[ORM\ManyToOne(targetEntity: Author::class)] + public Author $author; + + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\BookCollection: + constraints: + - EnableAutoMapping: ~ + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // src/Entity/BookCollection.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class BookCollection + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addConstraint(new Assert\EnableAutoMapping()); + } + } + +Options +------- + +The ``groups`` option is not available for this constraint. + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/EqualTo.rst b/reference/constraints/EqualTo.rst new file mode 100644 index 00000000000..fdc402b1a97 --- /dev/null +++ b/reference/constraints/EqualTo.rst @@ -0,0 +1,126 @@ +EqualTo +======= + +Validates that a value is equal to another value, defined in the options. +To force that a value is *not* equal, see :doc:`/reference/constraints/NotEqualTo`. + +.. warning:: + + This constraint compares using ``==``, so ``3`` and ``"3"`` are considered + equal. Use :doc:`/reference/constraints/IdenticalTo` to compare with + ``===``. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\EqualTo` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\EqualToValidator` +========== =================================================================== + +Basic Usage +----------- + +If you want to ensure that the ``firstName`` of a ``Person`` class is equal to ``Mary`` +and that the ``age`` is ``20``, you could do the following: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\EqualTo('Mary')] + protected string $firstName; + + #[Assert\EqualTo( + value: 20, + )] + protected int $age; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + firstName: + - EqualTo: Mary + age: + - EqualTo: + value: 20 + + .. code-block:: xml + + + + + + + + + Mary + + + + + + + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('firstName', new Assert\EqualTo('Mary')); + + $metadata->addPropertyConstraint('age', new Assert\EqualTo( + value: 20, + )); + } + } + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be equal to {{ compared_value }}.`` + +This is the message that will be shown if the value is not equal. + +You can use the following parameters in this message: + +============================= ================================================ +Parameter Description +============================= ================================================ +``{{ compared_value }}`` The expected value +``{{ compared_value_type }}`` The expected value type +``{{ value }}`` The current (invalid) value +============================= ================================================ + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. include:: /reference/constraints/_comparison-propertypath-option.rst.inc + +.. include:: /reference/constraints/_comparison-value-option.rst.inc diff --git a/reference/constraints/Expression.rst b/reference/constraints/Expression.rst new file mode 100644 index 00000000000..518c5c1f160 --- /dev/null +++ b/reference/constraints/Expression.rst @@ -0,0 +1,353 @@ +Expression +========== + +This constraint allows you to use an :ref:`expression ` +for more complex, dynamic validation. See `Basic Usage`_ for an example. +See :doc:`/reference/constraints/Callback` for a different constraint that +gives you similar flexibility. + +========== =================================================================== +Applies to :ref:`class ` + or :ref:`property/method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Expression` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\ExpressionValidator` +========== =================================================================== + +Basic Usage +----------- + +Imagine you have a class ``BlogPost`` with ``category`` and ``isTechnicalPost`` +properties:: + + // src/Model/BlogPost.php + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + + class BlogPost + { + private string $category; + + private bool $isTechnicalPost; + + // ... + + public function getCategory(): string + { + return $this->category; + } + + public function setIsTechnicalPost(bool $isTechnicalPost): void + { + $this->isTechnicalPost = $isTechnicalPost; + } + + // ... + } + +To validate the object, you have some special requirements: + +A) If ``isTechnicalPost`` is true, then ``category`` must be either ``php`` + or ``symfony``; +B) If ``isTechnicalPost`` is false, then ``category`` can be anything. + +One way to accomplish this is with the Expression constraint: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/BlogPost.php + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + + #[Assert\Expression( + "this.getCategory() in ['php', 'symfony'] or !this.isTechnicalPost()", + message: 'If this is a tech post, the category should be either php or symfony!', + )] + class BlogPost + { + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Model\BlogPost: + constraints: + - Expression: + expression: "this.getCategory() in ['php', 'symfony'] or !this.isTechnicalPost()" + message: "If this is a tech post, the category should be either php or symfony!" + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Model/BlogPost.php + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class BlogPost + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addConstraint(new Assert\Expression( + expression: 'this.getCategory() in ["php", "symfony"] or !this.isTechnicalPost()', + message: 'If this is a tech post, the category should be either php or symfony!', + )); + } + + // ... + } + +The :ref:`expression ` option is the +expression that must return true in order for validation to pass. Learn more +about the :doc:`expression language syntax `. + +Alternatively, you can set the ``negate`` option to ``false`` in order to +assert that the expression must return ``true`` for validation to fail. + +.. sidebar:: Mapping the Error to a Specific Field + + You can also attach the constraint to a specific property and still validate + based on the values of the entire entity. This is handy if you want to attach + the error to a specific field. In this context, ``value`` represents the value + of ``isTechnicalPost``. + + .. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/BlogPost.php + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + + class BlogPost + { + // ... + + #[Assert\Expression( + "this.getCategory() in ['php', 'symfony'] or value == false", + message: 'If this is a tech post, the category should be either php or symfony!', + )] + private bool $isTechnicalPost; + + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Model\BlogPost: + properties: + isTechnicalPost: + - Expression: + expression: "this.getCategory() in ['php', 'symfony'] or value == false" + message: "If this is a tech post, the category should be either php or symfony!" + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Model/BlogPost.php + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class BlogPost + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('isTechnicalPost', new Assert\Expression( + expression: 'this.getCategory() in ["php", "symfony"] or value == false', + message: 'If this is a tech post, the category should be either php or symfony!', + )); + } + + // ... + } + +For more information about the expression and what variables are available +to you, see the :ref:`expression ` +option details below. + +.. tip:: + + Internally, this expression validator constraint uses a service called + ``validator.expression_language`` to evaluate the expressions. You can + decorate or extend that service to fit your own needs. + +Options +------- + +.. _reference-constraint-expression-option: + +``expression`` +~~~~~~~~~~~~~~ + +**type**: ``string`` + +The expression that will be evaluated. If the expression evaluates to a false +value (using ``==``, not ``===``), validation will fail. Learn more about the +:doc:`expression language syntax `. + +Depending on how you use the constraint, you have access to different variables +in your expression: + +* ``this``: The object being validated (e.g. an instance of BlogPost); +* ``value``: The value of the property being validated (only available when + the constraint is applied directly to a property); + +You also have access to the ``is_valid()`` function in your expression. This function +checks that the data passed to function doesn't raise any validation violation. + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not valid.`` + +The default message supplied when the expression evaluates to false. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +``negate`` +~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``true`` + +If ``false``, the validation fails when expression returns ``true``. + +.. include:: /reference/constraints/_payload-option.rst.inc + +``values`` +~~~~~~~~~~ + +**type**: ``array`` **default**: ``[]`` + +The values of the custom variables used in the expression. Values can be of any +type (numeric, boolean, strings, null, etc.) + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/Analysis.php + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + + class Analysis + { + #[Assert\Expression( + 'value + error_margin < threshold', + values: ['error_margin' => 0.25, 'threshold' => 1.5], + )] + private float $metric; + + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Model\Analysis: + properties: + metric: + - Expression: + expression: "value + error_margin < threshold" + values: { error_margin: 0.25, threshold: 1.5 } + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Model/Analysis.php + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Analysis + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('metric', new Assert\Expression( + expression: 'value + error_margin < threshold', + values: ['error_margin' => 0.25, 'threshold' => 1.5], + )); + } + + // ... + } diff --git a/reference/constraints/ExpressionSyntax.rst b/reference/constraints/ExpressionSyntax.rst new file mode 100644 index 00000000000..37e0ad7de4a --- /dev/null +++ b/reference/constraints/ExpressionSyntax.rst @@ -0,0 +1,122 @@ +ExpressionSyntax +================ + +This constraint checks that the value is valid as an `ExpressionLanguage`_ +expression. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\ExpressionSyntax` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\ExpressionSyntaxValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraints ensure that: + +* the ``promotion`` property stores a value which is valid as an + ExpressionLanguage expression; +* the ``shippingOptions`` property also ensures that the expression only uses + certain variables. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Order + { + #[Assert\ExpressionSyntax] + protected string $promotion; + + #[Assert\ExpressionSyntax( + allowedVariables: ['user', 'shipping_centers'], + )] + protected string $shippingOptions; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Order: + properties: + promotion: + - ExpressionSyntax: ~ + shippingOptions: + - ExpressionSyntax: + allowedVariables: ['user', 'shipping_centers'] + + .. code-block:: xml + + + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Student.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Order + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('promotion', new Assert\ExpressionSyntax()); + + $metadata->addPropertyConstraint('shippingOptions', new Assert\ExpressionSyntax( + allowedVariables: ['user', 'shipping_centers'], + )); + } + } + +Options +------- + +allowedVariables +~~~~~~~~~~~~~~~~ + +**type**: ``array`` or ``null`` **default**: ``null`` + +If this option is defined, the expression can only use the variables whose names +are included in this option. Unset this option or set its value to ``null`` to +allow any variables. + +.. include:: /reference/constraints/_groups-option.rst.inc + +message +~~~~~~~ + +**type**: ``string`` **default**: ``This value should be a valid expression.`` + +This is the message displayed when the validation fails. + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`ExpressionLanguage`: https://symfony.com/components/ExpressionLanguage diff --git a/reference/constraints/False.rst b/reference/constraints/False.rst deleted file mode 100644 index 118999cede0..00000000000 --- a/reference/constraints/False.rst +++ /dev/null @@ -1,111 +0,0 @@ -False -===== - -Validates that a value is ``false``. Specifically, this checks to see if -the value is exactly ``false``, exactly the integer ``0``, or exactly the -string "``0``". - -Also see :doc:`True `. - -+----------------+---------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+---------------------------------------------------------------------+ -| Options | - `message`_ | -+----------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\False` | -+----------------+---------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\FalseValidator` | -+----------------+---------------------------------------------------------------------+ - -Basic Usage ------------ - -The ``False`` constraint can be applied to a property or a "getter" method, -but is most commonly useful in the latter case. For example, suppose that -you want to guarantee that some ``state`` property is *not* in a dynamic -``invalidStates`` array. First, you'd create a "getter" method:: - - protected $state; - - protected $invalidStates = array(); - - public function isStateInvalid() - { - return in_array($this->state, $this->invalidStates); - } - -In this case, the underlying object is only valid if the ``isStateInvalid`` -method returns **false**: - -.. configuration-block:: - - .. code-block:: yaml - - # src/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author - getters: - stateInvalid: - - "False": - message: You've entered an invalid state. - - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\False( - * message = "You've entered an invalid state." - * ) - */ - public function isStateInvalid() - { - // ... - } - } - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addGetterConstraint('stateInvalid', new Assert\False()); - } - } - -.. caution:: - - When using YAML, be sure to surround ``False`` with quotes (``"False"``) - or else YAML will convert this into a Boolean value. - -Options -------- - -message -~~~~~~~ - -**type**: ``string`` **default**: ``This value should be false`` - -This message is shown if the underlying data is not false. diff --git a/reference/constraints/File.rst b/reference/constraints/File.rst index d5d1605f99d..495c19f9cbe 100644 --- a/reference/constraints/File.rst +++ b/reference/constraints/File.rst @@ -3,138 +3,129 @@ File Validates that a value is a valid "file", which can be one of the following: -* A string (or object with a ``__toString()`` method) path to an existing file; - +* A string (or object with a ``__toString()`` method) path to an existing + file; * A valid :class:`Symfony\\Component\\HttpFoundation\\File\\File` object - (including objects of class :class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile`). + (including objects of :class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile` class). -This constraint is commonly used in forms with the :doc:`file` -form type. +This constraint is commonly used in forms with the :doc:`FileType ` +form field. -.. tip:: +.. seealso:: - If the file you're validating is an image, try the :doc:`Image` + If the file you're validating is an image, try the :doc:`Image ` constraint. -+----------------+---------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+---------------------------------------------------------------------+ -| Options | - `maxSize`_ | -| | - `mimeTypes`_ | -| | - `maxSizeMessage`_ | -| | - `mimeTypesMessage`_ | -| | - `notFoundMessage`_ | -| | - `notReadableMessage`_ | -| | - `uploadIniSizeErrorMessage`_ | -| | - `uploadFormSizeErrorMessage`_ | -| | - `uploadErrorMessage`_ | -+----------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\File` | -+----------------+---------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\FileValidator` | -+----------------+---------------------------------------------------------------------+ +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\File` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\FileValidator` +========== =================================================================== Basic Usage ----------- This constraint is most commonly used on a property that will be rendered -in a form as a :doc:`file` form type. For example, -suppose you're creating an author form where you can upload a "bio" PDF for -the author. In your form, the ``bioFile`` property would be a ``file`` type. -The ``Author`` class might look as follows:: +in a form as a :doc:`FileType ` field. For +example, suppose you're creating an author form where you can upload a "bio" +PDF for the author. In your form, the ``bioFile`` property would be a ``file`` +type. The ``Author`` class might look as follows:: - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; use Symfony\Component\HttpFoundation\File\File; class Author { - protected $bioFile; + protected File $bioFile; - public function setBioFile(File $file = null) + public function setBioFile(?File $file = null): void { $this->bioFile = $file; } - public function getBioFile() + public function getBioFile(): File { return $this->bioFile; } } -To guarantee that the ``bioFile`` ``File`` object is valid, and that it is +To guarantee that the ``bioFile`` ``File`` object is valid and that it is below a certain file size and a valid PDF, add the following: .. configuration-block:: - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - bioFile: - - File: - maxSize: 1024k - mimeTypes: [application/pdf, application/x-pdf] - mimeTypesMessage: Please upload a valid PDF - + .. code-block:: php-attributes - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\File( - * maxSize = "1024k", - * mimeTypes = {"application/pdf", "application/x-pdf"}, - * mimeTypesMessage = "Please upload a valid PDF" - * ) - */ - protected $bioFile; + #[Assert\File( + maxSize: '1024k', + extensions: ['pdf'], + extensionsMessage: 'Please upload a valid PDF', + )] + protected File $bioFile; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + bioFile: + - File: + maxSize: 1024k + extensions: [pdf] + extensionsMessage: Please upload a valid PDF + .. code-block:: xml - - - - - - - - - - + + + + + + + + + + + + + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('bioFile', new Assert\File(array( - 'maxSize' => '1024k', - 'mimeTypes' => array( - 'application/pdf', - 'application/x-pdf', - ), - 'mimeTypesMessage' => 'Please upload a valid PDF', - ))); + $metadata->addPropertyConstraint('bioFile', new Assert\File( + maxSize: '1024k', + extensions: [ + 'pdf', + ], + extensionsMessage: 'Please upload a valid PDF', + )); } } @@ -142,93 +133,307 @@ The ``bioFile`` property is validated to guarantee that it is a real file. Its size and mime type are also validated because the appropriate options have been specified. +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + Options ------- -maxSize -~~~~~~~ +``binaryFormat`` +~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``null`` + +When ``true``, the sizes will be displayed in messages with binary-prefixed +units (KiB, MiB). When ``false``, the sizes will be displayed with SI-prefixed +units (kB, MB). When ``null``, then the binaryFormat will be guessed from +the value defined in the ``maxSize`` option. + +For more information about the difference between binary and SI prefixes, +see `Wikipedia: Binary prefix`_. + +``extensions`` +~~~~~~~~~~~~~~ + +**type**: ``array`` or ``string`` + +If set, the validator will check that the extension and the media type +(formerly known as MIME type) of the underlying file are equal to the given +extension and associated media type (if a string) or exist in the collection +(if an array). + +By default, all media types associated with an extension are allowed. +The list of supported extensions and associated media types can be found on +the `IANA website`_. + +It's also possible to explicitly configure the authorized media types for +an extension. + +In the following example, allowed media types are explicitly set for the ``xml`` +and ``txt`` extensions, and all associated media types are allowed for ``jpg``:: + + [ + 'xml' => ['text/xml', 'application/xml'], + 'txt' => 'text/plain', + 'jpg', + ] + +``disallowEmptyMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``An empty file is not allowed.`` + +This constraint checks if the uploaded file is empty (i.e. 0 bytes). If it is, +this message is displayed. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ file }}`` Absolute file path +``{{ name }}`` Base file name +=============== ============================================================== + +.. include:: /reference/constraints/_groups-option.rst.inc + +``maxSize`` +~~~~~~~~~~~ **type**: ``mixed`` -If set, the size of the underlying file must be below this file size in order -to be valid. The size of the file can be given in one of the following formats: +If set, the size of the underlying file must be below this file size in +order to be valid. The size of the file can be given in one of the following +formats: + +====== ========= =============== ======== +Suffix Unit Name Value Example +====== ========= =============== ======== +(none) byte 1 byte ``4096`` +``k`` kilobyte 1,000 bytes ``200k`` +``M`` megabyte 1,000,000 bytes ``2M`` +``Ki`` kibibyte 1,024 bytes ``32Ki`` +``Mi`` mebibyte 1,048,576 bytes ``8Mi`` +====== ========= =============== ======== + +For more information about the difference between binary and SI prefixes, +see `Wikipedia: Binary prefix`_. + +``maxSizeMessage`` +~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.`` -* **bytes**: To specify the ``maxSize`` in bytes, pass a value that is entirely - numeric (e.g. ``4096``); +The message displayed if the file is larger than the `maxSize`_ option. -* **kilobytes**: To specify the ``maxSize`` in kilobytes, pass a number and - suffix it with a lowercase "k" (e.g. ``200k``); +You can use the following parameters in this message: -* **megabytes**: To specify the ``maxSize`` in megabytes, pass a number and - suffix it with a capital "M" (e.g. ``4M``). +================ ============================================================= +Parameter Description +================ ============================================================= +``{{ file }}`` Absolute file path +``{{ limit }}`` Maximum file size allowed +``{{ name }}`` Base file name +``{{ size }}`` File size of the given file +``{{ suffix }}`` Suffix for the used file size unit (see above) +================ ============================================================= -mimeTypes -~~~~~~~~~ +``mimeTypes`` +~~~~~~~~~~~~~ **type**: ``array`` or ``string`` -If set, the validator will check that the mime type of the underlying file -is equal to the given mime type (if a string) or exists in the collection -of given mime types (if an array). +.. warning:: -You can find a list of existing mime types on the `IANA website`_ + You should always use the ``extensions`` option instead of ``mimeTypes`` + except if you explicitly don't want to check that the extension of the file + is consistent with its content (this can be a security issue). -maxSizeMessage -~~~~~~~~~~~~~~ + By default, the ``extensions`` option also checks the media type of the file. -**type**: ``string`` **default**: ``The file is too large ({{ size }}). Allowed maximum size is {{ limit }}`` +If set, the validator will check that the media type (formerly known as MIME +type) of the underlying file is equal to the given mime type (if a string) or +exists in the collection of given mime types (if an array). -The message displayed if the file is larger than the `maxSize`_ option. +You can find a list of existing mime types on the `IANA website`_. -mimeTypesMessage -~~~~~~~~~~~~~~~~ +.. note:: + + When using this constraint on a :doc:`FileType field `, + the value of the ``mimeTypes`` option is also used in the ``accept`` + attribute of the related ```` HTML element. + + This behavior is applied only when using :ref:`form type guessing ` + (i.e. the form type is not defined explicitly in the ``->add()`` method of + the form builder) and when the field doesn't define its own ``accept`` value. -**type**: ``string`` **default**: ``The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}`` +``filenameMaxLength`` +~~~~~~~~~~~~~~~~~~~~~ -The message displayed if the mime type of the file is not a valid mime type +**type**: ``integer`` **default**: ``null`` + +If set, the validator will check that the filename of the underlying file +doesn't exceed a certain length. + +``filenameTooLongMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The filename is too long. It should have {{ filename_max_length }} character or less.|The filename is too long. It should have {{ filename_max_length }} characters or less.`` + +The message displayed if the filename of the file exceeds the limit set +with the ``filenameMaxLength`` option. + +You can use the following parameters in this message: + +============================== ============================================================== +Parameter Description +============================== ============================================================== +``{{ filename_max_length }}`` Maximum number of characters allowed +============================== ============================================================== + +``extensionsMessage`` +~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The extension of the file is invalid ({{ extension }}). Allowed extensions are {{ extensions }}.`` + +The message displayed if the extension of the file is not a valid extension +per the `extensions`_ option. + +==================== ============================================================== +Parameter Description +==================== ============================================================== +``{{ extension }}`` The extension of the given file +``{{ extensions }}`` The list of allowed file extensions +==================== ============================================================== + +``mimeTypesMessage`` +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.`` + +The message displayed if the media type of the file is not a valid media type per the `mimeTypes`_ option. -notFoundMessage -~~~~~~~~~~~~~~~ +.. include:: /reference/constraints/_parameters-mime-types-message-option.rst.inc + +``notFoundMessage`` +~~~~~~~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``The file could not be found`` +**type**: ``string`` **default**: ``The file could not be found.`` The message displayed if no file can be found at the given path. This error is only likely if the underlying value is a string path, as a ``File`` object cannot be constructed with an invalid file path. -notReadableMessage -~~~~~~~~~~~~~~~~~~ +You can use the following parameters in this message: -**type**: ``string`` **default**: ``The file is not readable`` +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ file }}`` Absolute file path +=============== ============================================================== -The message displayed if the file exists, but the PHP ``is_readable`` function +``notReadableMessage`` +~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The file is not readable.`` + +The message displayed if the file exists, but the PHP ``is_readable()`` function fails when passed the path to the file. -uploadIniSizeErrorMessage -~~~~~~~~~~~~~~~~~~~~~~~~~ +You can use the following parameters in this message: -**type**: ``string`` **default**: ``The file is too large. Allowed maximum size is {{ limit }}`` +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ file }}`` Absolute file path +=============== ============================================================== -The message that is displayed if the uploaded file is larger than the ``upload_max_filesize`` -PHP.ini setting. +.. include:: /reference/constraints/_payload-option.rst.inc -uploadFormSizeErrorMessage -~~~~~~~~~~~~~~~~~~~~~~~~~~ +``uploadCantWriteErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``Cannot write temporary file to disk.`` + +The message that is displayed if the uploaded file can't be stored in the +temporary folder. + +This message has no parameters. + +``uploadErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The file could not be uploaded.`` -**type**: ``string`` **default**: ``The file is too large`` +The message that is displayed if the uploaded file could not be uploaded +for some unknown reason. + +This message has no parameters. + +``uploadExtensionErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``A PHP extension caused the upload to fail.`` + +The message that is displayed if a PHP extension caused the file upload to +fail. + +This message has no parameters. + +``uploadFormSizeErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The file is too large.`` The message that is displayed if the uploaded file is larger than allowed by the HTML file input field. -uploadErrorMessage -~~~~~~~~~~~~~~~~~~ +This message has no parameters. -**type**: ``string`` **default**: ``The file could not be uploaded`` +``uploadIniSizeErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The message that is displayed if the uploaded file could not be uploaded -for some unknown reason, such as the file upload failed or it couldn't be written -to disk. +**type**: ``string`` **default**: ``The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.`` + +The message that is displayed if the uploaded file is larger than the ``upload_max_filesize`` +``php.ini`` setting. + +You can use the following parameters in this message: + +================ ============================================================= +Parameter Description +================ ============================================================= +``{{ limit }}`` Maximum file size allowed +``{{ suffix }}`` Suffix for the used file size unit (see above) +================ ============================================================= + +``uploadNoFileErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``No file was uploaded.`` + +The message that is displayed if no file was uploaded. + +This message has no parameters. + +``uploadNoTmpDirErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``No temporary folder was configured in php.ini.`` + +The message that is displayed if the php.ini setting ``upload_tmp_dir`` is +missing. + +This message has no parameters. + +``uploadPartialErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The file was only partially uploaded.`` + +The message that is displayed if the uploaded file is only partially uploaded. +This message has no parameters. -.. _`IANA website`: http://www.iana.org/assignments/media-types/index.html +.. _`IANA website`: https://www.iana.org/assignments/media-types/media-types.xhtml +.. _`Wikipedia: Binary prefix`: https://en.wikipedia.org/wiki/Binary_prefix diff --git a/reference/constraints/GreaterThan.rst b/reference/constraints/GreaterThan.rst new file mode 100644 index 00000000000..d1b79028acd --- /dev/null +++ b/reference/constraints/GreaterThan.rst @@ -0,0 +1,309 @@ +GreaterThan +=========== + +Validates that a value is greater than another value, defined in the options. To +force that a value is greater than or equal to another value, see +:doc:`/reference/constraints/GreaterThanOrEqual`. To force a value is less +than another value, see :doc:`/reference/constraints/LessThan`. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThan` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThanValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraints ensure that: + +* the number of ``siblings`` of a ``Person`` is greater than ``5`` +* the ``age`` of a ``Person`` class is greater than ``18`` + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\GreaterThan(5)] + protected int $siblings; + + #[Assert\GreaterThan( + value: 18, + )] + protected int $age; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + siblings: + - GreaterThan: 5 + age: + - GreaterThan: + value: 18 + + .. code-block:: xml + + + + + + + + + 5 + + + + + + + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('siblings', new Assert\GreaterThan(5)); + + $metadata->addPropertyConstraint('age', new Assert\GreaterThan( + value: 18, + )); + } + } + +Comparing Dates +--------------- + +This constraint can be used to compare ``DateTime`` objects against any date +string `accepted by the DateTime constructor`_. For example, you could check +that a date must at least be the next day: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Order + { + #[Assert\GreaterThan('today')] + protected \DateTimeInterface $deliveryDate; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Order: + properties: + deliveryDate: + - GreaterThan: today + + .. code-block:: xml + + + + + + + + today + + + + + .. code-block:: php + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Order + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('deliveryDate', new Assert\GreaterThan('today')); + } + } + +Be aware that PHP will use the server's configured timezone to interpret these +dates. If you want to fix the timezone, append it to the date string: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Order + { + #[Assert\GreaterThan('today UTC')] + protected \DateTimeInterface $deliveryDate; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Order: + properties: + deliveryDate: + - GreaterThan: today UTC + + .. code-block:: xml + + + + + + + + today UTC + + + + + .. code-block:: php + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Order + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('deliveryDate', new Assert\GreaterThan('today UTC')); + } + } + +The ``DateTime`` class also accepts relative dates or times. For example, you +can check that the above delivery date starts at least five hours after the +current time: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Order + { + #[Assert\GreaterThan('+5 hours')] + protected \DateTimeInterface $deliveryDate; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Order: + properties: + deliveryDate: + - GreaterThan: +5 hours + + .. code-block:: xml + + + + + + + + +5 hours + + + + + .. code-block:: php + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Order + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('deliveryDate', new Assert\GreaterThan('+5 hours')); + } + } + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be greater than {{ compared_value }}.`` + +This is the message that will be shown if the value is not greater than the +comparison value. + +You can use the following parameters in this message: + +============================= ================================================ +Parameter Description +============================= ================================================ +``{{ compared_value }}`` The lower limit +``{{ compared_value_type }}`` The expected value type +``{{ value }}`` The current (invalid) value +============================= ================================================ + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. include:: /reference/constraints/_comparison-propertypath-option.rst.inc + +.. include:: /reference/constraints/_comparison-value-option.rst.inc + +.. _`accepted by the DateTime constructor`: https://www.php.net/manual/en/datetime.formats.php diff --git a/reference/constraints/GreaterThanOrEqual.rst b/reference/constraints/GreaterThanOrEqual.rst new file mode 100644 index 00000000000..63c2ade6197 --- /dev/null +++ b/reference/constraints/GreaterThanOrEqual.rst @@ -0,0 +1,308 @@ +GreaterThanOrEqual +================== + +Validates that a value is greater than or equal to another value, defined in +the options. To force that a value is greater than another value, see +:doc:`/reference/constraints/GreaterThan`. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThanOrEqual` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThanOrEqualValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraints ensure that: + +* the number of ``siblings`` of a ``Person`` is greater than or equal to ``5`` +* the ``age`` of a ``Person`` class is greater than or equal to ``18`` + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\GreaterThanOrEqual(5)] + protected int $siblings; + + #[Assert\GreaterThanOrEqual( + value: 18, + )] + protected int $age; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + siblings: + - GreaterThanOrEqual: 5 + age: + - GreaterThanOrEqual: + value: 18 + + .. code-block:: xml + + + + + + + + + 5 + + + + + + + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('siblings', new Assert\GreaterThanOrEqual(5)); + + $metadata->addPropertyConstraint('age', new Assert\GreaterThanOrEqual( + value: 18, + )); + } + } + +Comparing Dates +--------------- + +This constraint can be used to compare ``DateTime`` objects against any date +string `accepted by the DateTime constructor`_. For example, you could check +that a date must at least be the current day: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Order + { + #[Assert\GreaterThanOrEqual('today')] + protected \DateTimeInterface $deliveryDate; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Order: + properties: + deliveryDate: + - GreaterThanOrEqual: today + + .. code-block:: xml + + + + + + + + today + + + + + .. code-block:: php + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Order + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('deliveryDate', new Assert\GreaterThanOrEqual('today')); + } + } + +Be aware that PHP will use the server's configured timezone to interpret these +dates. If you want to fix the timezone, append it to the date string: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Order + { + #[Assert\GreaterThanOrEqual('today UTC')] + protected \DateTimeInterface $deliveryDate; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Order: + properties: + deliveryDate: + - GreaterThanOrEqual: today UTC + + .. code-block:: xml + + + + + + + + today UTC + + + + + .. code-block:: php + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Order + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('deliveryDate', new Assert\GreaterThanOrEqual('today UTC')); + } + } + +The ``DateTime`` class also accepts relative dates or times. For example, you +can check that the above delivery date starts at least five hours after the +current time: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Order + { + #[Assert\GreaterThanOrEqual('+5 hours')] + protected \DateTimeInterface $deliveryDate; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Order: + properties: + deliveryDate: + - GreaterThanOrEqual: +5 hours + + .. code-block:: xml + + + + + + + + +5 hours + + + + + .. code-block:: php + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Order + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('deliveryDate', new Assert\GreaterThanOrEqual('+5 hours')); + } + } + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be greater than or equal to {{ compared_value }}.`` + +This is the message that will be shown if the value is not greater than or equal +to the comparison value. + +You can use the following parameters in this message: + +============================= ================================================ +Parameter Description +============================= ================================================ +``{{ compared_value }}`` The lower limit +``{{ compared_value_type }}`` The expected value type +``{{ value }}`` The current (invalid) value +============================= ================================================ + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. include:: /reference/constraints/_comparison-propertypath-option.rst.inc + +.. include:: /reference/constraints/_comparison-value-option.rst.inc + +.. _`accepted by the DateTime constructor`: https://www.php.net/manual/en/datetime.formats.php diff --git a/reference/constraints/Hostname.rst b/reference/constraints/Hostname.rst new file mode 100644 index 00000000000..58ac0364669 --- /dev/null +++ b/reference/constraints/Hostname.rst @@ -0,0 +1,128 @@ +Hostname +======== + +This constraint ensures that the given value is a valid host name (internally it +uses the ``FILTER_VALIDATE_DOMAIN`` option of the :phpfunction:`filter_var` PHP +function). + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Hostname` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\HostnameValidator` +========== =================================================================== + +Basic Usage +----------- + +To use the Hostname validator, apply it to a property on an object that +will contain a host name. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/ServerSettings.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class ServerSettings + { + #[Assert\Hostname(message: 'The server name must be a valid hostname.')] + protected string $name; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\ServerSettings: + properties: + name: + - Hostname: + message: The server name must be a valid hostname. + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/ServerSettings.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class ServerSettings + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('name', new Assert\Hostname( + message: 'The server name must be a valid hostname.', + )); + } + } + +The following top-level domains (TLD) are reserved according to `RFC 2606`_ and +that's why hostnames containing them are not considered valid: ``.example``, +``.invalid``, ``.localhost``, and ``.test``. + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid hostname.`` + +The default message supplied when the value is not a valid hostname. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +``requireTld`` +~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``true`` + +By default, hostnames are considered valid only when they are fully qualified +and include their TLDs (top-level domain names). For instance, ``example.com`` +is valid but ``example`` is not. + +Set this option to ``false`` to not require any TLD in the hostnames. + +.. note:: + + This constraint does not validate that the given TLD value is included in + the `list of official top-level domains`_ (because that list is growing + continuously and it's hard to keep track of it). + +.. _`RFC 2606`: https://tools.ietf.org/html/rfc2606 +.. _`list of official top-level domains`: https://en.wikipedia.org/wiki/List_of_Internet_top-level_domains diff --git a/reference/constraints/Iban.rst b/reference/constraints/Iban.rst new file mode 100644 index 00000000000..8d5982eea6d --- /dev/null +++ b/reference/constraints/Iban.rst @@ -0,0 +1,111 @@ +IBAN +==== + +This constraint is used to ensure that a bank account number has the proper +format of an `International Bank Account Number (IBAN)`_. IBAN is an +internationally agreed means of identifying bank accounts across national +borders with a reduced risk of propagating transcription errors. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Iban` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\IbanValidator` +========== =================================================================== + +Basic Usage +----------- + +To use the IBAN validator, apply it to a property on an object that +will contain an International Bank Account Number. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Transaction.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Transaction + { + #[Assert\Iban( + message: 'This is not a valid International Bank Account Number (IBAN).', + )] + protected string $bankAccountNumber; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Transaction: + properties: + bankAccountNumber: + - Iban: + message: This is not a valid International Bank Account Number (IBAN). + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Transaction.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Transaction + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('bankAccountNumber', new Assert\Iban( + message: 'This is not a valid International Bank Account Number (IBAN).', + )); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This is not a valid International Bank Account Number (IBAN).`` + +The default message supplied when the value does not pass the IBAN check. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`International Bank Account Number (IBAN)`: https://en.wikipedia.org/wiki/International_Bank_Account_Number diff --git a/reference/constraints/IdenticalTo.rst b/reference/constraints/IdenticalTo.rst new file mode 100644 index 00000000000..f8844f90a72 --- /dev/null +++ b/reference/constraints/IdenticalTo.rst @@ -0,0 +1,129 @@ +IdenticalTo +=========== + +Validates that a value is identical to another value, defined in the options. +To force that a value is *not* identical, see +:doc:`/reference/constraints/NotIdenticalTo`. + +.. warning:: + + This constraint compares using ``===``, so ``3`` and ``"3"`` are *not* + considered equal. Use :doc:`/reference/constraints/EqualTo` to compare + with ``==``. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\IdenticalTo` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\IdenticalToValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraints ensure that: + +* ``firstName`` of ``Person`` class is equal to ``Mary`` *and* is a string +* ``age`` is equal to ``20`` *and* is of type integer + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\IdenticalTo('Mary')] + protected string $firstName; + + #[Assert\IdenticalTo( + value: 20, + )] + protected int $age; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + firstName: + - IdenticalTo: Mary + age: + - IdenticalTo: + value: 20 + + .. code-block:: xml + + + + + + + + + Mary + + + + + + + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('firstName', new Assert\IdenticalTo('Mary')); + + $metadata->addPropertyConstraint('age', new Assert\IdenticalTo( + value: 20, + )); + } + } + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be identical to {{ compared_value_type }} {{ compared_value }}.`` + +This is the message that will be shown if the value is not identical. + +You can use the following parameters in this message: + +============================= ================================================ +Parameter Description +============================= ================================================ +``{{ compared_value }}`` The expected value +``{{ compared_value_type }}`` The expected value type +``{{ value }}`` The current (invalid) value +============================= ================================================ + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. include:: /reference/constraints/_comparison-propertypath-option.rst.inc + +.. include:: /reference/constraints/_comparison-value-option.rst.inc diff --git a/reference/constraints/Image.rst b/reference/constraints/Image.rst index ca90c6476c7..5dd270c44f8 100644 --- a/reference/constraints/Image.rst +++ b/reference/constraints/Image.rst @@ -1,75 +1,79 @@ Image ===== -The Image constraint works exactly like the :doc:`File` -constraint, except that its `mimeTypes`_ and `mimeTypesMessage` options are -automatically setup to work for image files specifically. - -Additionally, as of Symfony 2.1, it has options so you can validate against -the width and height of the image. - -See the :doc:`File` constraint for the bulk of -the documentation on this constraint. - -+----------------+----------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+----------------------------------------------------------------------+ -| Options | - `mimeTypes`_ | -| | - `minWidth`_ | -| | - `maxWidth`_ | -| | - `maxHeight`_ | -| | - `minHeight`_ | -| | - `mimeTypesMessage`_ | -| | - `sizeNotDetectedMessage`_ | -| | - `maxWidthMessage`_ | -| | - `minWidthMessage`_ | -| | - `maxHeightMessage`_ | -| | - `minHeightMessage`_ | -| | - See :doc:`File` for inherited options | -+----------------+----------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\File` | -+----------------+----------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\FileValidator` | -+----------------+----------------------------------------------------------------------+ +The Image constraint works exactly like the :doc:`File ` +constraint, except that its `mimeTypes`_ and `mimeTypesMessage`_ options +are automatically setup to work for image files specifically. + +Additionally it has options so you can validate against the width and height +of the image. + +See the :doc:`File ` constraint for the bulk +of the documentation on this constraint. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Image` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\ImageValidator` +========== =================================================================== Basic Usage ----------- This constraint is most commonly used on a property that will be rendered -in a form as a :doc:`file` form type. For example, -suppose you're creating an author form where you can upload a "headshot" -image for the author. In your form, the ``headshot`` property would be a -``file`` type. The ``Author`` class might look as follows:: +in a form as a :doc:`FileType ` field. For +example, suppose you're creating an author form where you can upload a +"headshot" image for the author. In your form, the ``headshot`` property +would be a ``file`` type. The ``Author`` class might look as follows:: - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; use Symfony\Component\HttpFoundation\File\File; class Author { - protected $headshot; + protected File $headshot; - public function setHeadshot(File $file = null) + public function setHeadshot(?File $file = null): void { $this->headshot = $file; } - public function getHeadshot() + public function getHeadshot(): File { return $this->headshot; } } -To guarantee that the ``headshot`` ``File`` object is a valid image and that -it is between a certain size, add the following: +To guarantee that the ``headshot`` ``File`` object is a valid image and +that it is between a certain size, add the following: .. configuration-block:: + .. code-block:: php-attributes + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\HttpFoundation\File\File; + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + #[Assert\Image( + minWidth: 200, + maxWidth: 400, + minHeight: 200, + maxHeight: 400, + )] + protected File $headshot; + } + .. code-block:: yaml - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author + # config/validator/validation.yaml + App\Entity\Author: properties: headshot: - Image: @@ -77,155 +81,474 @@ it is between a certain size, add the following: maxWidth: 400 minHeight: 200 maxHeight: 400 - - .. code-block:: php-annotations + .. code-block:: xml + + + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Author.php + namespace App\Entity; - // src/Acme/BlogBundle/Entity/Author.php use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - /** - * @Assert\Image( - * minWidth = 200, - * maxWidth = 400, - * minHeight = 200, - * maxHeight = 400 - * ) - */ - protected $headshot; + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('headshot', new Assert\Image( + minWidth: 200, + maxWidth: 400, + minHeight: 200, + maxHeight: 400, + )); + } } +The ``headshot`` property is validated to guarantee that it is a real image +and that it is between a certain width and height. + +You may also want to guarantee the ``headshot`` image to be square. In this +case you can disable portrait and landscape orientations as shown in the +following code: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\HttpFoundation\File\File; + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + #[Assert\Image( + allowLandscape: false, + allowPortrait: false, + )] + protected File $headshot; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + headshot: + - Image: + allowLandscape: false + allowPortrait: false + .. code-block:: xml - - + + - - - - + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php - // ... + // src/Entity/Author.php + namespace App\Entity; + use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\Image; class Author { // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('headshot', new Image(array( - 'minWidth' => 200, - 'maxWidth' => 400, - 'minHeight' => 200, - 'maxHeight' => 400, - ))); + $metadata->addPropertyConstraint('headshot', new Assert\Image( + allowLandscape: false, + allowPortrait: false, + )); } } -The ``headshot`` property is validated to guarantee that it is a real image -and that it is between a certain width and height. +You can mix all the constraint options to create powerful validation rules. Options ------- -This constraint shares all of its options with the :doc:`File` +This constraint shares all of its options with the :doc:`File ` constraint. It does, however, modify two of the default option values and add several other options. -mimeTypes -~~~~~~~~~ +``allowLandscape`` +~~~~~~~~~~~~~~~~~~ -**type**: ``array`` or ``string`` **default**: ``image/*`` +**type**: ``Boolean`` **default**: ``true`` + +If this option is false, the image cannot be landscape oriented. + +.. versionadded:: 7.3 + + The ``allowLandscape`` option support for SVG files was introduced in Symfony 7.3. + +``allowLandscapeMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The image is landscape oriented ({{ width }}x{{ height }}px). +Landscape oriented images are not allowed`` + +The error message if the image is landscape oriented and you set `allowLandscape`_ to ``false``. + +You can use the following parameters in this message: + +================ ============================================================= +Parameter Description +================ ============================================================= +``{{ height }}`` The current height +``{{ width }}`` The current width +================ ============================================================= + +``allowPortrait`` +~~~~~~~~~~~~~~~~~ + +**type**: ``Boolean`` **default**: ``true`` + +If this option is false, the image cannot be portrait oriented. + +.. versionadded:: 7.3 + + The ``allowPortrait`` option support for SVG files was introduced in Symfony 7.3. + +``allowPortraitMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The image is portrait oriented ({{ width }}x{{ height }}px). +Portrait oriented images are not allowed`` + +The error message if the image is portrait oriented and you set `allowPortrait`_ to ``false``. + +You can use the following parameters in this message: + +================ ============================================================= +Parameter Description +================ ============================================================= +``{{ height }}`` The current height +``{{ width }}`` The current width +================ ============================================================= + +``allowSquare`` +~~~~~~~~~~~~~~~ + +**type**: ``Boolean`` **default**: ``true`` + +If this option is false, the image cannot be a square. If you want to force +a square image, then leave this option as its default ``true`` value +and set `allowLandscape`_ and `allowPortrait`_ both to ``false``. + +.. versionadded:: 7.3 + + The ``allowSquare`` option support for SVG files was introduced in Symfony 7.3. + +``allowSquareMessage`` +~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The image is square ({{ width }}x{{ height }}px). +Square images are not allowed`` + +The error message if the image is square and you set `allowSquare`_ to ``false``. + +You can use the following parameters in this message: + +================ ============================================================= +Parameter Description +================ ============================================================= +``{{ height }}`` The current height +``{{ width }}`` The current width +================ ============================================================= + +``corruptedMessage`` +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The image file is corrupted.`` -You can find a list of existing image mime types on the `IANA website`_ +The error message when the `detectCorrupted`_ option is enabled and the image +is corrupted. -mimeTypesMessage -~~~~~~~~~~~~~~~~ +This message has no parameters. -**type**: ``string`` **default**: ``This file is not a valid image`` +``detectCorrupted`` +~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 2.1 - All of the min/max width/height options are new to Symfony 2.1. +**type**: ``boolean`` **default**: ``false`` -minWidth -~~~~~~~~ +If this option is true, the image contents are validated to ensure that the +image is not corrupted. This validation is done with PHP's :phpfunction:`imagecreatefromstring` +function, which requires the `PHP GD extension`_ to be enabled. + +.. include:: /reference/constraints/_groups-option.rst.inc + +``maxHeight`` +~~~~~~~~~~~~~ **type**: ``integer`` -If set, the width of the image file must be greater than or equal to this +If set, the height of the image file must be less than or equal to this value in pixels. -maxWidth -~~~~~~~~ +``maxHeightMessage`` +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The image height is too big ({{ height }}px). +Allowed maximum height is {{ max_height }}px.`` + +The error message if the height of the image exceeds `maxHeight`_. + +You can use the following parameters in this message: + +==================== ========================================================= +Parameter Description +==================== ========================================================= +``{{ height }}`` The current (invalid) height +``{{ max_height }}`` The maximum allowed height +==================== ========================================================= + +``maxPixels`` +~~~~~~~~~~~~~ + +**type**: ``integer`` + +If set, the amount of pixels of the image file must be less than or equal to this +value. + +``maxPixelsMessage`` +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The image has too many pixels ({{ pixels }} pixels). +Maximum amount expected is {{ max_pixels }} pixels.`` + +The error message if the amount of pixels of the image exceeds `maxPixels`_. + +You can use the following parameters in this message: + +==================== ========================================================= +Parameter Description +==================== ========================================================= +``{{ height }}`` The current image height +``{{ max_pixels }}`` The maximum allowed amount of pixels +``{{ pixels }}`` The current amount of pixels +``{{ width }}`` The current image width +==================== ========================================================= + +``maxRatio`` +~~~~~~~~~~~~ + +**type**: ``float`` + +If set, the aspect ratio (``width / height``) of the image file must be less +than or equal to this value. + +.. versionadded:: 7.3 + + The ``maxRatio`` option support for SVG files was introduced in Symfony 7.3. + +``maxRatioMessage`` +~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The image ratio is too big ({{ ratio }}). +Allowed maximum ratio is {{ max_ratio }}`` + +The error message if the aspect ratio of the image exceeds `maxRatio`_. + +You can use the following parameters in this message: + +=================== ========================================================== +Parameter Description +=================== ========================================================== +``{{ max_ratio }}`` The maximum required ratio +``{{ ratio }}`` The current (invalid) ratio +=================== ========================================================== + +``maxWidth`` +~~~~~~~~~~~~ **type**: ``integer`` If set, the width of the image file must be less than or equal to this value in pixels. -minHeight -~~~~~~~~~ +``maxWidthMessage`` +~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The image width is too big ({{ width }}px). +Allowed maximum width is {{ max_width }}px.`` + +The error message if the width of the image exceeds `maxWidth`_. + +You can use the following parameters in this message: + +=================== ========================================================== +Parameter Description +=================== ========================================================== +``{{ max_width }}`` The maximum allowed width +``{{ width }}`` The current (invalid) width +=================== ========================================================== + +``mimeTypes`` +~~~~~~~~~~~~~ + +**type**: ``array`` or ``string`` **default**: ``image/*`` + +You can find a list of existing image mime types on the `IANA website`_. + +``mimeTypesMessage`` +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This file is not a valid image.`` + +If all the values of the `mimeTypes`_ option are a subset of ``image/*``, the +error message will be instead: ``The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.`` + +.. include:: /reference/constraints/_parameters-mime-types-message-option.rst.inc + +``minHeight`` +~~~~~~~~~~~~~ **type**: ``integer`` If set, the height of the image file must be greater than or equal to this value in pixels. -maxHeight -~~~~~~~~~ +``minHeightMessage`` +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The image height is too small ({{ height }}px). +Minimum height expected is {{ min_height }}px.`` + +The error message if the height of the image is less than `minHeight`_. + +You can use the following parameters in this message: + +==================== ========================================================= +Parameter Description +==================== ========================================================= +``{{ height }}`` The current (invalid) height +``{{ min_height }}`` The minimum required height +==================== ========================================================= + +``minPixels`` +~~~~~~~~~~~~~ **type**: ``integer`` -If set, the height of the image file must be less than or equal to this -value in pixels. +If set, the amount of pixels of the image file must be greater than or equal to this +value. -sizeNotDetectedMessage -~~~~~~~~~~~~~~~~~~~~~~ +``minPixelsMessage`` +~~~~~~~~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``The size of the image could not be detected`` +**type**: ``string`` **default**: ``The image has too few pixels ({{ pixels }} pixels). +Minimum amount expected is {{ min_pixels }} pixels.`` -If the system is unable to determine the size of the image, this error will -be displayed. This will only occur when at least one of the four size constraint -options has been set. +The error message if the amount of pixels of the image is less than `minPixels`_. -maxWidthMessage -~~~~~~~~~~~~~~~ +You can use the following parameters in this message: -**type**: ``string`` **default**: ``The image width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px`` +==================== ========================================================= +Parameter Description +==================== ========================================================= +``{{ height }}`` The current image height +``{{ min_pixels }}`` The minimum required amount of pixels +``{{ pixels }}`` The current amount of pixels +``{{ width }}`` The current image width +==================== ========================================================= -The error message if the width of the image exceeds `maxWidth`_. +``minRatio`` +~~~~~~~~~~~~ -minWidthMessage -~~~~~~~~~~~~~~~ +**type**: ``float`` -**type**: ``string`` **default**: ``The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px`` +If set, the aspect ratio (``width / height``) of the image file must be greater +than or equal to this value. + +.. versionadded:: 7.3 + + The ``minRatio`` option support for SVG files was introduced in Symfony 7.3. + +``minRatioMessage`` +~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The image ratio is too small ({{ ratio }}). +Minimum ratio expected is {{ min_ratio }}`` + +The error message if the aspect ratio of the image is less than `minRatio`_. + +You can use the following parameters in this message: + +=================== ========================================================== +Parameter Description +=================== ========================================================== +``{{ min_ratio }}`` The minimum required ratio +``{{ ratio }}`` The current (invalid) ratio +=================== ========================================================== + +``minWidth`` +~~~~~~~~~~~~ + +**type**: ``integer`` + +If set, the width of the image file must be greater than or equal to this +value in pixels. + +``minWidthMessage`` +~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The image width is too small ({{ width }}px). +Minimum width expected is {{ min_width }}px.`` The error message if the width of the image is less than `minWidth`_. -maxHeightMessage -~~~~~~~~~~~~~~~~ +You can use the following parameters in this message: -**type**: ``string`` **default**: ``The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px`` +=================== ========================================================== +Parameter Description +=================== ========================================================== +``{{ min_width }}`` The minimum required width +``{{ width }}`` The current (invalid) width +=================== ========================================================== -The error message if the height of the image exceeds `maxHeight`_. +``sizeNotDetectedMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ -minHeightMessage -~~~~~~~~~~~~~~~~ +**type**: ``string`` **default**: ``The size of the image could not be detected.`` -**type**: ``string`` **default**: ``The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px`` +If the system is unable to determine the size of the image, this error will +be displayed. This will only occur when at least one of the size constraint +options has been set. -The error message if the height of the image is less than `minHeight`_. +This message has no parameters. -.. _`IANA website`: http://www.iana.org/assignments/media-types/image/index.html +.. _`IANA website`: https://www.iana.org/assignments/media-types/media-types.xhtml +.. _`PHP GD extension`: https://www.php.net/manual/en/book.image.php diff --git a/reference/constraints/Ip.rst b/reference/constraints/Ip.rst index ede78b3efea..20cd4400c0a 100644 --- a/reference/constraints/Ip.rst +++ b/reference/constraints/Ip.rst @@ -5,108 +5,122 @@ Validates that a value is a valid IP address. By default, this will validate the value as IPv4, but a number of different options exist to validate as IPv6 and many other combinations. -+----------------+---------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+---------------------------------------------------------------------+ -| Options | - `version`_ | -| | - `message`_ | -+----------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Ip` | -+----------------+---------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\IpValidator` | -+----------------+---------------------------------------------------------------------+ +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Ip` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\IpValidator` +========== =================================================================== Basic Usage ----------- .. configuration-block:: - .. code-block:: yaml - - # src/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - ipAddress: - - Ip: + .. code-block:: php-attributes - .. code-block:: php-annotations + // src/Entity/Author.php + namespace App\Entity; - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\Ip - */ - protected $ipAddress; + #[Assert\Ip] + protected string $ipAddress; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + ipAddress: + - Ip: ~ + .. code-block:: xml - - - - - - + + + + + + + + + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - - use Symfony\Component\Validator\Mapping\ClassMetadata; + // src/Entity/Author.php + namespace App\Entity; + use Symfony\Component\Validator\Constraints as Assert; - + use Symfony\Component\Validator\Mapping\ClassMetadata; + class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('ipAddress', new Assert\Ip()); } } +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + Options ------- -version -~~~~~~~ +.. include:: /reference/constraints/_groups-option.rst.inc -**type**: ``string`` **default**: ``4`` +``message`` +~~~~~~~~~~~ -This determines exactly *how* the ip address is validated and can take one -of a variety of different values: +**type**: ``string`` **default**: ``This is not a valid IP address.`` -**All ranges** - -* ``4`` - Validates for IPv4 addresses -* ``6`` - Validates for IPv6 addresses -* ``all`` - Validates all IP formats - -**No private ranges** +This message is shown if the string is not a valid IP address. -* ``4_no_priv`` - Validates for IPv4 but without private IP ranges -* ``6_no_priv`` - Validates for IPv6 but without private IP ranges -* ``all_no_priv`` - Validates for all IP formats but without private IP ranges +You can use the following parameters in this message: -**No reserved ranges** +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== -* ``4_no_res`` - Validates for IPv4 but without reserved IP ranges -* ``6_no_res`` - Validates for IPv6 but without reserved IP ranges -* ``all_no_res`` - Validates for all IP formats but without reserved IP ranges +.. include:: /reference/constraints/_normalizer-option.rst.inc -**Only public ranges** +.. include:: /reference/constraints/_payload-option.rst.inc -* ``4_public`` - Validates for IPv4 but without private and reserved ranges -* ``6_public`` - Validates for IPv6 but without private and reserved ranges -* ``all_public`` - Validates for all IP formats but without private and reserved ranges +.. _reference-constraint-ip-version: -message -~~~~~~~ +``version`` +~~~~~~~~~~~ -**type**: ``string`` **default**: ``This is not a valid IP address`` +**type**: ``string`` **default**: ``4`` -This message is shown if the string is not a valid IP address. +This determines exactly *how* the IP address is validated. This option defines a +lot of different possible values based on the ranges and the type of IP address +that you want to allow/deny: + +==================== =================== =================== ================== +Ranges Allowed IPv4 addresses only IPv6 addresses only Both IPv4 and IPv6 +==================== =================== =================== ================== +All ``4`` ``6`` ``all`` +All except private ``4_no_priv`` ``6_no_priv`` ``all_no_priv`` +All except reserved ``4_no_res`` ``6_no_res`` ``all_no_res`` +All except public ``4_no_public`` ``6_no_public`` ``all_no_public`` +Only private ``4_private`` ``6_private`` ``all_private`` +Only reserved ``4_reserved`` ``6_reserved`` ``all_reserved`` +Only public ``4_public`` ``6_public`` ``all_public`` +==================== =================== =================== ================== + +.. versionadded:: 7.1 + + The ``*_no_public``, ``*_reserved`` and ``*_public`` ranges were introduced + in Symfony 7.1. diff --git a/reference/constraints/IsFalse.rst b/reference/constraints/IsFalse.rst new file mode 100644 index 00000000000..3d0a1665944 --- /dev/null +++ b/reference/constraints/IsFalse.rst @@ -0,0 +1,130 @@ +IsFalse +======= + +Validates that a value is ``false``. Specifically, this checks to see if +the value is exactly ``false``, exactly the integer ``0``, or exactly the +string ``'0'``. + +Also see :doc:`IsTrue `. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\IsFalse` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\IsFalseValidator` +========== =================================================================== + +Basic Usage +----------- + +The ``IsFalse`` constraint can be applied to a property or a "getter" method, +but is most commonly useful in the latter case. For example, suppose that +you want to guarantee that some ``state`` property is *not* in a dynamic +``invalidStates`` array. First, you'd create a "getter" method:: + + protected string $state; + + protected array $invalidStates = []; + + public function isStateInvalid(): bool + { + return in_array($this->state, $this->invalidStates); + } + +In this case, the underlying object is only valid if the ``isStateInvalid()`` +method returns **false**: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + #[Assert\IsFalse( + message: "You've entered an invalid state." + )] + public function isStateInvalid(): bool + { + // ... + } + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + getters: + stateInvalid: + - 'IsFalse': + message: You've entered an invalid state. + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Author + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addGetterConstraint('stateInvalid', new Assert\IsFalse( + message: "You've entered an invalid state.", + )); + } + + public function isStateInvalid(): bool + { + // ... + } + } + +.. include:: /reference/constraints/_null-values-are-valid.rst.inc + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be false.`` + +This message is shown if the underlying data is not false. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/IsNull.rst b/reference/constraints/IsNull.rst new file mode 100644 index 00000000000..0f9726110ba --- /dev/null +++ b/reference/constraints/IsNull.rst @@ -0,0 +1,99 @@ +IsNull +====== + +Validates that a value is exactly equal to ``null``. To force that a property +is blank (blank string or ``null``), see the :doc:`/reference/constraints/Blank` +constraint. To ensure that a property is not null, see :doc:`/reference/constraints/NotNull`. + +Also see :doc:`NotNull `. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\IsNull` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\IsNullValidator` +========== =================================================================== + +Basic Usage +----------- + +If, for some reason, you wanted to ensure that the ``firstName`` property +of an ``Author`` class exactly equal to ``null``, you could do the following: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + #[Assert\IsNull] + protected ?string $firstName = null; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + firstName: + - 'IsNull': ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Author + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('firstName', Assert\IsNull()); + } + } + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be null.`` + +This is the message that will be shown if the value is not ``null``. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/IsTrue.rst b/reference/constraints/IsTrue.rst new file mode 100644 index 00000000000..b50ba4f3e8b --- /dev/null +++ b/reference/constraints/IsTrue.rst @@ -0,0 +1,138 @@ +IsTrue +====== + +Validates that a value is ``true``. Specifically, this checks if the value is +exactly ``true``, exactly the integer ``1``, or exactly the string ``'1'``. + +Also see :doc:`IsFalse `. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\IsTrue` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\IsTrueValidator` +========== =================================================================== + +Basic Usage +----------- + +This constraint can be applied to properties (e.g. a ``termsAccepted`` property +on a registration model) and methods. It's most powerful in the latter case, +where you can assert that a method returns a true value. For example, suppose +you have the following method:: + + // src/Entity/Author.php + namespace App\Entity; + + class Author + { + protected string $token; + + public function isTokenValid(): bool + { + return $this->token === $this->generateToken(); + } + } + +Then you can validate this method with ``IsTrue`` as follows: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + protected string $token; + + #[Assert\IsTrue(message: 'The token is invalid.')] + public function isTokenValid(): bool + { + return $this->token === $this->generateToken(); + } + + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + getters: + tokenValid: + - 'IsTrue': + message: The token is invalid. + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints\IsTrue; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Author + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addGetterConstraint('tokenValid', new IsTrue( + message: 'The token is invalid.', + )); + } + + public function isTokenValid(): bool + { + return $this->token === $this->generateToken(); + } + + // ... + } + +If the ``isTokenValid()`` returns false, the validation will fail. + +.. include:: /reference/constraints/_null-values-are-valid.rst.inc + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be true.`` + +This message is shown if the underlying data is not true. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Isbn.rst b/reference/constraints/Isbn.rst new file mode 100644 index 00000000000..52d10565fe5 --- /dev/null +++ b/reference/constraints/Isbn.rst @@ -0,0 +1,171 @@ +Isbn +==== + +This constraint validates that an `International Standard Book Number (ISBN)`_ +is either a valid ISBN-10 or a valid ISBN-13. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Isbn` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\IsbnValidator` +========== =================================================================== + +Basic Usage +----------- + +To use the ``Isbn`` validator, apply it to a property or method +on an object that will contain an ISBN. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Book.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Book + { + #[Assert\Isbn( + type: Assert\Isbn::ISBN_10, + message: 'This value is not valid.', + )] + protected string $isbn; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Book: + properties: + isbn: + - Isbn: + type: isbn10 + message: This value is not valid. + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Book.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Book + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('isbn', new Assert\Isbn( + type: Assert\Isbn::ISBN_10, + message: 'This value is not valid.', + )); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Available Options +----------------- + +``bothIsbnMessage`` +~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is neither a valid ISBN-10 nor a valid ISBN-13.`` + +The message that will be shown if the `type`_ option is ``null`` and the given +value does not pass any of the ISBN checks. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_groups-option.rst.inc + +``isbn10Message`` +~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid ISBN-10.`` + +The message that will be shown if the `type`_ option is ``isbn10`` and the given +value does not pass the ISBN-10 check. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +``isbn13Message`` +~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid ISBN-13.`` + +The message that will be shown if the `type`_ option is ``isbn13`` and the given +value does not pass the ISBN-13 check. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +The message that will be shown if the value is not valid. If not ``null``, +this message has priority over all the other messages. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +``type`` +~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +The type of ISBN to validate against. Valid values are ``isbn10``, ``isbn13`` +and ``null`` to accept any kind of ISBN. + +.. _`International Standard Book Number (ISBN)`: https://en.wikipedia.org/wiki/Isbn diff --git a/reference/constraints/Isin.rst b/reference/constraints/Isin.rst new file mode 100644 index 00000000000..d611cf60898 --- /dev/null +++ b/reference/constraints/Isin.rst @@ -0,0 +1,97 @@ +Isin +==== + +Validates that a value is a valid +`International Securities Identification Number (ISIN)`_. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Isin` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\IsinValidator` +========== =================================================================== + +Basic Usage +----------- + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/UnitAccount.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class UnitAccount + { + #[Assert\Isin] + protected string $isin; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\UnitAccount: + properties: + isin: + - Isin: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/UnitAccount.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class UnitAccount + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('isin', new Assert\Isin()); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +message +~~~~~~~ + +**type**: ``string`` default: ``This value is not a valid International Securities Identification Number (ISIN).`` + +The message shown if the given value is not a valid ISIN. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`International Securities Identification Number (ISIN)`: https://en.wikipedia.org/wiki/International_Securities_Identification_Number diff --git a/reference/constraints/Issn.rst b/reference/constraints/Issn.rst new file mode 100644 index 00000000000..fa2fbae5bf5 --- /dev/null +++ b/reference/constraints/Issn.rst @@ -0,0 +1,113 @@ +Issn +==== + +Validates that a value is a valid +`International Standard Serial Number (ISSN)`_. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Issn` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\IssnValidator` +========== =================================================================== + +Basic Usage +----------- + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Journal.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Journal + { + #[Assert\Issn] + protected string $issn; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Journal: + properties: + issn: + - Issn: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Journal.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Journal + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('issn', new Assert\Issn()); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +``caseSensitive`` +~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` default: ``false`` + +The validator will allow ISSN values to end with a lower case 'x' by default. +When switching this to ``true``, the validator requires an upper case 'X'. + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` default: ``This value is not a valid ISSN.`` + +The message shown if the given value is not a valid ISSN. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +``requireHyphen`` +~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` default: ``false`` + +The validator will allow non hyphenated ISSN values by default. When switching +this to ``true``, the validator requires a hyphenated ISSN value. + +.. _`International Standard Serial Number (ISSN)`: https://en.wikipedia.org/wiki/Issn diff --git a/reference/constraints/Json.rst b/reference/constraints/Json.rst new file mode 100644 index 00000000000..337b2dc6a1e --- /dev/null +++ b/reference/constraints/Json.rst @@ -0,0 +1,90 @@ +Json +==== + +Validates that a value has valid `JSON`_ syntax. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Json` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\JsonValidator` +========== =================================================================== + +Basic Usage +----------- + +The ``Json`` constraint can be applied to a property or a "getter" method: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Book.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Book + { + #[Assert\Json( + message: "You've entered an invalid Json." + )] + private string $chapters; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Book: + properties: + chapters: + - Json: + message: You've entered an invalid Json. + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Book.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Book + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('chapters', new Assert\Json( + message: 'You\'ve entered an invalid Json.', + )); + } + } + +Options +------- + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be valid JSON.`` + +This message is shown if the underlying data is not a valid JSON value. + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`JSON`: https://en.wikipedia.org/wiki/JSON diff --git a/reference/constraints/Language.rst b/reference/constraints/Language.rst index 7d4639802bb..e3752c4d47f 100644 --- a/reference/constraints/Language.rst +++ b/reference/constraints/Language.rst @@ -1,77 +1,107 @@ Language ======== -Validates that a value is a valid language code. - -+----------------+------------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+------------------------------------------------------------------------+ -| Options | - `message`_ | -+----------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Language` | -+----------------+------------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\LanguageValidator` | -+----------------+------------------------------------------------------------------------+ +Validates that a value is a valid language *Unicode language identifier* +(e.g. ``fr`` or ``zh-Hant``). + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Language` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\LanguageValidator` +========== =================================================================== Basic Usage ----------- .. configuration-block:: - .. code-block:: yaml + .. code-block:: php-attributes - # src/UserBundle/Resources/config/validation.yml - Acme\UserBundle\Entity\User: - properties: - preferredLanguage: - - Language: + // src/Entity/User.php + namespace App\Entity; - .. code-block:: php-annotations - - // src/Acme/UserBundle/Entity/User.php - namespace Acme\UserBundle\Entity; - use Symfony\Component\Validator\Constraints as Assert; - + class User { - /** - * @Assert\Language - */ - protected $preferredLanguage; + #[Assert\Language] + protected string $preferredLanguage; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\User: + properties: + preferredLanguage: + - Language: ~ + .. code-block:: xml - - - - - - + + + + + + + + + + .. code-block:: php - // src/Acme/UserBundle/Entity/User.php - namespace Acme\UserBundle\Entity; - - use Symfony\Component\Validator\Mapping\ClassMetadata; + // src/Entity/User.php + namespace App\Entity; + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class User { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('preferredLanguage', new Assert\Language()); } } +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + Options ------- -message -~~~~~~~ +alpha3 +~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +If this option is ``true``, the constraint checks that the value is a +`ISO 639-2 (2T)`_ three-letter code (e.g. French = ``fra``) instead of the default +`ISO 639-1`_ two-letter code (e.g. French = ``fr``). -**type**: ``string`` **default**: ``This value is not a valid language`` +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid language.`` This message is shown if the string is not a valid language code. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`ISO 639-1`: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes +.. _`ISO 639-2 (2T)`: https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes diff --git a/reference/constraints/Length.rst b/reference/constraints/Length.rst index a78cf42c62e..c1a8575070b 100644 --- a/reference/constraints/Length.rst +++ b/reference/constraints/Length.rst @@ -3,146 +3,240 @@ Length Validates that a given string length is *between* some minimum and maximum value. -.. versionadded:: 2.1 - The Length constraint was added in Symfony 2.1. - -+----------------+----------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+----------------------------------------------------------------------+ -| Options | - `min`_ | -| | - `max`_ | -| | - `charset`_ | -| | - `minMessage`_ | -| | - `maxMessage`_ | -| | - `exactMessage`_ | -+----------------+----------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Length` | -+----------------+----------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\LengthValidator` | -+----------------+----------------------------------------------------------------------+ +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Length` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\LengthValidator` +========== =================================================================== Basic Usage ----------- -To verify that the ``firstName`` field length of a class is between "2" and -"50", you might add the following: +To verify that the ``firstName`` field length of a class is between ``2`` +and ``50``, you might add the following: .. configuration-block:: - .. code-block:: yaml - - # src/Acme/EventBundle/Resources/config/validation.yml - Acme\EventBundle\Entity\Participant: - properties: - firstName: - - Length: - min: 2 - max: 50 - minMessage: "Your first name must be at least {{ limit }} characters length" - maxMessage: "Your first name cannot be longer than than {{ limit }} characters length" + .. code-block:: php-attributes - .. code-block:: php-annotations - - // src/Acme/EventBundle/Entity/Participant.php - namespace Acme\EventBundle\Entity; + // src/Entity/Participant.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; class Participant { - /** - * @Assert\Length( - * min = "2", - * max = "50", - * minMessage = "Your first name must be at least {{ limit }} characters length", - * maxMessage = "Your first name cannot be longer than than {{ limit }} characters length" - * ) - */ - protected $firstName; + #[Assert\Length( + min: 2, + max: 50, + minMessage: 'Your first name must be at least {{ limit }} characters long', + maxMessage: 'Your first name cannot be longer than {{ limit }} characters', + )] + protected string $firstName; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Participant: + properties: + firstName: + - Length: + min: 2 + max: 50 + minMessage: 'Your first name must be at least {{ limit }} characters long' + maxMessage: 'Your first name cannot be longer than {{ limit }} characters' + .. code-block:: xml - - - - - - - - - - - + + + + + + + + + + + + + + + .. code-block:: php - // src/Acme/EventBundle/Entity/Participant.php - namespace Acme\EventBundle\Entity; + // src/Entity/Participant.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Participant { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('firstName', new Assert\Length(array( - 'min' => 2, - 'max' => 50, - 'minMessage' => 'Your first name must be at least {{ limit }} characters length', - 'maxMessage' => 'Your first name cannot be longer than than {{ limit }} characters length', - ))); + $metadata->addPropertyConstraint('firstName', new Assert\Length( + min: 2, + max: 50, + minMessage: 'Your first name must be at least {{ limit }} characters long', + maxMessage: 'Your first name cannot be longer than {{ limit }} characters', + )); } } +.. include:: /reference/constraints/_null-values-are-valid.rst.inc + Options ------- -min -~~~ +``charset`` +~~~~~~~~~~~ -**type**: ``integer`` [:ref:`default option`] +**type**: ``string`` **default**: ``UTF-8`` -This required option is the "min" length value. Validation will fail if the given -value's length is **less** than this min value. +The charset to be used when computing value's length with the +:phpfunction:`mb_check_encoding` and :phpfunction:`mb_strlen` +PHP functions. -max -~~~ +``charsetMessage`` +~~~~~~~~~~~~~~~~~~ -**type**: ``integer`` [:ref:`default option`] +**type**: ``string`` **default**: ``This value does not match the expected {{ charset }} charset.`` -This required option is the "max" length value. Validation will fail if the given -value's length is **greater** than this max value. +The message that will be shown if the value is not using the given `charset`_. -charset -~~~~~~~ +You can use the following parameters in this message: -**type**: ``string`` **default**: ``UTF-8`` +================= ============================================================ +Parameter Description +================= ============================================================ +``{{ charset }}`` The expected charset +``{{ value }}`` The current (invalid) value +================= ============================================================ + +``countUnit`` +~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``Length::COUNT_CODEPOINTS`` + +The character count unit to use for the length check. By default :phpfunction:`mb_strlen` +is used, which counts Unicode code points. -The charset to be used when computing value's length. The :phpfunction:`grapheme_strlen` PHP -function is used if available. If not, the the :phpfunction:`mb_strlen` PHP function -is used if available. If neither are available, the :phpfunction:`strlen` PHP function -is used. +Can be one of the following constants of the +:class:`Symfony\\Component\\Validator\\Constraints\\Length` class: -minMessage -~~~~~~~~~~ +* ``COUNT_BYTES``: Uses :phpfunction:`strlen` counting the length of the string in bytes. +* ``COUNT_CODEPOINTS``: Uses :phpfunction:`mb_strlen` counting the length of the string in Unicode + code points. This was the sole behavior until Symfony 6.2 and is the default since Symfony 6.3. + Simple (multibyte) Unicode characters count as 1 character, while for example ZWJ sequences of + composed emojis count as multiple characters. +* ``COUNT_GRAPHEMES``: Uses :phpfunction:`grapheme_strlen` counting the length of the string in + graphemes, i.e. even emojis and ZWJ sequences of composed emojis count as 1 character. -**type**: ``string`` **default**: ``This value is too short. It should have {{ limit }} characters or more.``. +``exactly`` +~~~~~~~~~~~ -The message that will be shown if the underlying value's length is less than the `min`_ option. +**type**: ``integer`` -maxMessage -~~~~~~~~~~ +This option is the exact length value. Validation will fail if +the given value's length is not **exactly** equal to this value. -**type**: ``string`` **default**: ``This value is too long. It should have {{ limit }} characters or less.``. +.. note:: -The message that will be shown if the underlying value's length is more than the `max`_ option. + This option is the one being set by default when using the Length constraint + without passing any named argument to it. This means that for example, + ``#[Assert\Length(20)]`` and ``#[Assert\Length(exactly: 20)]`` are equivalent. -exactMessage -~~~~~~~~~~~~ +``exactMessage`` +~~~~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``This value should have exactly {{ limit }} characters.``. +**type**: ``string`` **default**: ``This value should have exactly {{ limit }} characters.`` The message that will be shown if min and max values are equal and the underlying value's length is not exactly this value. + +You can use the following parameters in this message: + +====================== ============================================================ +Parameter Description +====================== ============================================================ +``{{ limit }}`` The exact expected length +``{{ value }}`` The current (invalid) value +``{{ value_length }}`` The current value's length +====================== ============================================================ + +.. include:: /reference/constraints/_groups-option.rst.inc + +``max`` +~~~~~~~ + +**type**: ``integer`` + +This option is the "max" length value. Validation will fail if +the given value's length is **greater** than this max value. + +This option is required when the ``min`` option is not defined. + +``maxMessage`` +~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is too long. It should have {{ limit }} characters or less.`` + +The message that will be shown if the underlying value's length is more +than the `max`_ option. + +You can use the following parameters in this message: + +====================== ============================================================ +Parameter Description +====================== ============================================================ +``{{ limit }}`` The expected maximum length +``{{ value }}`` The current (invalid) value +``{{ value_length }}`` The current value's length +====================== ============================================================ + +``min`` +~~~~~~~ + +**type**: ``integer`` + +This option is the "min" length value. Validation will fail if +the given value's length is **less** than this min value. + +This option is required when the ``max`` option is not defined. + +It is important to notice that ``null`` values are considered +valid no matter if the constraint requires a minimum length. Validators +are triggered only if the value is not ``null``. + +``minMessage`` +~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is too short. It should have {{ limit }} characters or more.`` + +The message that will be shown if the underlying value's length is less +than the `min`_ option. + +You can use the following parameters in this message: + +====================== ============================================================ +Parameter Description +====================== ============================================================ +``{{ limit }}`` The expected minimum length +``{{ value }}`` The current (invalid) value +``{{ value_length }}`` The current value's length +====================== ============================================================ + +.. include:: /reference/constraints/_normalizer-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/LessThan.rst b/reference/constraints/LessThan.rst new file mode 100644 index 00000000000..3d23bcda445 --- /dev/null +++ b/reference/constraints/LessThan.rst @@ -0,0 +1,308 @@ +LessThan +======== + +Validates that a value is less than another value, defined in the options. To +force that a value is less than or equal to another value, see +:doc:`/reference/constraints/LessThanOrEqual`. To force a value is greater +than another value, see :doc:`/reference/constraints/GreaterThan`. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\LessThan` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\LessThanValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraints ensure that: + +* the number of ``siblings`` of a ``Person`` is less than ``5`` +* ``age`` is less than ``80`` + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\LessThan(5)] + protected int $siblings; + + #[Assert\LessThan( + value: 80, + )] + protected int $age; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + siblings: + - LessThan: 5 + age: + - LessThan: + value: 80 + + .. code-block:: xml + + + + + + + + + 5 + + + + + + + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('siblings', new Assert\LessThan(5)); + + $metadata->addPropertyConstraint('age', new Assert\LessThan( + value: 80, + )); + } + } + +Comparing Dates +--------------- + +This constraint can be used to compare ``DateTime`` objects against any date +string `accepted by the DateTime constructor`_. For example, you could check +that a date must be in the past like this: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\LessThan('today')] + protected \DateTimeInterface $dateOfBirth; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + dateOfBirth: + - LessThan: today + + .. code-block:: xml + + + + + + + + today + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('dateOfBirth', new Assert\LessThan('today')); + } + } + +Be aware that PHP will use the server's configured timezone to interpret these +dates. If you want to fix the timezone, append it to the date string: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\LessThan('today UTC')] + protected \DateTimeInterface $dateOfBirth; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + dateOfBirth: + - LessThan: today UTC + + .. code-block:: xml + + + + + + + + today UTC + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('age', new Assert\LessThan('today UTC')); + } + } + +The ``DateTime`` class also accepts relative dates or times. For example, you +can check that a person must be at least 18 years old like this: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\LessThan('-18 years')] + protected \DateTimeInterface $dateOfBirth; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + dateOfBirth: + - LessThan: -18 years + + .. code-block:: xml + + + + + + + + -18 years + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('dateOfBirth', new Assert\LessThan('-18 years')); + } + } + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be less than {{ compared_value }}.`` + +This is the message that will be shown if the value is not less than the +comparison value. + +You can use the following parameters in this message: + +============================= ================================================ +Parameter Description +============================= ================================================ +``{{ compared_value }}`` The upper limit +``{{ compared_value_type }}`` The expected value type +``{{ value }}`` The current (invalid) value +============================= ================================================ + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. include:: /reference/constraints/_comparison-propertypath-option.rst.inc + +.. include:: /reference/constraints/_comparison-value-option.rst.inc + +.. _`accepted by the DateTime constructor`: https://www.php.net/manual/en/datetime.formats.php diff --git a/reference/constraints/LessThanOrEqual.rst b/reference/constraints/LessThanOrEqual.rst new file mode 100644 index 00000000000..ac66c62d7d0 --- /dev/null +++ b/reference/constraints/LessThanOrEqual.rst @@ -0,0 +1,307 @@ +LessThanOrEqual +=============== + +Validates that a value is less than or equal to another value, defined in the +options. To force that a value is less than another value, see +:doc:`/reference/constraints/LessThan`. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\LessThanOrEqual` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\LessThanOrEqualValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraints ensure that: + +* the number of ``siblings`` of a ``Person`` is less than or equal to ``5`` +* the ``age`` is less than or equal to ``80`` + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\LessThanOrEqual(5)] + protected int $siblings; + + #[Assert\LessThanOrEqual( + value: 80, + )] + protected int $age; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + siblings: + - LessThanOrEqual: 5 + age: + - LessThanOrEqual: + value: 80 + + .. code-block:: xml + + + + + + + + + 5 + + + + + + + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('siblings', new Assert\LessThanOrEqual(5)); + + $metadata->addPropertyConstraint('age', new Assert\LessThanOrEqual( + value: 80, + )); + } + } + +Comparing Dates +--------------- + +This constraint can be used to compare ``DateTime`` objects against any date +string `accepted by the DateTime constructor`_. For example, you could check +that a date must be today or in the past like this: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\LessThanOrEqual('today')] + protected \DateTimeInterface $dateOfBirth; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + dateOfBirth: + - LessThanOrEqual: today + + .. code-block:: xml + + + + + + + + today + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('dateOfBirth', new Assert\LessThanOrEqual('today')); + } + } + +Be aware that PHP will use the server's configured timezone to interpret these +dates. If you want to fix the timezone, append it to the date string: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\LessThanOrEqual('today UTC')] + protected \DateTimeInterface $dateOfBirth; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + dateOfBirth: + - LessThanOrEqual: today UTC + + .. code-block:: xml + + + + + + + + today UTC + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('dateOfBirth', new Assert\LessThanOrEqual('today UTC')); + } + } + +The ``DateTime`` class also accepts relative dates or times. For example, you +can check that a person must be at least 18 years old like this: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\LessThanOrEqual('-18 years')] + protected \DateTimeInterface $dateOfBirth; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + dateOfBirth: + - LessThanOrEqual: -18 years + + .. code-block:: xml + + + + + + + + -18 years + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('dateOfBirth', new Assert\LessThanOrEqual('-18 years')); + } + } + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be less than or equal to {{ compared_value }}.`` + +This is the message that will be shown if the value is not less than or equal +to the comparison value. + +You can use the following parameters in this message: + +============================= ================================================ +Parameter Description +============================= ================================================ +``{{ compared_value }}`` The upper limit +``{{ compared_value_type }}`` The expected value type +``{{ value }}`` The current (invalid) value +============================= ================================================ + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. include:: /reference/constraints/_comparison-propertypath-option.rst.inc + +.. include:: /reference/constraints/_comparison-value-option.rst.inc + +.. _`accepted by the DateTime constructor`: https://www.php.net/manual/en/datetime.formats.php diff --git a/reference/constraints/Locale.rst b/reference/constraints/Locale.rst index b841651577b..4bba45ae12b 100644 --- a/reference/constraints/Locale.rst +++ b/reference/constraints/Locale.rst @@ -3,79 +3,112 @@ Locale Validates that a value is a valid locale. -The "value" for each locale is either the two letter ISO639-1 *language* code -(e.g. ``fr``), or the language code followed by an underscore (``_``), then -the ISO3166 *country* code (e.g. ``fr_FR`` for French/France). - -+----------------+------------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+------------------------------------------------------------------------+ -| Options | - `message`_ | -+----------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Locale` | -+----------------+------------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\LocaleValidator` | -+----------------+------------------------------------------------------------------------+ +The "value" for each locale is any of the `ICU format locale IDs`_. For example, +the two letter `ISO 639-1`_ *language* code (e.g. ``fr``), or the language code +followed by an underscore (``_``) and the `ISO 3166-1 alpha-2`_ *country* code +(e.g. ``fr_FR`` for French/France). + +The given locale values are *canonicalized* before validating them to avoid +issues with wrong uppercase/lowercase values and to remove unneeded elements +(e.g. ``FR-fr.utf8`` will be validated as ``fr_FR``). + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Locale` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\LocaleValidator` +========== =================================================================== Basic Usage ----------- .. configuration-block:: - .. code-block:: yaml - - # src/UserBundle/Resources/config/validation.yml - Acme\UserBundle\Entity\User: - properties: - locale: - - Locale: + .. code-block:: php-attributes - .. code-block:: php-annotations + // src/Entity/User.php + namespace App\Entity; - // src/Acme/UserBundle/Entity/User.php - namespace Acme\UserBundle\Entity; - use Symfony\Component\Validator\Constraints as Assert; class User { - /** - * @Assert\Locale - */ - protected $locale; + #[Assert\Locale( + canonicalize: true, + )] + protected string $locale; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\User: + properties: + locale: + - Locale: + canonicalize: true + .. code-block:: xml - - - - - - + + + + + + + + + + + + .. code-block:: php - // src/Acme/UserBundle/Entity/User.php - namespace Acme\UserBundle\Entity; - - use Symfony\Component\Validator\Mapping\ClassMetadata; + // src/Entity/User.php + namespace App\Entity; + use Symfony\Component\Validator\Constraints as Assert; - + use Symfony\Component\Validator\Mapping\ClassMetadata; + class User { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('locale', new Assert\Locale()); + $metadata->addPropertyConstraint('locale', new Assert\Locale( + canonicalize: true, + )); } } +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + Options ------- -message -~~~~~~~ +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ -**type**: ``string`` **default**: ``This value is not a valid locale`` +**type**: ``string`` **default**: ``This value is not a valid locale.`` This message is shown if the string is not a valid locale. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`ICU format locale IDs`: https://unicode-org.github.io/icu/userguide/locale/ +.. _`ISO 639-1`: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes +.. _`ISO 3166-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes diff --git a/reference/constraints/Luhn.rst b/reference/constraints/Luhn.rst index f2103cc2a62..0c835204091 100644 --- a/reference/constraints/Luhn.rst +++ b/reference/constraints/Luhn.rst @@ -1,35 +1,41 @@ Luhn -====== +==== -.. versionadded:: 2.2 - The Luhn validation is new in Symfony 2.2. +This constraint is used to ensure that a credit card number passes the +`Luhn algorithm`_. It is useful as a first step to validating a credit +card: before communicating with a payment gateway. -This constraint is used to ensure that a credit card number passes the `Luhn algorithm`_. -It is useful as a first step to validating a credit card: before communicating with a -payment gateway. - -+----------------+-----------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+-----------------------------------------------------------------------+ -| Options | - `message`_ | -+----------------+-----------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Luhn` | -+----------------+-----------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\LuhnValidator` | -+----------------+-----------------------------------------------------------------------+ +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Luhn` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\LuhnValidator` +========== =================================================================== Basic Usage ----------- -To use the Luhn validator, simply apply it to a property on an object that +To use the Luhn validator, apply it to a property on an object that will contain a credit card number. .. configuration-block:: + .. code-block:: php-attributes + + // src/Entity/Transaction.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Transaction + { + #[Assert\Luhn(message: 'Please check your credit card number.')] + protected string $cardNumber; + } + .. code-block:: yaml - # src/Acme/SubscriptionBundle/Resources/config/validation.yml - Acme\SubscriptionBundle\Entity\Transaction: + # config/validator/validation.yaml + App\Entity\Transaction: properties: cardNumber: - Luhn: @@ -37,54 +43,64 @@ will contain a credit card number. .. code-block:: xml - - - - - - - - - - .. code-block:: php-annotations - - // src/Acme/SubscriptionBundle/Entity/Transaction.php - use Symfony\Component\Validator\Constraints as Assert; - - class Transaction - { - /** - * @Assert\Luhn(message = "Please check your credit card number.") - */ - protected $cardNumber; - } + + + + + + + + + + + + .. code-block:: php - // src/Acme/SubscriptionBundle/Entity/Transaction.php + // src/Entity/Transaction.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\Luhn; class Transaction { - protected $cardNumber; + // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('luhn', new Luhn(array( - 'message' => 'Please check your credit card number', - ))); + $metadata->addPropertyConstraint('cardNumber', new Assert\Luhn( + message: 'Please check your credit card number', + )); } } -Available Options ------------------ +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc -message -~~~~~~~ +Options +------- -**type**: ``string`` **default**: ``Invalid card number`` +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``Invalid card number.`` The default message supplied when the value does not pass the Luhn check. -.. _`Luhn algorithm`: http://en.wikipedia.org/wiki/Luhn_algorithm \ No newline at end of file +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`Luhn algorithm`: https://en.wikipedia.org/wiki/Luhn_algorithm diff --git a/reference/constraints/MacAddress.rst b/reference/constraints/MacAddress.rst new file mode 100644 index 00000000000..9a282ddf118 --- /dev/null +++ b/reference/constraints/MacAddress.rst @@ -0,0 +1,139 @@ +MacAddress +========== + +.. versionadded:: 7.1 + + The ``MacAddress`` constraint was introduced in Symfony 7.1. + +This constraint ensures that the given value is a valid `MAC address`_ (internally it +uses the ``FILTER_VALIDATE_MAC`` option of the :phpfunction:`filter_var` PHP +function). + +========== ===================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\MacAddress` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\MacAddressValidator` +========== ===================================================================== + +Basic Usage +----------- + +To use the MacAddress validator, apply it to a property on an object that +can contain a MAC address: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Device.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Device + { + #[Assert\MacAddress] + protected string $mac; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Device: + properties: + mac: + - MacAddress: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Device.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Device + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('mac', new Assert\MacAddress()); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid MAC address.`` + +This is the message that will be shown if the value is not a valid MAC address. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +=============== ============================================================== + +.. include:: /reference/constraints/_normalizer-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _reference-constraint-mac-address-type: + +``type`` +~~~~~~~~ + +**type**: ``string`` **default**: ``all`` + +.. versionadded:: 7.1 + + The ``type`` option was introduced in Symfony 7.1. + +This option defines the kind of MAC addresses that are allowed. There are a lot +of different possible values based on your needs: + +================================ ========================================= +Parameter Allowed MAC addresses +================================ ========================================= +``all`` All +``all_no_broadcast`` All except broadcast +``broadcast`` Only broadcast +``local_all`` Only local +``local_multicast_no_broadcast`` Only local and multicast except broadcast +``local_multicast`` Only local and multicast +``local_no_broadcast`` Only local except broadcast +``local_unicast`` Only local and unicast +``multicast_all`` Only multicast +``multicast_no_broadcast`` Only multicast except broadcast +``unicast_all`` Only unicast +``universal_all`` Only universal +``universal_unicast`` Only universal and unicast +``universal_multicast`` Only universal and multicast +================================ ========================================= + +.. _`MAC address`: https://en.wikipedia.org/wiki/MAC_address diff --git a/reference/constraints/Negative.rst b/reference/constraints/Negative.rst new file mode 100644 index 00000000000..0d043ee8f6e --- /dev/null +++ b/reference/constraints/Negative.rst @@ -0,0 +1,98 @@ +Negative +======== + +Validates that a value is a negative number. Zero is neither positive nor +negative, so you must use :doc:`/reference/constraints/NegativeOrZero` if you +want to allow zero as value. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Negative` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\LessThanValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraint ensures that the ``withdraw`` of a bank account +``TransferItem`` is a negative number (lesser than zero): + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/TransferItem.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class TransferItem + { + #[Assert\Negative] + protected int $withdraw; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\TransferItem: + properties: + withdraw: + - Negative: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/TransferItem.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class TransferItem + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('withdraw', new Assert\Negative()); + } + } + +Available Options +----------------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be negative.`` + +The default message supplied when the value is not less than zero. + +You can use the following parameters in this message: + +============================= ================================================ +Parameter Description +============================= ================================================ +``{{ compared_value }}`` Always zero +``{{ compared_value_type }}`` The expected value type +``{{ value }}`` The current (invalid) value +============================= ================================================ + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/NegativeOrZero.rst b/reference/constraints/NegativeOrZero.rst new file mode 100644 index 00000000000..5f221950528 --- /dev/null +++ b/reference/constraints/NegativeOrZero.rst @@ -0,0 +1,97 @@ +NegativeOrZero +============== + +Validates that a value is a negative number or equal to zero. If you don't +want to allow zero as value, use :doc:`/reference/constraints/Negative` instead. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\NegativeOrZero` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\LessThanOrEqualValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraint ensures that the ``level`` of a ``UnderGroundGarage`` +is a negative number or equal to zero: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/TransferItem.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class UnderGroundGarage + { + #[Assert\NegativeOrZero] + protected int $level; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\UnderGroundGarage: + properties: + level: + - NegativeOrZero: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/UnderGroundGarage.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class UnderGroundGarage + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('level', new Assert\NegativeOrZero()); + } + } + +Available Options +----------------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be either negative or zero.`` + +The default message supplied when the value is not less than or equal to zero. + +You can use the following parameters in this message: + +============================= ================================================ +Parameter Description +============================= ================================================ +``{{ compared_value }}`` Always zero +``{{ compared_value_type }}`` The expected value type +``{{ value }}`` The current (invalid) value +============================= ================================================ + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/NoSuspiciousCharacters.rst b/reference/constraints/NoSuspiciousCharacters.rst new file mode 100644 index 00000000000..00e28cd6da1 --- /dev/null +++ b/reference/constraints/NoSuspiciousCharacters.rst @@ -0,0 +1,165 @@ +NoSuspiciousCharacters +====================== + +Validates that the given string does not contain characters used in spoofing +security attacks, such as invisible characters such as zero-width spaces or +characters that are visually similar. + +"symfony.com" and "ѕymfony.com" look similar, but their first letter is different +(in the second string, the "s" is actually a `cyrillic small letter dze`_). +This can make a user think they'll navigate to Symfony's website, whereas it +would be somewhere else. + +This is a kind of `spoofing attack`_ (called "IDN homograph attack"). It tries +to identify something as something else to exploit the resulting confusion. +This is why it is recommended to check user-submitted, public-facing identifiers +for suspicious characters in order to prevent such attacks. + +Because Unicode contains such a large number of characters and incorporates the +varied writing systems of the world, incorrect usage can expose programs or +systems to possible security attacks. + +That's why this constraint ensures strings or :phpclass:`Stringable`s do not +include any suspicious characters. As it leverages PHP's :phpclass:`Spoofchecker`, +the intl extension must be enabled to use it. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\NoSuspiciousCharacters` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\NoSuspiciousCharactersValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraint will use different detection mechanisms to ensure that +the username is not spoofed: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class User + { + #[Assert\NoSuspiciousCharacters] + private string $username; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\User: + properties: + username: + - NoSuspiciousCharacters: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class User + { + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addPropertyConstraint('username', new Assert\NoSuspiciousCharacters()); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +``checks`` +~~~~~~~~~~ + +**type**: ``integer`` **default**: all + +This option is a bitmask of the checks you want to perform on the string: + +* ``NoSuspiciousCharacters::CHECK_INVISIBLE`` checks for the presence of invisible + characters such as zero-width spaces, or character sequences that are likely + not to display, such as multiple occurrences of the same non-spacing mark. +* ``NoSuspiciousCharacters::CHECK_MIXED_NUMBERS`` (usable with ICU 58 or higher) + checks for numbers from different numbering systems. +* ``NoSuspiciousCharacters::CHECK_HIDDEN_OVERLAY`` (usable with ICU 62 or higher) + checks for combining characters hidden in their preceding one. + +You can also configure additional requirements using :ref:`locales ` and +:ref:`restrictionLevel `. + +``locales`` +~~~~~~~~~~~ + +**type**: ``array`` **default**: :ref:`framework.enabled_locales ` + +Restrict the string's characters to those normally used with the associated languages. + +For example, the character "π" would be considered suspicious if you restricted the +locale to "English", because the Greek script is not associated with it. + +Passing an empty array, or configuring :ref:`restrictionLevel ` to +``NoSuspiciousCharacters::RESTRICTION_LEVEL_NONE`` will disable this requirement. + +``restrictionLevel`` +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``integer`` **default**: ``NoSuspiciousCharacters::RESTRICTION_LEVEL_MODERATE`` on ICU >= 58, otherwise ``NoSuspiciousCharacters::RESTRICTION_LEVEL_SINGLE_SCRIPT`` + +Configures the set of acceptable characters for the validated string through a +specified "level": + +* ``NoSuspiciousCharacters::RESTRICTION_LEVEL_MINIMAL`` requires the string's + characters to match :ref:`the configured locales `'. +* ``NoSuspiciousCharacters::RESTRICTION_LEVEL_MODERATE`` also requires the string + to be `covered`_ by Latin and any one other `Recommended`_ or `Limited Use`_ + script, except Cyrillic, Greek, and Cherokee. +* ``NoSuspiciousCharacters::RESTRICTION_LEVEL_HIGH`` (usable with ICU 58 or higher) + also requires the string to be `covered`_ by any of the following sets of scripts: + + * Latin + Han + Bopomofo (or equivalently: Latn + Hanb) + * Latin + Han + Hiragana + Katakana (or equivalently: Latn + Jpan) + * Latin + Han + Hangul (or equivalently: Latn + Kore) + +* ``NoSuspiciousCharacters::RESTRICTION_LEVEL_SINGLE_SCRIPT`` also requires the + string to be `single-script`_. +* ``NoSuspiciousCharacters::RESTRICTION_LEVEL_ASCII`` (usable with ICU 58 or higher) + also requires the string's characters to be in the ASCII range. + +You can accept all characters by setting this option to +``NoSuspiciousCharacters::RESTRICTION_LEVEL_NONE``. + +.. include:: /reference/constraints/_groups-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`cyrillic small letter dze`: https://graphemica.com/%D1%95 +.. _`spoofing attack`: https://en.wikipedia.org/wiki/Spoofing_attack +.. _`single-script`: https://unicode.org/reports/tr39/#def-single-script +.. _`covered`: https://unicode.org/reports/tr39/#def-cover +.. _`Recommended`: https://www.unicode.org/reports/tr31/#Table_Recommended_Scripts +.. _`Limited Use`: https://www.unicode.org/reports/tr31/#Table_Limited_Use_Scripts diff --git a/reference/constraints/NotBlank.rst b/reference/constraints/NotBlank.rst index bcb6628f0b0..388206e34bd 100644 --- a/reference/constraints/NotBlank.rst +++ b/reference/constraints/NotBlank.rst @@ -1,71 +1,74 @@ NotBlank ======== -Validates that a value is not blank, defined as not equal to a blank string -and also not equal to ``null``. To force that a value is simply not equal to -``null``, see the :doc:`/reference/constraints/NotNull` constraint. - -+----------------+------------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+------------------------------------------------------------------------+ -| Options | - `message`_ | -+----------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\NotBlank` | -+----------------+------------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\NotBlankValidator` | -+----------------+------------------------------------------------------------------------+ +Validates that a value is not blank - meaning not equal to a blank string, +a blank array, ``false`` or ``null`` (null behavior is configurable). To check +that a value is not equal to ``null``, see the +:doc:`/reference/constraints/NotNull` constraint. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\NotBlank` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\NotBlankValidator` +========== =================================================================== Basic Usage ----------- -If you wanted to ensure that the ``firstName`` property of an ``Author`` class -were not blank, you could do the following: +If you wanted to ensure that the ``firstName`` property of an ``Author`` +class were not blank, you could do the following: .. configuration-block:: - .. code-block:: yaml - - # src/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - firstName: - - NotBlank: ~ - - .. code-block:: php-annotations + .. code-block:: php-attributes - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\NotBlank() - */ - protected $firstName; + #[Assert\NotBlank] + protected string $firstName; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + firstName: + - NotBlank: ~ + .. code-block:: xml - - - - - - + + + + + + + + + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', new Assert\NotBlank()); } @@ -74,9 +77,32 @@ were not blank, you could do the following: Options ------- -message -~~~~~~~ +``allowNull`` +~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +If set to ``true``, ``null`` values are considered valid and won't trigger a +constraint violation. -**type**: ``string`` **default**: ``This value should not be blank`` +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should not be blank.`` This is the message that will be shown if the value is blank. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_normalizer-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/NotCompromisedPassword.rst b/reference/constraints/NotCompromisedPassword.rst new file mode 100644 index 00000000000..6641f9d8cb2 --- /dev/null +++ b/reference/constraints/NotCompromisedPassword.rst @@ -0,0 +1,128 @@ +NotCompromisedPassword +====================== + +Validates that the given password has not been compromised by checking that it is +not included in any of the public data breaches tracked by `haveibeenpwned.com`_. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\NotCompromisedPassword` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\NotCompromisedPasswordValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraint ensures that the ``rawPassword`` property of the +``User`` class doesn't store a compromised password: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class User + { + #[Assert\NotCompromisedPassword] + protected string $rawPassword; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\User: + properties: + rawPassword: + - NotCompromisedPassword + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class User + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('rawPassword', new Assert\NotCompromisedPassword()); + } + } + +In order to make the password validation, this constraint doesn't send the raw +password value to the ``haveibeenpwned.com`` API. Instead, it follows a secure +process known as `k-anonymity password validation`_. + +In practice, the raw password is hashed using SHA-1 and only the first bytes of +the hash are sent. Then, the ``haveibeenpwned.com`` API compares those bytes +with the SHA-1 hashes of all leaked passwords and returns the list of hashes +that start with those same bytes. That's how the constraint can check if the +password has been compromised without fully disclosing it. + +For example, if the password is ``test``, the entire SHA-1 hash is +``a94a8fe5ccb19ba61c4c0873d391e987982fbbd3`` but the validator only sends +``a94a8`` to the ``haveibeenpwned.com`` API. + +.. seealso:: + + When using this constraint inside a Symfony application, define the + :ref:`not_compromised_password ` + option to avoid making HTTP requests in the ``dev`` and ``test`` environments. + +Available Options +----------------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This password has been leaked in a data breach, it must not be used. Please use another password.`` + +The default message supplied when the password has been compromised. + +.. include:: /reference/constraints/_payload-option.rst.inc + +``skipOnError`` +~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +When the HTTP request made to the ``haveibeenpwned.com`` API fails for any +reason, an exception is thrown (no validation error is displayed). Set this +option to ``true`` to not throw the exception and consider the password valid. + +``threshold`` +~~~~~~~~~~~~~ + +**type**: ``integer`` **default**: ``1`` + +This value defines the number of times a password should have been leaked +publicly to consider it compromised. Think carefully before setting this option +to a higher value because it could decrease the security of your application. + +.. _`haveibeenpwned.com`: https://haveibeenpwned.com/ +.. _`k-anonymity password validation`: https://blog.cloudflare.com/validating-leaked-passwords-with-k-anonymity/ diff --git a/reference/constraints/NotEqualTo.rst b/reference/constraints/NotEqualTo.rst new file mode 100644 index 00000000000..dd3f633b4a1 --- /dev/null +++ b/reference/constraints/NotEqualTo.rst @@ -0,0 +1,128 @@ +NotEqualTo +========== + +Validates that a value is **not** equal to another value, defined in the +options. To force that a value is equal, see +:doc:`/reference/constraints/EqualTo`. + +.. warning:: + + This constraint compares using ``!=``, so ``3`` and ``"3"`` are considered + equal. Use :doc:`/reference/constraints/NotIdenticalTo` to compare with + ``!==``. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\NotEqualTo` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\NotEqualToValidator` +========== =================================================================== + +Basic Usage +----------- + +If you want to ensure that the ``firstName`` of a ``Person`` is not equal to +``Mary`` and that the ``age`` of a ``Person`` class is not ``15``, you could do +the following: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\NotEqualTo('Mary')] + protected string $firstName; + + #[Assert\NotEqualTo( + value: 15, + )] + protected int $age; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + firstName: + - NotEqualTo: Mary + age: + - NotEqualTo: + value: 15 + + .. code-block:: xml + + + + + + + + + Mary + + + + + + + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('firstName', new Assert\NotEqualTo('Mary')); + + $metadata->addPropertyConstraint('age', new Assert\NotEqualTo( + value: 15, + )); + } + } + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should not be equal to {{ compared_value }}.`` + +This is the message that will be shown if the value is equal. + +You can use the following parameters in this message: + +============================= ================================================ +Parameter Description +============================= ================================================ +``{{ compared_value }}`` The expected value +``{{ compared_value_type }}`` The expected value type +``{{ value }}`` The current (invalid) value +============================= ================================================ + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. include:: /reference/constraints/_comparison-propertypath-option.rst.inc + +.. include:: /reference/constraints/_comparison-value-option.rst.inc diff --git a/reference/constraints/NotIdenticalTo.rst b/reference/constraints/NotIdenticalTo.rst new file mode 100644 index 00000000000..b2c20027292 --- /dev/null +++ b/reference/constraints/NotIdenticalTo.rst @@ -0,0 +1,129 @@ +NotIdenticalTo +============== + +Validates that a value is **not** identical to another value, defined in +the options. To force that a value is identical, see +:doc:`/reference/constraints/IdenticalTo`. + +.. warning:: + + This constraint compares using ``!==``, so ``3`` and ``"3"`` are + considered not equal. Use :doc:`/reference/constraints/NotEqualTo` to + compare with ``!=``. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\NotIdenticalTo` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\NotIdenticalToValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraints ensure that: + +* ``firstName`` of ``Person`` is not equal to ``Mary`` *or* not of the same type +* ``age`` of ``Person`` class is not equal to ``15`` *or* not of the same type + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\NotIdenticalTo('Mary')] + protected string $firstName; + + #[Assert\NotIdenticalTo( + value: 15, + )] + protected int $age; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + firstName: + - NotIdenticalTo: Mary + age: + - NotIdenticalTo: + value: 15 + + .. code-block:: xml + + + + + + + + + Mary + + + + + + + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('age', new Assert\NotIdenticalTo('Mary')); + + $metadata->addPropertyConstraint('age', new Assert\NotIdenticalTo( + value: 15, + )); + } + } + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should not be identical to {{ compared_value_type }} {{ compared_value }}.`` + +This is the message that will be shown if the value is identical. + +You can use the following parameters in this message: + +============================= ================================================ +Parameter Description +============================= ================================================ +``{{ compared_value }}`` The expected value +``{{ compared_value_type }}`` The expected value type +``{{ value }}`` The current (invalid) value +============================= ================================================ + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. include:: /reference/constraints/_comparison-propertypath-option.rst.inc + +.. include:: /reference/constraints/_comparison-value-option.rst.inc diff --git a/reference/constraints/NotNull.rst b/reference/constraints/NotNull.rst index 1bd63d7cc55..f1a27bd6560 100644 --- a/reference/constraints/NotNull.rst +++ b/reference/constraints/NotNull.rst @@ -2,70 +2,72 @@ NotNull ======= Validates that a value is not strictly equal to ``null``. To ensure that -a value is simply not blank (not a blank string), see the :doc:`/reference/constraints/NotBlank` +a value is not blank (not a blank string), see the :doc:`/reference/constraints/NotBlank` constraint. -+----------------+-----------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+-----------------------------------------------------------------------+ -| Options | - `message`_ | -+----------------+-----------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\NotNull` | -+----------------+-----------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\NotNullValidator` | -+----------------+-----------------------------------------------------------------------+ +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\NotNull` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\NotNullValidator` +========== =================================================================== Basic Usage ----------- -If you wanted to ensure that the ``firstName`` property of an ``Author`` class -were not strictly equal to ``null``, you would: +If you wanted to ensure that the ``firstName`` property of an ``Author`` +class were not strictly equal to ``null``, you would: .. configuration-block:: - .. code-block:: yaml - - # src/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - firstName: - - NotNull: ~ + .. code-block:: php-attributes - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\NotNull() - */ - protected $firstName; + #[Assert\NotNull] + protected string $firstName; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + firstName: + - NotNull: ~ + .. code-block:: xml - - - - - - + + + + + + + + + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', new Assert\NotNull()); } @@ -74,9 +76,22 @@ were not strictly equal to ``null``, you would: Options ------- -message -~~~~~~~ +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ -**type**: ``string`` **default**: ``This value should not be null`` +**type**: ``string`` **default**: ``This value should not be null.`` This is the message that will be shown if the value is ``null``. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Null.rst b/reference/constraints/Null.rst deleted file mode 100644 index 0d34cb6311d..00000000000 --- a/reference/constraints/Null.rst +++ /dev/null @@ -1,82 +0,0 @@ -Null -==== - -Validates that a value is exactly equal to ``null``. To force that a property -is simply blank (blank string or ``null``), see the :doc:`/reference/constraints/Blank` -constraint. To ensure that a property is not null, see :doc:`/reference/constraints/NotNull`. - -+----------------+-----------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+-----------------------------------------------------------------------+ -| Options | - `message`_ | -+----------------+-----------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Null` | -+----------------+-----------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\NullValidator` | -+----------------+-----------------------------------------------------------------------+ - -Basic Usage ------------ - -If, for some reason, you wanted to ensure that the ``firstName`` property -of an ``Author`` class exactly equal to ``null``, you could do the following: - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - firstName: - - 'Null': ~ - - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\Null() - */ - protected $firstName; - } - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('firstName', Assert\Null()); - } - } - -Options -------- - -message -~~~~~~~ - -**type**: ``string`` **default**: ``This value should be null`` - -This is the message that will be shown if the value is not ``null``. diff --git a/reference/constraints/PasswordStrength.rst b/reference/constraints/PasswordStrength.rst new file mode 100644 index 00000000000..0b242cacf08 --- /dev/null +++ b/reference/constraints/PasswordStrength.rst @@ -0,0 +1,214 @@ +PasswordStrength +================ + +Validates that the given password has reached the minimum strength required by +the constraint. The strength of the password is not evaluated with a set of +predefined rules (include a number, use lowercase and uppercase characters, +etc.) but by measuring the entropy of the password based on its length and the +number of unique characters used. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\PasswordStrength` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\PasswordStrengthValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraint ensures that the ``rawPassword`` property of the +``User`` class reaches the minimum strength required by the constraint. +By default, the minimum required score is ``2``. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class User + { + #[Assert\PasswordStrength] + protected $rawPassword; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\User: + properties: + rawPassword: + - PasswordStrength + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class User + { + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addPropertyConstraint('rawPassword', new Assert\PasswordStrength()); + } + } + +Available Options +----------------- + +``minScore`` +~~~~~~~~~~~~ + +**type**: ``integer`` **default**: ``PasswordStrength::STRENGTH_MEDIUM`` (``2``) + +The minimum required strength of the password. Available constants are: + +* ``PasswordStrength::STRENGTH_WEAK`` = ``1`` +* ``PasswordStrength::STRENGTH_MEDIUM`` = ``2`` +* ``PasswordStrength::STRENGTH_STRONG`` = ``3`` +* ``PasswordStrength::STRENGTH_VERY_STRONG`` = ``4`` + +``PasswordStrength::STRENGTH_VERY_WEAK`` is available but only used internally +or by a custom password strength estimator. + +.. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class User + { + #[Assert\PasswordStrength( + minScore: PasswordStrength::STRENGTH_VERY_STRONG, // Very strong password required + )] + protected $rawPassword; + } + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The password strength is too low. Please use a stronger password.`` + +The default message supplied when the password does not reach the minimum required score. + +.. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class User + { + #[Assert\PasswordStrength( + message: 'Your password is too easy to guess. Company\'s security policy requires to use a stronger password.' + )] + protected $rawPassword; + } + +Customizing the Password Strength Estimation +-------------------------------------------- + +.. versionadded:: 7.2 + + The feature to customize the password strength estimation was introduced in Symfony 7.2. + +By default, this constraint calculates the strength of a password based on its +length and the number of unique characters used. You can get the calculated +password strength (e.g. to display it in the user interface) using the following +static function:: + + use Symfony\Component\Validator\Constraints\PasswordStrengthValidator; + + $passwordEstimatedStrength = PasswordStrengthValidator::estimateStrength($password); + +If you need to override the default password strength estimation algorithm, you +can pass a ``Closure`` to the :class:`Symfony\\Component\\Validator\\Constraints\\PasswordStrengthValidator` +constructor (e.g. using the :doc:`service closures `). + +First, create a custom password strength estimation algorithm within a dedicated +callable class:: + + namespace App\Validator; + + class CustomPasswordStrengthEstimator + { + /** + * @return PasswordStrength::STRENGTH_* + */ + public function __invoke(string $password): int + { + // Your custom password strength estimation algorithm + } + } + +Then, configure the :class:`Symfony\\Component\\Validator\\Constraints\\PasswordStrengthValidator` +service to use your own estimator: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + custom_password_strength_estimator: + class: App\Validator\CustomPasswordStrengthEstimator + + Symfony\Component\Validator\Constraints\PasswordStrengthValidator: + arguments: [!closure '@custom_password_strength_estimator'] + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\Validator\Constraints\PasswordStrengthValidator; + + return function (ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set('custom_password_strength_estimator', CustomPasswordStrengthEstimator::class); + + $services->set(PasswordStrengthValidator::class) + ->args([closure('custom_password_strength_estimator')]); + }; diff --git a/reference/constraints/Positive.rst b/reference/constraints/Positive.rst new file mode 100644 index 00000000000..b43fdde67d8 --- /dev/null +++ b/reference/constraints/Positive.rst @@ -0,0 +1,98 @@ +Positive +======== + +Validates that a value is a positive number. Zero is neither positive nor +negative, so you must use :doc:`/reference/constraints/PositiveOrZero` if you +want to allow zero as value. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Positive` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThanValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraint ensures that the ``income`` of an ``Employee`` is a +positive number (greater than zero): + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Employee.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Employee + { + #[Assert\Positive] + protected int $income; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Employee: + properties: + income: + - Positive: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Employee.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Employee + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('income', new Assert\Positive()); + } + } + +Available Options +----------------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be positive.`` + +The default message supplied when the value is not greater than zero. + +You can use the following parameters in this message: + +============================= ================================================ +Parameter Description +============================= ================================================ +``{{ compared_value }}`` Always zero +``{{ compared_value_type }}`` The expected value type +``{{ value }}`` The current (invalid) value +============================= ================================================ + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/PositiveOrZero.rst b/reference/constraints/PositiveOrZero.rst new file mode 100644 index 00000000000..4aa8420993c --- /dev/null +++ b/reference/constraints/PositiveOrZero.rst @@ -0,0 +1,97 @@ +PositiveOrZero +============== + +Validates that a value is a positive number or equal to zero. If you don't +want to allow zero as value, use :doc:`/reference/constraints/Positive` instead. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\PositiveOrZero` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThanOrEqualValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraint ensures that the number of ``siblings`` of a ``Person`` +is positive or zero: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\PositiveOrZero] + protected int $siblings; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + siblings: + - PositiveOrZero: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('siblings', new Assert\PositiveOrZero()); + } + } + +Available Options +----------------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be either positive or zero.`` + +The default message supplied when the value is not greater than or equal to zero. + +You can use the following parameters in this message: + +============================= ================================================ +Parameter Description +============================= ================================================ +``{{ compared_value }}`` Always zero +``{{ compared_value_type }}`` The expected value type +``{{ value }}`` The current (invalid) value +============================= ================================================ + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Range.rst b/reference/constraints/Range.rst index 41c820b61e3..46a9e3799b3 100644 --- a/reference/constraints/Range.rst +++ b/reference/constraints/Range.rst @@ -1,141 +1,450 @@ Range ===== -Validates that a given number is *between* some minimum and maximum number. - -.. versionadded:: 2.1 - The Range constraint was added in Symfony 2.1. - -+----------------+---------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+---------------------------------------------------------------------+ -| Options | - `min`_ | -| | - `max`_ | -| | - `minMessage`_ | -| | - `maxMessage`_ | -| | - `invalidMessage`_ | -+----------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Range` | -+----------------+---------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\RangeValidator` | -+----------------+---------------------------------------------------------------------+ +Validates that a given number or ``DateTime`` object is *between* some minimum and maximum. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Range` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\RangeValidator` +========== =================================================================== Basic Usage ----------- -To verify that the "height" field of a class is between "120" and "180", you might add -the following: +To verify that the ``height`` field of a class is between ``120`` and ``180``, +you might add the following: .. configuration-block:: + .. code-block:: php-attributes + + // src/Entity/Participant.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Participant + { + #[Assert\Range( + min: 120, + max: 180, + notInRangeMessage: 'You must be between {{ min }}cm and {{ max }}cm tall to enter', + )] + protected int $height; + } + .. code-block:: yaml - # src/Acme/EventBundle/Resources/config/validation.yml - Acme\EventBundle\Entity\Participant: + # config/validator/validation.yaml + App\Entity\Participant: properties: height: - Range: min: 120 max: 180 - minMessage: You must be at least 120cm tall to enter - maxMessage: You cannot be taller than 180cm to enter + notInRangeMessage: You must be between {{ min }}cm and {{ max }}cm tall to enter - .. code-block:: php-annotations + .. code-block:: xml + + + + + + + + + + + + + + + - // src/Acme/EventBundle/Entity/Participant.php - namespace Acme\EventBundle\Entity; + .. code-block:: php + + // src/Entity/Participant.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Participant { - /** - * @Assert\Range( - * min = "120", - * max = "180", - * minMessage = "You must be at least 120cm tall to enter", - * maxMessage = "You cannot be taller than 180cm to enter" - * ) - */ - protected $height; + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('height', new Assert\Range( + min: 120, + max: 180, + notInRangeMessage: 'You must be between {{ min }}cm and {{ max }}cm tall to enter', + )); + } } +Date Ranges +----------- + +This constraint can be used to compare ``DateTime`` objects against date ranges. +The minimum and maximum date of the range should be given as any date string +`accepted by the DateTime constructor`_. For example, you could check that a +date must lie within the current year like this: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Event.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Event + { + #[Assert\Range( + min: 'first day of January', + max: 'first day of January next year', + )] + protected \DateTimeInterface $startDate; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Event: + properties: + startDate: + - Range: + min: first day of January + max: first day of January next year + .. code-block:: xml - - - - - - - - - - - + + + + + + + + + + + + + .. code-block:: php - // src/Acme/EventBundle/Entity/Participant.php - namespace Acme\EventBundle\Entity; + // src/Entity/Event.php + namespace App\Entity; + use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Event + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('startDate', new Assert\Range( + min: 'first day of January', + max: 'first day of January next year', + )); + } + } + +Be aware that PHP will use the server's configured timezone to interpret these +dates. If you want to fix the timezone, append it to the date string: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Event.php + namespace App\Entity; + use Symfony\Component\Validator\Constraints as Assert; - class Participant + class Event + { + #[Assert\Range( + min: 'first day of January UTC', + max: 'first day of January next year UTC', + )] + protected \DateTimeInterface $startDate; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Event: + properties: + startDate: + - Range: + min: first day of January UTC + max: first day of January next year UTC + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Event + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('startDate', new Assert\Range( + min: 'first day of January UTC', + max: 'first day of January next year UTC', + )); + } + } + +The ``DateTime`` class also accepts relative dates or times. For example, you +can check that a delivery date starts within the next five hours like this: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Order { - public static function loadValidatorMetadata(ClassMetadata $metadata) + #[Assert\Range( + min: 'now', + max: '+5 hours', + )] + protected \DateTimeInterface $deliveryDate; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Order: + properties: + deliveryDate: + - Range: + min: now + max: +5 hours + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Order.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Order + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('height', new Assert\Range(array( - 'min' => 120, - 'max' => 180, - 'minMessage' => 'You must be at least 120cm tall to enter', - 'maxMessage' => 'You cannot be taller than 180cm to enter', - ))); + $metadata->addPropertyConstraint('deliveryDate', new Assert\Range( + min: 'now', + max: '+5 hours', + )); } } Options ------- -min -~~~ +.. include:: /reference/constraints/_groups-option.rst.inc -**type**: ``integer`` [:ref:`default option`] +``invalidDateTimeMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ -This required option is the "min" value. Validation will fail if the given -value is **less** than this min value. +**type**: ``string`` **default**: ``This value should be a valid number.`` + +The message displayed when the ``min`` and ``max`` values are PHP datetimes but +the given value is not. -max -~~~ +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +=============== ============================================================== + +``invalidMessage`` +~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be a valid number.`` -**type**: ``integer`` [:ref:`default option`] +The message displayed when the ``min`` and ``max`` values are numeric (per +the :phpfunction:`is_numeric` PHP function) but the given value is not. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +``max`` +~~~~~~~ + +**type**: ``number`` or ``string`` (date format) This required option is the "max" value. Validation will fail if the given value is **greater** than this max value. -minMessage -~~~~~~~~~~ +``maxMessage`` +~~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``This value should be {{ limit }} or more.`` +**type**: ``string`` **default**: ``This value should be {{ limit }} or less.`` -The message that will be shown if the underlying value is less than the `min`_ -option. +The message that will be shown if the underlying value is more than the +`max`_ option, and no `min`_ option has been defined (if both are defined, use +`notInRangeMessage`_). -maxMessage -~~~~~~~~~~ +You can use the following parameters in this message: -**type**: ``string`` **default**: ``This value should be {{ limit }} or less.`` +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ limit }}`` The upper limit +``{{ value }}`` The current (invalid) value +=============== ============================================================== + +``maxPropertyPath`` +~~~~~~~~~~~~~~~~~~~ -The message that will be shown if the underlying value is more than the `max`_ -option. +**type**: ``string`` + +It defines the object property whose value is used as ``max`` option. + +For example, if you want to compare the ``$submittedDate`` property of some object +with regard to the ``$deadline`` property of the same object, use +``maxPropertyPath="deadline"`` in the range constraint of ``$submittedDate``. + +.. tip:: + + When using this option, its value is available in error messages as the + ``{{ max_limit_path }}`` placeholder. Although it's not intended to + include it in the error messages displayed to end users, it's useful when + using APIs for doing any mapping logic on client-side. + +``min`` +~~~~~~~ + +**type**: ``number`` or ``string`` (date format) + +This required option is the "min" value. Validation will fail if the given +value is **less** than this min value. -invalidMessage +``minMessage`` ~~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``This value should be a valid number.`` +**type**: ``string`` **default**: ``This value should be {{ limit }} or more.`` + +The message that will be shown if the underlying value is less than the +`min`_ option, and no `max`_ option has been defined (if both are defined, use +`notInRangeMessage`_). + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ limit }}`` The lower limit +``{{ value }}`` The current (invalid) value +=============== ============================================================== + +``minPropertyPath`` +~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` + +It defines the object property whose value is used as ``min`` option. + +For example, if you want to compare the ``$endDate`` property of some object +with regard to the ``$startDate`` property of the same object, use +``minPropertyPath="startDate"`` in the range constraint of ``$endDate``. + +.. tip:: + + When using this option, its value is available in error messages as the + ``{{ min_limit_path }}`` placeholder. Although it's not intended to + include it in the error messages displayed to end users, it's useful when + using APIs for doing any mapping logic on client-side. + +``notInRangeMessage`` +~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value should be between {{ min }} and {{ max }}.`` + +The message that will be shown if the underlying value is less than the +`min`_ option or greater than the `max`_ option. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ max }}`` The upper limit +``{{ min }}`` The lower limit +``{{ value }}`` The current (invalid) value +=============== ============================================================== -The message that will be shown if the underlying value is not a number (per -the `is_numeric`_ PHP function). +.. include:: /reference/constraints/_payload-option.rst.inc -.. _`is_numeric`: http://www.php.net/manual/en/function.is-numeric.php +.. _`accepted by the DateTime constructor`: https://www.php.net/manual/en/datetime.formats.php diff --git a/reference/constraints/Regex.rst b/reference/constraints/Regex.rst index 426bd72f196..e3b4d4711b2 100644 --- a/reference/constraints/Regex.rst +++ b/reference/constraints/Regex.rst @@ -3,177 +3,286 @@ Regex Validates that a value matches a regular expression. -+----------------+-----------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+-----------------------------------------------------------------------+ -| Options | - `pattern`_ | -| | - `match`_ | -| | - `message`_ | -+----------------+-----------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Regex` | -+----------------+-----------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\RegexValidator` | -+----------------+-----------------------------------------------------------------------+ +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Regex` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\RegexValidator` +========== =================================================================== Basic Usage ----------- -Suppose you have a ``description`` field and you want to verify that it begins -with a valid word character. The regular expression to test for this would -be ``/^\w+/``, indicating that you're looking for at least one or more word -characters at the beginning of your string: +Suppose you have a ``description`` field and you want to verify that it +begins with a valid word character. The regular expression to test for this +would be ``/^\w+/``, indicating that you're looking for at least one or +more word characters at the beginning of your string: .. configuration-block:: - .. code-block:: yaml + .. code-block:: php-attributes - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - description: - - Regex: "/^\w+/" + // src/Entity/Author.php + namespace App\Entity; - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\Regex("/^\w+/") - */ - protected $description; + #[Assert\Regex('/^\w+/')] + protected string $description; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + description: + - Regex: '/^\w+/' + .. code-block:: xml - - - - - - - - + + + + + + + + + + + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - - use Symfony\Component\Validator\Mapping\ClassMetadata; + // src/Entity/Author.php + namespace App\Entity; + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('description', new Assert\Regex(array( - 'pattern' => '/^\w+/', - ))); + $metadata->addPropertyConstraint('description', new Assert\Regex( + pattern: '/^\w+/', + )); } } -Alternatively, you can set the `match`_ option to ``false`` in order to assert -that a given string does *not* match. In the following example, you'll assert -that the ``firstName`` field does not contain any numbers and give it a custom -message: +Alternatively, you can set the `match`_ option to ``false`` in order to +assert that a given string does *not* match. In the following example, you'll +assert that the ``firstName`` field does not contain any numbers and give +it a custom message: .. configuration-block:: + .. code-block:: php-attributes + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + #[Assert\Regex( + pattern: '/\d/', + match: false, + message: 'Your name cannot contain a number', + )] + protected string $firstName; + } + .. code-block:: yaml - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: + # config/validator/validation.yaml + App\Entity\Author: properties: firstName: - Regex: - pattern: "/\d/" + pattern: '/\d/' match: false message: Your name cannot contain a number - .. code-block:: php-annotations + .. code-block:: xml + + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Author.php + namespace App\Entity; - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - /** - * @Assert\Regex( - * pattern="/\d/", - * match=false, - * message="Your name cannot contain a number" - * ) - */ - protected $firstName; + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('firstName', new Assert\Regex( + pattern: '/\d/', + match: false, + message: 'Your name cannot contain a number', + )); + } } +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``htmlPattern`` +~~~~~~~~~~~~~~~ + +**type**: ``string|null`` **default**: ``null`` + +This option specifies the pattern to use in the HTML5 ``pattern`` attribute. +You usually don't need to specify this option because by default, the constraint +will convert the pattern given in the `pattern`_ option into an HTML5 compatible +pattern. Notably, the delimiters are removed and the anchors are implicit (e.g. +``/^[a-z]+$/`` becomes ``[a-z]+``, and ``/[a-z]+/`` becomes ``.*[a-z]+.*``). + +However, there are some other incompatibilities between both patterns which +cannot be fixed by the constraint. For instance, the HTML5 ``pattern`` attribute +does not support flags. If you have a pattern like ``/^[a-z]+$/i``, you +need to specify the HTML5 compatible pattern in the ``htmlPattern`` option: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + #[Assert\Regex( + pattern: '/^[a-z]+$/i', + htmlPattern: '^[a-zA-Z]+$' + )] + protected string $name; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + name: + - Regex: + pattern: '/^[a-z]+$/i' + htmlPattern: '[a-zA-Z]+' + .. code-block:: xml - - - - - - - - - - + + + + + + + + + + + + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('firstName', new Assert\Regex(array( - 'pattern' => '/\d/', - 'match' => false, - 'message' => 'Your name cannot contain a number', - ))); + $metadata->addPropertyConstraint('name', new Assert\Regex( + pattern: '/^[a-z]+$/i', + htmlPattern: '[a-zA-Z]+', + )); } } -Options -------- - -pattern -~~~~~~~ - -**type**: ``string`` [:ref:`default option`] - -This required option is the regular expression pattern that the input will -be matched against. By default, this validator will fail if the input string -does *not* match this regular expression (via the :phpfunction:`preg_match` PHP function). -However, if `match`_ is set to false, then validation will fail if the input -string *does* match this pattern. +Setting ``htmlPattern`` to the empty string will disable client side validation. -match -~~~~~ +``match`` +~~~~~~~~~ -**type**: ``Boolean`` default: ``true`` +**type**: ``boolean`` default: ``true`` If ``true`` (or not set), this validator will pass if the given string matches the given `pattern`_ regular expression. However, when this option is set -to ``false``, the opposite will occur: validation will pass only if the given -string does **not** match the `pattern`_ regular expression. +to ``false``, the opposite will occur: validation will pass only if the +given string does **not** match the `pattern`_ regular expression. -message -~~~~~~~ +``message`` +~~~~~~~~~~~ -**type**: ``string`` **default**: ``This value is not valid`` +**type**: ``string`` **default**: ``This value is not valid.`` This is the message that will be shown if this validator fails. + +You can use the following parameters in this message: + +================= ============================================================== +Parameter Description +================= ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +``{{ pattern }}`` The expected matching pattern +================= ============================================================== + +``pattern`` +~~~~~~~~~~~ + +**type**: ``string`` + +This required option is the regular expression pattern that the input will +be matched against. By default, this validator will fail if the input string +does *not* match this regular expression (via the :phpfunction:`preg_match` +PHP function). However, if `match`_ is set to false, then validation will +fail if the input string *does* match this pattern. + +.. include:: /reference/constraints/_normalizer-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Sequentially.rst b/reference/constraints/Sequentially.rst new file mode 100644 index 00000000000..078be338cdf --- /dev/null +++ b/reference/constraints/Sequentially.rst @@ -0,0 +1,133 @@ +Sequentially +============ + +This constraint allows you to apply a set of rules that should be validated +step-by-step, allowing to interrupt the validation once the first violation is raised. + +As an alternative in situations ``Sequentially`` cannot solve, you may consider +using :doc:`GroupSequence ` which allows more control. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Sequentially` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\SequentiallyValidator` +========== =================================================================== + +Basic Usage +----------- + +Suppose that you have a ``Place`` object with an ``$address`` property which +must match the following requirements: + +* it's a non-blank string +* of at least 10 chars long +* with a specific format +* and geolocalizable using an external service + +In such situations, you may encounter three issues: + +* the ``Length`` or ``Regex`` constraints may fail hard with a :class:`Symfony\\Component\\Validator\\Exception\\UnexpectedValueException` + exception if the actual value is not a string, as enforced by ``Type``. +* you may end with multiple error messages for the same property. +* you may perform a useless and heavy external call to geolocalize the address, + while the format isn't valid. + +You can validate each of these constraints sequentially to solve these issues: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Localization/Place.php + namespace App\Localization; + + use App\Validator\Constraints as AcmeAssert; + use Symfony\Component\Validator\Constraints as Assert; + + class Place + { + #[Assert\Sequentially([ + new Assert\NotNull, + new Assert\Type('string'), + new Assert\Length(min: 10), + new Assert\Regex(Place::ADDRESS_REGEX), + new AcmeAssert\Geolocalizable, + ])] + public string $address; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Localization\Place: + properties: + address: + - Sequentially: + - NotNull: ~ + - Type: string + - Length: { min: 10 } + - Regex: !php/const App\Localization\Place::ADDRESS_REGEX + - App\Validator\Constraints\Geolocalizable: ~ + + .. code-block:: xml + + + + + + + + + + string + + + + + + + + + + + + + .. code-block:: php + + // src/Localization/Place.php + namespace App\Localization; + + use App\Validator\Constraints as AcmeAssert; + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Place + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('address', new Assert\Sequentially([ + new Assert\NotNull(), + new Assert\Type('string'), + new Assert\Length(min: 10), + new Assert\Regex(self::ADDRESS_REGEX), + new AcmeAssert\Geolocalizable(), + ])); + } + } + +Options +------- + +``constraints`` +~~~~~~~~~~~~~~~ + +**type**: ``array`` + +This required option is the array of validation constraints that you want +to apply sequentially. + +.. include:: /reference/constraints/_groups-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Slug.rst b/reference/constraints/Slug.rst new file mode 100644 index 00000000000..2eb82cd9c10 --- /dev/null +++ b/reference/constraints/Slug.rst @@ -0,0 +1,119 @@ +Slug +==== + +.. versionadded:: 7.3 + + The ``Slug`` constraint was introduced in Symfony 7.3. + +Validates that a value is a slug. By default, a slug is a string that matches +the following regular expression: ``/^[a-z0-9]+(?:-[a-z0-9]+)*$/``. + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Slug` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\SlugValidator` +========== =================================================================== + +Basic Usage +----------- + +The ``Slug`` constraint can be applied to a property or a getter method: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + #[Assert\Slug] + protected string $slug; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + slug: + - Slug: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Author + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('slug', new Assert\Slug()); + } + } + +Examples of valid values: + +* foobar +* foo-bar +* foo123 +* foo-123bar + +Uppercase characters would result in an violation of this constraint. + +Options +------- + +``regex`` +~~~~~~~~~ + +**type**: ``string`` default: ``/^[a-z0-9]+(?:-[a-z0-9]+)*$/`` + +This option allows you to modify the regular expression pattern that the input +will be matched against via the :phpfunction:`preg_match` PHP function. + +If you need to use it, you might also want to take a look at the :doc:`Regex constraint `. + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid slug`` + +This is the message that will be shown if this validator fails. + +You can use the following parameters in this message: + +================= ============================================================== +Parameter Description +================= ============================================================== +``{{ value }}`` The current (invalid) value +================= ============================================================== + +.. include:: /reference/constraints/_groups-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Time.rst b/reference/constraints/Time.rst index 82267074381..6d4de73398f 100644 --- a/reference/constraints/Time.rst +++ b/reference/constraints/Time.rst @@ -1,82 +1,118 @@ Time ==== -Validates that a value is a valid time, meaning either a ``DateTime`` object -or a string (or an object that can be cast into a string) that follows -a valid "HH:MM:SS" format. - -+----------------+------------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+------------------------------------------------------------------------+ -| Options | - `message`_ | -+----------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Time` | -+----------------+------------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\TimeValidator` | -+----------------+------------------------------------------------------------------------+ +Validates that a value is a valid time, meaning a string (or an object that can +be cast into a string) that follows a valid ``H:i:s`` format (e.g. ``'16:27:36'``). + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Time` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\TimeValidator` +========== =================================================================== Basic Usage ----------- -Suppose you have an Event class, with a ``startAt`` field that is the time +Suppose you have an Event class, with a ``startsAt`` field that is the time of the day when the event starts: .. configuration-block:: - .. code-block:: yaml + .. code-block:: php-attributes - # src/Acme/EventBundle/Resources/config/validation.yml - Acme\EventBundle\Entity\Event: - properties: - startsAt: - - Time: ~ - - .. code-block:: php-annotations + // src/Entity/Event.php + namespace App\Entity; - // src/Acme/EventBundle/Entity/Event.php - namespace Acme\EventBundle\Entity; - use Symfony\Component\Validator\Constraints as Assert; class Event { /** - * @Assert\Time() + * @var string A "H:i:s" formatted value */ - protected $startsAt; + #[Assert\Time] + protected string $startsAt; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Event: + properties: + startsAt: + - Time: ~ + .. code-block:: xml - - - - - - + + + + + + + + + + .. code-block:: php - - // src/Acme/EventBundle/Entity/Event.php - namespace Acme\EventBundle\Entity; - - use Symfony\Component\Validator\Mapping\ClassMetadata; + + // src/Entity/Event.php + namespace App\Entity; + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Event { - public static function loadValidatorMetadata(ClassMetadata $metadata) + /** + * @var string A "H:i:s" formatted value + */ + protected string $startsAt; + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('startsAt', new Assert\Time()); } } +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + Options ------- -message -~~~~~~~ +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ -**type**: ``string`` **default**: ``This value is not a valid time`` +**type**: ``string`` **default**: ``This value is not a valid time.`` This message is shown if the underlying data is not a valid time. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +``withSeconds`` +~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``true`` + +This option allows you to specify whether the time should include seconds. + +========= =============================== ============== ================ +Option Pattern Correct value Incorrect value +========= =============================== ============== ================ +``true`` ``/^(\d{2}):(\d{2}):(\d{2})$/`` ``12:00:00`` ``12:00`` +``false`` ``/^(\d{2}):(\d{2})$/`` ``12:00`` ``12:00:00`` +========= =============================== ============== ================ + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Timezone.rst b/reference/constraints/Timezone.rst new file mode 100644 index 00000000000..ffc1cee9fdd --- /dev/null +++ b/reference/constraints/Timezone.rst @@ -0,0 +1,153 @@ +Timezone +======== + +Validates that a value is a valid timezone identifier (e.g. ``Europe/Paris``). + +========== ====================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Timezone` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\TimezoneValidator` +========== ====================================================================== + +Basic Usage +----------- + +Suppose you have a ``UserSettings`` class, with a ``timezone`` field that is a +string which contains any of the `PHP timezone identifiers`_ (e.g. ``America/New_York``): + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/UserSettings.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class UserSettings + { + #[Assert\Timezone] + protected string $timezone; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\UserSettings: + properties: + timezone: + - Timezone: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/UserSettings.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class UserSettings + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('timezone', new Assert\Timezone()); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +``countryCode`` +~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +If the ``zone`` option is set to ``\DateTimeZone::PER_COUNTRY``, this option +restricts the valid timezone identifiers to the ones that belong to the given +country. + +The value of this option must be a valid `ISO 3166-1 alpha-2`_ country code +(e.g. ``CN`` for China). + +.. include:: /reference/constraints/_groups-option.rst.inc + +``intlCompatible`` +~~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +This constraint considers valid both the `PHP timezone identifiers`_ and the +:ref:`ICU timezones ` provided by Symfony's +:doc:`Intl component ` + +However, the timezones provided by the Intl component can be different from the +timezones provided by PHP's Intl extension (because they use different ICU +versions). If this option is set to ``true``, this constraint only considers +valid the values compatible with the PHP ``\IntlTimeZone::createTimeZone()`` method. + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid timezone.`` + +This message is shown if the underlying data is not a valid timezone identifier. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +``zone`` +~~~~~~~~ + +**type**: ``string`` **default**: ``\DateTimeZone::ALL`` + +Set this option to any of the following constants to restrict the valid timezone +identifiers to the ones that belong to that geographical zone: + +* ``\DateTimeZone::AFRICA`` +* ``\DateTimeZone::AMERICA`` +* ``\DateTimeZone::ANTARCTICA`` +* ``\DateTimeZone::ARCTIC`` +* ``\DateTimeZone::ASIA`` +* ``\DateTimeZone::ATLANTIC`` +* ``\DateTimeZone::AUSTRALIA`` +* ``\DateTimeZone::EUROPE`` +* ``\DateTimeZone::INDIAN`` +* ``\DateTimeZone::PACIFIC`` + +In addition, there are some special zone values: + +* ``\DateTimeZone::ALL`` accepts any timezone excluding deprecated timezones; +* ``\DateTimeZone::ALL_WITH_BC`` accepts any timezone including deprecated + timezones; +* ``\DateTimeZone::PER_COUNTRY`` restricts the valid timezones to a certain + country (which is defined using the ``countryCode`` option). + +.. _`PHP timezone identifiers`: https://www.php.net/manual/en/timezones.php +.. _`ISO 3166-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 diff --git a/reference/constraints/Traverse.rst b/reference/constraints/Traverse.rst new file mode 100644 index 00000000000..56d400fb964 --- /dev/null +++ b/reference/constraints/Traverse.rst @@ -0,0 +1,203 @@ +Traverse +======== + +Object properties are only validated if they are accessible, either by being +public or having public accessor methods (e.g. a public getter). +If your object needs to be traversed to validate its data, you can use this +constraint. + +========== =================================================================== +Applies to :ref:`class ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Traverse` +========== =================================================================== + +Basic Usage +----------- + +In the following example, create two classes ``BookCollection`` and ``Book`` +that all have constraints on their properties. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/BookCollection.php + namespace App\Entity; + + use App\Entity\Book; + use Doctrine\Common\Collections\ArrayCollection; + use Doctrine\Common\Collections\Collection; + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Validator\Constraints as Assert; + + #[ORM\Entity] + #[Assert\Traverse] + class BookCollection implements \IteratorAggregate + { + /** + * @var string + */ + #[ORM\Column] + #[Assert\NotBlank] + protected string $name = ''; + + /** + * @var Collection|Book[] + */ + #[ORM\ManyToMany(targetEntity: Book::class)] + protected ArrayCollection $books; + + // some other properties + + public function __construct() + { + $this->books = new ArrayCollection(); + } + + // ... setter for name, adder and remover for books + + // the name can be validated by calling the getter + public function getName(): string + { + return $this->name; + } + + /** + * @return \Generator|Book[] The books for a given author + */ + public function getBooksForAuthor(Author $author): iterable + { + foreach ($this->books as $book) { + if ($book->isAuthoredBy($author)) { + yield $book; + } + } + } + + // neither the method above nor any other specific getter + // could be used to validated all nested books; + // this object needs to be traversed to call the iterator + public function getIterator(): \Iterator + { + return $this->books->getIterator(); + } + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\BookCollection: + constraints: + - Traverse: ~ + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // src/Entity/BookCollection.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class BookCollection + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addConstraint(new Assert\Traverse()); + } + } + +When the object implements ``\Traversable`` (like here with its child +``\IteratorAggregate``), its traversal strategy will implicitly be set and the +object will be iterated over without defining the constraint. +It's mostly useful to add it to be explicit or to disable the traversal using +the ``traverse`` option. +If a public getter exists to return the inner books collection like +``getBooks(): Collection``, the :doc:`/reference/constraints/Valid` constraint +can be used on the ``$books`` property instead. + +Options +------- + +The ``groups`` option is not available for this constraint. + +.. _traverse-option: + +``traverse`` +~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``true`` + +Instances of ``\Traversable`` are traversed by default, use this option to +disable validating: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/BookCollection.php + + // ... same as above + + /** + * ... + */ + #[Assert\Traverse(false)] + class BookCollection implements \IteratorAggregate + { + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\BookCollection: + constraints: + - Traverse: false + + .. code-block:: xml + + + + + + + false + + + + .. code-block:: php + + // src/Entity/BookCollection.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class BookCollection + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addConstraint(new Assert\Traverse(false)); + } + } + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/True.rst b/reference/constraints/True.rst deleted file mode 100644 index 682f3c12a55..00000000000 --- a/reference/constraints/True.rst +++ /dev/null @@ -1,121 +0,0 @@ -True -==== - -Validates that a value is ``true``. Specifically, this checks to see if -the value is exactly ``true``, exactly the integer ``1``, or exactly the -string "``1``". - -Also see :doc:`False `. - -+----------------+---------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+---------------------------------------------------------------------+ -| Options | - `message`_ | -+----------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\True` | -+----------------+---------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\TrueValidator` | -+----------------+---------------------------------------------------------------------+ - -Basic Usage ------------ - -This constraint can be applied to properties (e.g. a ``termsAccepted`` property -on a registration model) or to a "getter" method. It's most powerful in the -latter case, where you can assert that a method returns a true value. For -example, suppose you have the following method: - -.. code-block:: php - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - - class Author - { - protected $token; - - public function isTokenValid() - { - return $this->token == $this->generateToken(); - } - } - -Then you can constrain this method with ``True``. - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - getters: - tokenValid: - - "True": { message: "The token is invalid." } - - .. code-block:: php-annotations - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - protected $token; - - /** - * @Assert\True(message = "The token is invalid") - */ - public function isTokenValid() - { - return $this->token == $this->generateToken(); - } - } - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\True; - - class Author - { - protected $token; - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addGetterConstraint('tokenValid', new True(array( - 'message' => 'The token is invalid.', - ))); - } - - public function isTokenValid() - { - return $this->token == $this->generateToken(); - } - } - -If the ``isTokenValid()`` returns false, the validation will fail. - -Options -------- - -message -~~~~~~~ - -**type**: ``string`` **default**: ``This value should be true`` - -This message is shown if the underlying data is not true. diff --git a/reference/constraints/Twig.rst b/reference/constraints/Twig.rst new file mode 100644 index 00000000000..8435c191855 --- /dev/null +++ b/reference/constraints/Twig.rst @@ -0,0 +1,130 @@ +Twig Constraint +=============== + +.. versionadded:: 7.3 + + The ``Twig`` constraint was introduced in Symfony 7.3. + +Validates that a given string contains valid :ref:`Twig syntax `. +This is particularly useful when template content is user-generated or +configurable, and you want to ensure it can be rendered by the Twig engine. + +.. note:: + + Using this constraint requires having the ``symfony/twig-bridge`` package + installed in your application (e.g. by running ``composer require symfony/twig-bridge``). + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Bridge\\Twig\\Validator\\Constraints\\Twig` +Validator :class:`Symfony\\Bridge\\Twig\\Validator\\Constraints\\TwigValidator` +========== =================================================================== + +Basic Usage +----------- + +Apply the ``Twig`` constraint to validate the contents of any property or the +returned value of any method: + + use Symfony\Bridge\Twig\Validator\Constraints\Twig; + + class Template + { + #[Twig] + private string $templateCode; + } + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Page.php + namespace App\Entity; + + use Symfony\Bridge\Twig\Validator\Constraints\Twig; + + class Page + { + #[Twig] + private string $templateCode; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Page: + properties: + templateCode: + - Symfony\Bridge\Twig\Validator\Constraints\Twig: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Page.php + namespace App\Entity; + + use Symfony\Bridge\Twig\Validator\Constraints\Twig; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Page + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('templateCode', new Twig()); + } + } + +Constraint Options +------------------ + +``message`` +~~~~~~~~~~~ + +**type**: ``message`` **default**: ``This value is not a valid Twig template.`` + +This is the message displayed when the given string does *not* contain valid Twig syntax:: + + // ... + + class Page + { + #[Twig(message: 'Check this Twig code; it contains errors.')] + private string $templateCode; + } + +This message has no parameters. + +``skipDeprecations`` +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``true`` + +If ``true``, Twig deprecation warnings are ignored during validation. Set it to +``false`` to trigger validation errors when the given Twig code contains any deprecations:: + + // ... + + class Page + { + #[Twig(skipDeprecations: false)] + private string $templateCode; + } + +This can be helpful when enforcing stricter template rules or preparing for major +Twig version upgrades. diff --git a/reference/constraints/Type.rst b/reference/constraints/Type.rst index cf2024c6236..b49536dff8b 100644 --- a/reference/constraints/Type.rst +++ b/reference/constraints/Type.rst @@ -2,113 +2,231 @@ Type ==== Validates that a value is of a specific data type. For example, if a variable -should be an array, you can use this constraint with the ``array`` type option -to validate this. - -+----------------+---------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+---------------------------------------------------------------------+ -| Options | - :ref:`type` | -| | - `message`_ | -+----------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Type` | -+----------------+---------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\TypeValidator` | -+----------------+---------------------------------------------------------------------+ +should be an array, you can use this constraint with the ``array`` type +option to validate this. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Type` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\TypeValidator` +========== =================================================================== Basic Usage ----------- -.. configuration-block:: +This constraint should be applied to untyped variables/properties. If a property +or variable is typed and you pass a value of a different type, PHP will throw an +exception before this constraint is checked. - .. code-block:: yaml +The following example checks if ``emailAddress`` is an instance of ``Symfony\Component\Mime\Address``, +``firstName`` is of type ``string`` (using :phpfunction:`is_string` PHP function), +``age`` is an ``integer`` (using :phpfunction:`is_int` PHP function) and +``accessCode`` contains either only letters or only digits (using +:phpfunction:`ctype_alpha` and :phpfunction:`ctype_digit` PHP functions). - # src/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - age: - - Type: - type: integer - message: The value {{ value }} is not a valid {{ type }}. +.. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; + use Symfony\Component\Mime\Address; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\Type(type="integer", message="The value {{ value }} is not a valid {{ type }}.") - */ + #[Assert\Type(Address::class)] + protected $emailAddress; + + #[Assert\Type('string')] + protected $firstName; + + #[Assert\Type( + type: 'integer', + message: 'The value {{ value }} is not a valid {{ type }}.', + )] protected $age; + + #[Assert\Type(type: ['alpha', 'digit'])] + protected $accessCode; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + emailAddress: + - Type: Symfony\Component\Mime\Address + + firstName: + - Type: string + + age: + - Type: + type: integer + message: The value {{ value }} is not a valid {{ type }}. + + accessCode: + - Type: + type: [alpha, digit] + .. code-block:: xml - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + .. code-block:: php - - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Mime\Address; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('age', new Assert\Type(array( - 'type' => 'integer', - 'message' => 'The value {{ value }} is not a valid {{ type }}.', - ))); + $metadata->addPropertyConstraint('emailAddress', new Assert\Type(Address::class)); + + $metadata->addPropertyConstraint('firstName', new Assert\Type('string')); + + $metadata->addPropertyConstraint('age', new Assert\Type( + type: 'integer', + message: 'The value {{ value }} is not a valid {{ type }}.', + )); + + $metadata->addPropertyConstraint('accessCode', new Assert\Type( + type: ['alpha', 'digit'], + )); } } +.. include:: /reference/constraints/_null-values-are-valid.rst.inc + Options ------- -.. _reference-constraint-type-type: +.. include:: /reference/constraints/_groups-option.rst.inc -type -~~~~ +``message`` +~~~~~~~~~~~ -**type**: ``string`` [:ref:`default option`] +**type**: ``string`` **default**: ``This value should be of type {{ type }}.`` -This required option is the fully qualified class name or one of the PHP datatypes -as determined by PHP's ``is_`` functions. +The message if the underlying data is not of the given type. -* `array `_ -* `bool `_ -* `callable `_ -* `float `_ -* `double `_ -* `int `_ -* `integer `_ -* `long `_ -* `null `_ -* `numeric `_ -* `object `_ -* `real `_ -* `resource `_ -* `scalar `_ -* `string `_ +You can use the following parameters in this message: -message -~~~~~~~ +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ type }}`` The expected type +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== -**type**: ``string`` **default**: ``This value should be of type {{ type }}`` +.. include:: /reference/constraints/_payload-option.rst.inc -The message if the underlying data is not of the given type. +.. _reference-constraint-type-type: + +``type`` +~~~~~~~~ + +**type**: ``string`` or ``array`` + +This required option defines the type or collection of types allowed for the +given value. Each type is either the FQCN (fully qualified class name) of some +PHP class/interface or a valid PHP datatype (checked by PHP's ``is_()`` functions): + +* :phpfunction:`bool ` +* :phpfunction:`boolean ` +* :phpfunction:`int ` +* :phpfunction:`integer ` +* :phpfunction:`long ` +* :phpfunction:`float ` +* :phpfunction:`double ` +* :phpfunction:`real ` +* :phpfunction:`numeric ` +* :phpfunction:`string ` +* :phpfunction:`scalar ` +* :phpfunction:`array ` +* :phpfunction:`iterable ` +* :phpfunction:`countable ` +* :phpfunction:`callable ` +* :phpfunction:`object ` +* :phpfunction:`resource ` +* :phpfunction:`null ` + +If you're dealing with arrays, you can use the following types in the constraint: + +* ``list`` which uses :phpfunction:`array_is_list ` internally +* ``associative_array`` which is true for any **non-empty** array that is not a list + +Also, you can use ``ctype_*()`` functions from corresponding +`built-in PHP extension`_. Consider `a list of ctype functions`_: + +* :phpfunction:`alnum ` +* :phpfunction:`alpha ` +* :phpfunction:`cntrl ` +* :phpfunction:`digit ` +* :phpfunction:`graph ` +* :phpfunction:`lower ` +* :phpfunction:`print ` +* :phpfunction:`punct ` +* :phpfunction:`space ` +* :phpfunction:`upper ` +* :phpfunction:`xdigit ` + +Make sure that the proper :phpfunction:`locale ` is set before +using one of these. + +.. versionadded:: 7.1 + + The ``list`` and ``associative_array`` types were introduced in Symfony + 7.1. + +Finally, you can use aggregated functions: + +* ``number``: ``is_int || is_float && !is_nan`` +* ``finite-float``: ``is_float && is_finite`` +* ``finite-number``: ``is_int || is_float && is_finite`` + +.. _built-in PHP extension: https://www.php.net/book.ctype +.. _a list of ctype functions: https://www.php.net/ref.ctype diff --git a/reference/constraints/Ulid.rst b/reference/constraints/Ulid.rst new file mode 100644 index 00000000000..4094bab98f5 --- /dev/null +++ b/reference/constraints/Ulid.rst @@ -0,0 +1,116 @@ +ULID +==== + +Validates that a value is a valid `Universally Unique Lexicographically Sortable Identifier (ULID)`_. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Ulid` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\UlidValidator` +========== =================================================================== + +Basic Usage +----------- + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/File.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class File + { + #[Assert\Ulid] + protected string $identifier; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\File: + properties: + identifier: + - Ulid: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/File.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class File + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('identifier', new Assert\Ulid()); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +``format`` +~~~~~~~~~~ + +**type**: ``string`` **default**: ``Ulid::FORMAT_BASE_32`` + +The format of the ULID to validate. The following formats are available: + +* ``Ulid::FORMAT_BASE_32``: The ULID is encoded in `base32`_ (default) +* ``Ulid::FORMAT_BASE_58``: The ULID is encoded in `base58`_ +* ``Ulid::FORMAT_RFC4122``: The ULID is encoded in the `RFC 4122 format`_ + +.. versionadded:: 7.2 + + The ``format`` option was introduced in Symfony 7.2. + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This is not a valid ULID.`` + +This message is shown if the string is not a valid ULID. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_normalizer-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`Universally Unique Lexicographically Sortable Identifier (ULID)`: https://github.com/ulid/spec +.. _`base32`: https://en.wikipedia.org/wiki/Base32 +.. _`base58`: https://en.wikipedia.org/wiki/Binary-to-text_encoding#Base58 +.. _`RFC 4122 format`: https://datatracker.ietf.org/doc/html/rfc4122 diff --git a/reference/constraints/Unique.rst b/reference/constraints/Unique.rst new file mode 100644 index 00000000000..9ce84139cd5 --- /dev/null +++ b/reference/constraints/Unique.rst @@ -0,0 +1,232 @@ +Unique +====== + +Validates that all the elements of the given collection are unique (none of them +is present more than once). By default elements are compared strictly, +so ``'7'`` and ``7`` are considered different elements (a string and an integer, respectively). +If you want to apply any other comparison logic, use the `normalizer`_ option. + +.. seealso:: + + If you want to apply different validation constraints to the elements of a + collection or want to make sure that certain collection keys are present, + use the :doc:`Collection constraint `. + +.. seealso:: + + If you want to validate that the value of an entity property is unique among + all entities of the same type (e.g. the registration email of all users) use + the :doc:`UniqueEntity constraint `. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Unique` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\UniqueValidator` +========== =================================================================== + +Basic Usage +----------- + +This constraint can be applied to any property of type ``array`` or +``\Traversable``. In the following example, ``$contactEmails`` is an array of +strings: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Person + { + #[Assert\Unique] + protected array $contactEmails; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Person: + properties: + contactEmails: + - Unique: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Person.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Person + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('contactEmails', new Assert\Unique()); + } + } + +Options +------- + +``fields`` +~~~~~~~~~~ + +**type**: ``array`` | ``string`` + +This is defines the key or keys in a collection that should be checked for +uniqueness. By default, all collection keys are checked for uniqueness. + +For instance, assume you have a collection of items that contain a +``latitude``, ``longitude`` and ``label`` fields. By default, you can have +duplicate coordinates as long as the label is different. By setting the +``fields`` option, you can force latitude+longitude to be unique in the +collection:: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/PointOfInterest.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class PointOfInterest + { + #[Assert\Unique(fields: ['latitude', 'longitude'])] + protected array $coordinates; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\PointOfInterest: + properties: + coordinates: + - Unique: + fields: [latitude, longitude] + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/PointOfInterest.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class PointOfInterest + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('coordinates', new Assert\Unique( + fields: ['latitude', 'longitude'], + )); + } + } + +.. include:: /reference/constraints/_groups-option.rst.inc + +``errorPath`` +~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +.. versionadded:: 7.2 + + The ``errorPath`` option was introduced in Symfony 7.2. + +If a validation error occurs, the error message is, by default, bound to the +first element in the collection. Use this option to bind the error message to a +specific field within the first item of the collection. + +The value of this option must use any :doc:`valid PropertyAccess syntax ` +(e.g. ``'point_of_interest'``, ``'user.email'``). + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This collection should contain only unique elements.`` + +This is the message that will be shown if at least one element is repeated in +the collection. + +You can use the following parameters in this message: + +============================= ================================================ +Parameter Description +============================= ================================================ +``{{ value }}`` The current (invalid) value +============================= ================================================ + +``normalizer`` +~~~~~~~~~~~~~~ + +**type**: a `PHP callable`_ **default**: ``null`` + +This option defined the PHP callable applied to each element of the given +collection before checking if the collection is valid. + +For example, you can pass the ``'trim'`` string to apply the :phpfunction:`trim` +PHP function to each element of the collection in order to ignore leading and +trailing whitespace during validation. + +.. include:: /reference/constraints/_payload-option.rst.inc + +``stopOnFirstError`` +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``true`` + +By default, this constraint stops at the first violation. If this option is set +to ``false``, validation continues on all elements and returns all detected +:class:`Symfony\\Component\\Validator\\ConstraintViolation` objects. + +.. versionadded:: 7.3 + + The ``stopOnFirstError`` option was introduced in Symfony 7.3. + +.. _`PHP callable`: https://www.php.net/callable diff --git a/reference/constraints/UniqueEntity.rst b/reference/constraints/UniqueEntity.rst index 5ec43a1c225..e819a8009dc 100644 --- a/reference/constraints/UniqueEntity.rst +++ b/reference/constraints/UniqueEntity.rst @@ -5,147 +5,396 @@ Validates that a particular field (or fields) in a Doctrine entity is (are) unique. This is commonly used, for example, to prevent a new user to register using an email address that already exists in the system. -+----------------+-------------------------------------------------------------------------------------+ -| Applies to | :ref:`class` | -+----------------+-------------------------------------------------------------------------------------+ -| Options | - `fields`_ | -| | - `message`_ | -| | - `em`_ | -| | - `repositoryMethod`_ | -+----------------+-------------------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Bridge\\Doctrine\\Validator\\Constraints\\UniqueEntity` | -+----------------+-------------------------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Bridge\\Doctrine\\Validator\\Constraints\\UniqueEntityValidator` | -+----------------+-------------------------------------------------------------------------------------+ +.. seealso:: + + If you want to validate that all the elements of the collection are unique + use the :doc:`Unique constraint `. + +.. note:: + + In order to use this constraint, you should have installed the + symfony/doctrine-bridge with Composer. + +========== =================================================================== +Applies to :ref:`class ` +Class :class:`Symfony\\Bridge\\Doctrine\\Validator\\Constraints\\UniqueEntity` +Validator :class:`Symfony\\Bridge\\Doctrine\\Validator\\Constraints\\UniqueEntityValidator` +========== =================================================================== Basic Usage ----------- -Suppose you have an ``AcmeUserBundle`` bundle with a ``User`` entity that has an -``email`` field. You can use the ``UniqueEntity`` constraint to guarantee that -the ``email`` field remains unique between all of the constraints in your user -table: +Suppose you have a ``User`` entity that has an ``email`` field. You can use the +``UniqueEntity`` constraint to guarantee that the ``email`` field remains unique +between all of the rows in your user table: .. configuration-block:: + .. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + + // DON'T forget the following use statement!!! + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + + use Symfony\Component\Validator\Constraints as Assert; + + #[ORM\Entity] + #[UniqueEntity('email')] + class User + { + #[ORM\Column(name: 'email', type: 'string', length: 255, unique: true)] + #[Assert\Email] + protected string $email; + } + .. code-block:: yaml - # src/Acme/UserBundle/Resources/config/validation.yml - Acme\UserBundle\Entity\Author: + # config/validator/validation.yaml + App\Entity\User: constraints: - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: email properties: email: - Email: ~ - .. code-block:: php-annotations + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php - // Acme/UserBundle/Entity/User.php - namespace Acme\UserBundle\Entity; + // src/Entity/User.php + namespace App\Entity; + + // DON'T forget the following use statement!!! + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; - use Doctrine\ORM\Mapping as ORM; - // DON'T forget this use statement!!! + class User + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addConstraint(new UniqueEntity( + fields: 'email', + )); + + $metadata->addPropertyConstraint('email', new Assert\Email()); + } + } + + // src/Form/Type/UserType.php + namespace App\Form\Type; + + // ... + // DON'T forget the following use statement!!! use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; - /** - * @ORM\Entity - * @UniqueEntity("email") - */ - class Author + class UserType extends AbstractType { - /** - * @var string $email - * - * @ORM\Column(name="email", type="string", length=255, unique=true) - * @Assert\Email() - */ - protected $email; - // ... + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + // ... + 'data_class' => User::class, + 'constraints' => [ + new UniqueEntity(fields: ['email']), + ], + ]); + } } - .. code-block:: xml +.. warning:: - - - - - - - - - + This constraint doesn't provide any protection against `race conditions`_. + They may occur when another entity is persisted by an external process after + this validation has passed and before this entity is actually persisted in + the database. - .. code-block:: php +.. warning:: + This constraint cannot deal with duplicates found in a collection of items + that haven't been persisted as entities yet. You'll need to create your own + validator to handle that case. - // Acme/UserBundle/Entity/User.php - namespace Acme\UserBundle\Entity; +Options +------- - use Symfony\Component\Validator\Constraints as Assert; +em +~~ + +**type**: ``string`` **default**: ``null`` + +The name of the entity manager to use for making the query to determine +the uniqueness. If it's left blank, the correct entity manager will be +determined for this class. For that reason, this option should probably +not need to be used. + +``entityClass`` +~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` - // DON'T forget this use statement!!! +By default, the query performed to ensure the uniqueness uses the repository of +the current class instance. However, in some cases, such as when using Doctrine +inheritance mapping, you need to execute the query in a different repository. +Use this option to define the fully-qualified class name (FQCN) of the Doctrine +entity associated with the repository you want to use. + +``errorPath`` +~~~~~~~~~~~~~ + +**type**: ``string`` **default**: The name of the first field in `fields`_ + +If the entity violates the constraint the error message is bound to the +first field in `fields`_. If there is more than one field, you may want +to map the error message to another field. + +Consider this example: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Service.php + namespace App\Entity; + + use App\Entity\Host; + use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; - - class Author + + #[ORM\Entity] + #[UniqueEntity( + fields: ['host', 'port'], + message: 'This port is already in use on that host.', + errorPath: 'port', + )] + class Service { - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addConstraint(new UniqueEntity(array( - 'fields' => 'email', - 'message' => 'This email already exists.', - ))); + #[ORM\ManyToOne(targetEntity: Host::class)] + public Host $host; + + #[ORM\Column(type: 'integer')] + public int $port; + } - $metadata->addPropertyConstraint(new Assert\Email()); + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Service: + constraints: + - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: + fields: [host, port] + message: 'This port is already in use on that host.' + errorPath: port + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Service.php + namespace App\Entity; + + use App\Entity\Host; + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Service + { + public Host $host; + public int $port; + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addConstraint(new UniqueEntity([ + 'fields' => ['host', 'port'], + 'message' => 'This port is already in use on that host.', + 'errorPath' => 'port', + ])); } } -Options -------- +Now, the message would be bound to the ``port`` field with this configuration. -fields -~~~~~~ +``fields`` +~~~~~~~~~~ -**type**: ``array`` | ``string`` [:ref:`default option`] +**type**: ``array`` | ``string`` This required option is the field (or list of fields) on which this entity should be unique. For example, if you specified both the ``email`` and ``name`` field in a single ``UniqueEntity`` constraint, then it would enforce that -the combination value where unique (e.g. two users could have the same email, +the combination value is unique (e.g. two users could have the same email, as long as they don't have the same name also). If you need to require two fields to be individually unique (e.g. a unique -``email`` *and* a unique ``username``), you use two ``UniqueEntity`` entries, +``email`` and a unique ``username``), you use two ``UniqueEntity`` entries, each with a single field. -message -~~~~~~~ +.. include:: /reference/constraints/_groups-option.rst.inc + +``ignoreNull`` +~~~~~~~~~~~~~~ + +**type**: ``boolean`` | ``string`` | ``array`` **default**: ``true`` + +If this option is set to ``true``, then the constraint will allow multiple +entities to have a ``null`` value for a field without failing validation. +If set to ``false``, only one ``null`` value is allowed - if a second entity +also has a ``null`` value, validation would fail. + +In addition to ignoring the ``null`` values of all unique fields, you can also use +this option to specify one or more fields to only ignore ``null`` values on them: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + use Symfony\Component\Validator\Constraints as Assert; + + #[ORM\Entity] + #[UniqueEntity(fields: ['email', 'phoneNumber'], ignoreNull: 'phoneNumber')] + class User + { + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\User: + constraints: + - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: + fields: ['email', 'phoneNumber'] + ignoreNull: 'phoneNumber' + properties: + # ... + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + use Symfony\Component\Validator\Constraints as Assert; + + class User + { + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addConstraint(new UniqueEntity( + fields: ['email', 'phoneNumber'], + ignoreNull: 'phoneNumber', + )); + + // ... + } + } + +.. warning:: + + If you ``ignoreNull`` on fields that are part of a unique index in your + database, you might see insertion errors when your application attempts to + persist entities that the ``UniqueEntity`` constraint considers valid. + +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This value is already used.`` -The message that's displayed when this constraint fails. +The message that's displayed when this constraint fails. This message is by default +mapped to the first field causing the violation. When using multiple fields +in the constraint, the mapping can be specified via the `errorPath`_ property. -em -~~ +Messages can include the ``{{ value }}`` placeholder to display a string +representation of the invalid entity. If the entity doesn't define the +``__toString()`` method, the following generic value will be used: *"Object of +class __CLASS__ identified by "* + +You can use the following parameters in this message: -**type**: ``string`` +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== -The name of the entity manager to use for making the query to determine the -uniqueness. If it's left blank, the correct entity manager will determined for -this class. For that reason, this option should probably not need to be -used. +.. include:: /reference/constraints/_payload-option.rst.inc -repositoryMethod -~~~~~~~~~~~~~~~~ +``repositoryMethod`` +~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``findBy`` -.. versionadded:: 2.1 - The ``repositoryMethod`` option was added in Symfony 2.1. Before, it - always used the ``findBy`` method. +The name of the repository method used to determine the uniqueness. If it's left +blank, ``findBy()`` will be used. The method receives as its argument a +``fieldName => value`` associative array (where ``fieldName`` is each of the +fields configured in the ``fields`` option). The method should return a +:phpfunction:`countable PHP variable `. -The name of the repository method to use for making the query to determine the -uniqueness. If it's left blank, the ``findBy`` method will be used. This -method should return a countable result. +.. _`race conditions`: https://en.wikipedia.org/wiki/Race_condition diff --git a/reference/constraints/Url.rst b/reference/constraints/Url.rst index 2b6614f59d9..fbeaa6da522 100644 --- a/reference/constraints/Url.rst +++ b/reference/constraints/Url.rst @@ -3,85 +3,423 @@ Url Validates that a value is a valid URL string. -+----------------+---------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+---------------------------------------------------------------------+ -| Options | - `message`_ | -| | - `protocols`_ | -+----------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Url` | -+----------------+---------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\UrlValidator` | -+----------------+---------------------------------------------------------------------+ +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Url` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\UrlValidator` +========== =================================================================== Basic Usage ----------- .. configuration-block:: + .. code-block:: php-attributes + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + #[Assert\Url] + protected string $bioUrl; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + bioUrl: + - Url: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Author + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('bioUrl', new Assert\Url()); + } + } + +This constraint doesn't check that the host of the given URL really exists, +because the information of the DNS records is not reliable. Use the +:phpfunction:`checkdnsrr` PHP function if you still want to check that. + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid URL.`` + +This message is shown if the URL is invalid. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + #[Assert\Url( + message: 'The url {{ value }} is not a valid url', + )] + protected string $bioUrl; + } + .. code-block:: yaml - # src/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: + # config/validator/validation.yaml + App\Entity\Author: properties: bioUrl: - Url: + message: The url "{{ value }}" is not a valid url. - .. code-block:: php-annotations + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Author.php + namespace App\Entity; - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - /** - * @Assert\Url() - */ - protected $bioUrl; + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('bioUrl', new Assert\Url( + message: 'The url "{{ value }}" is not a valid url.', + )); + } } +.. include:: /reference/constraints/_normalizer-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc + +``protocols`` +~~~~~~~~~~~~~ + +**type**: ``array`` **default**: ``['http', 'https']`` + +The protocols considered to be valid for the URL. For example, if you also consider +the ``ftp://`` type URLs to be valid, redefine the ``protocols`` array, listing +``http``, ``https``, and also ``ftp``. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + #[Assert\Url( + protocols: ['http', 'https', 'ftp'], + )] + protected string $bioUrl; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + bioUrl: + - Url: { protocols: [http, https, ftp] } + .. code-block:: xml - - - - - - + + + + + + + + + + + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; - - use Symfomy\Component\Validator\Mapping\ClassMetadata; + // src/Entity/Author.php + namespace App\Entity; + use Symfony\Component\Validator\Constraints as Assert; - + use Symfony\Component\Validator\Mapping\ClassMetadata; + class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('bioUrl', new Assert\Url()); + $metadata->addPropertyConstraint('bioUrl', new Assert\Url( + protocols: ['http', 'https', 'ftp'], + )); } } - -Options -------- -message -~~~~~~~ +``relativeProtocol`` +~~~~~~~~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``This value is not a valid URL`` +**type**: ``boolean`` **default**: ``false`` -This message is shown if the URL is invalid. +If ``true``, the protocol is considered optional when validating the syntax of +the given URL. This means that both ``http://`` and ``https://`` are valid but +also relative URLs that contain no protocol (e.g. ``//example.com``). + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + #[Assert\Url( + relativeProtocol: true, + )] + protected string $bioUrl; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + bioUrl: + - Url: { relativeProtocol: true } + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Author + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('bioUrl', new Assert\Url( + relativeProtocol: true, + )); + } + } + +``requireTld`` +~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +.. versionadded:: 7.1 -protocols -~~~~~~~~~ + The ``requireTld`` option was introduced in Symfony 7.1. -**type**: ``array`` **default**: ``array('http', 'https')`` +.. deprecated:: 7.1 + + Not setting the ``requireTld`` option is deprecated since Symfony 7.1 + and will default to ``true`` in Symfony 8.0. + +By default, URLs like ``https://aaa`` or ``https://foobar`` are considered valid +because they are tecnically correct according to the `URL spec`_. If you set this option +to ``true``, the host part of the URL will have to include a TLD (top-level domain +name): e.g. ``https://example.com`` will be valid but ``https://example`` won't. + +.. note:: + + This constraint does not validate that the given TLD value is included in + the `list of official top-level domains`_ (because that list is growing + continuously and it's hard to keep track of it). + +``tldMessage`` +~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This URL does not contain a TLD.`` + +.. versionadded:: 7.1 + + The ``tldMessage`` option was introduced in Symfony 7.1. + +This message is shown if the ``requireTld`` option is set to ``true`` and the URL +does not contain at least one TLD. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Website.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Website + { + #[Assert\Url( + requireTld: true, + tldMessage: 'Add at least one TLD to the {{ value }} URL.', + )] + protected string $homepageUrl; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Website: + properties: + homepageUrl: + - Url: + requireTld: true + tldMessage: Add at least one TLD to the {{ value }} URL. + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Website.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Website + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('homepageUrl', new Assert\Url( + requireTld: true, + tldMessage: 'Add at least one TLD to the {{ value }} URL.', + )); + } + } -The protocols that will be considered to be valid. For example, if you also -needed ``ftp://`` type URLs to be valid, you'd redefine the ``protocols`` -array, listing ``http``, ``https``, and also ``ftp``. +.. _`URL spec`: https://datatracker.ietf.org/doc/html/rfc1738 +.. _`list of official top-level domains`: https://en.wikipedia.org/wiki/List_of_Internet_top-level_domains diff --git a/reference/constraints/UserPassword.rst b/reference/constraints/UserPassword.rst index dc0dfcab277..5981be99b66 100644 --- a/reference/constraints/UserPassword.rst +++ b/reference/constraints/UserPassword.rst @@ -1,106 +1,115 @@ UserPassword ============ -.. versionadded:: 2.1 - This constraint is new in version 2.1. +This validates that an input value is equal to the current authenticated +user's password. This is useful in a form where a user can change their +password, but needs to enter their old password for security. .. note:: - Since Symfony 2.2, the `UserPassword*` classes in the - `Symfony\Component\Security\Core\Validator\Constraint` namespace are - deprecated and will be removed in Symfony 2.3. Please use the - `UserPassword*` classes in the - `Symfony\Component\Security\Core\Validator\Constraints` namespace instead. - -This validates that an input value is equal to the current authenticated -user's password. This is useful in a form where a user can change his password, -but needs to enter his old password for security. + This should **not** be used to validate a login form, since this is + done automatically by the security system. .. note:: - This should **not** be used to validate a login form, since this is done - automatically by the security system. + In order to use this constraint, you should have installed the + symfony/security-core component with Composer. -+----------------+--------------------------------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+--------------------------------------------------------------------------------------------+ -| Options | - `message`_ | -+----------------+--------------------------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Security\\Core\\Validator\\Constraints\\UserPassword` | -+----------------+--------------------------------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Security\\Core\\Validator\\Constraints\\UserPasswordValidator` | -+----------------+--------------------------------------------------------------------------------------------+ +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Security\\Core\\Validator\\Constraints\\UserPassword` +Validator :class:`Symfony\\Component\\Security\\Core\\Validator\\Constraints\\UserPasswordValidator` +========== =================================================================== Basic Usage ----------- -Suppose you have a `PasswordChange` class, that's used in a form where the -user can change his password by entering his old password and a new password. -This constraint will validate that the old password matches the user's current -password: +Suppose you have a ``ChangePassword`` class, that's used in a form where +the user can change their password by entering their old password and a +new password. This constraint will validate that the old password matches +the user's current password: .. configuration-block:: - .. code-block:: yaml + .. code-block:: php-attributes - # src/UserBundle/Resources/config/validation.yml - Acme\UserBundle\Form\Model\ChangePassword: - properties: - oldPassword: - - Symfony\Component\Security\Core\Validator\Constraints\UserPassword: - message: "Wrong value for your current password" - - .. code-block:: php-annotations - - // src/Acme/UserBundle/Form/Model/ChangePassword.php - namespace Acme\UserBundle\Form\Model; + // src/Form/Model/ChangePassword.php + namespace App\Form\Model; use Symfony\Component\Security\Core\Validator\Constraints as SecurityAssert; class ChangePassword { - /** - * @SecurityAssert\UserPassword( - * message = "Wrong value for your current password" - * ) - */ - protected $oldPassword; + #[SecurityAssert\UserPassword( + message: 'Wrong value for your current password', + )] + protected string $oldPassword; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Form\Model\ChangePassword: + properties: + oldPassword: + - Symfony\Component\Security\Core\Validator\Constraints\UserPassword: + message: 'Wrong value for your current password' + .. code-block:: xml - - - - - - + + + + + + + + + + + + .. code-block:: php - // src/Acme/UserBundle/Form/Model/ChangePassword.php - namespace Acme\UserBundle\Form\Model; + // src/Form/Model/ChangePassword.php + namespace App\Form\Model; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Security\Core\Validator\Constraints as SecurityAssert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class ChangePassword { - public static function loadValidatorData(ClassMetadata $metadata) + // ... + + public static function loadValidatorData(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('oldPassword', new SecurityAssert\UserPassword(array( - 'message' => 'Wrong value for your current password', - ))); + $metadata->addPropertyConstraint( + 'oldPassword', + new SecurityAssert\UserPassword([ + 'message' => 'Wrong value for your current password', + ]) + ); } } Options ------- -message -~~~~~~~ +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ -**type**: ``message`` **default**: ``This value should be the user current password`` +**type**: ``message`` **default**: ``This value should be the user current password.`` This is the message that's displayed when the underlying string does *not* match the current user's password. + +This message has no parameters. + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Uuid.rst b/reference/constraints/Uuid.rst new file mode 100644 index 00000000000..c9f6c9741bf --- /dev/null +++ b/reference/constraints/Uuid.rst @@ -0,0 +1,134 @@ +UUID +==== + +Validates that a value is a valid `Universally unique identifier (UUID)`_ per `RFC 4122`_. +By default, this will validate the format according to the RFC's guidelines, but this can +be relaxed to accept non-standard UUIDs that other systems (like PostgreSQL) accept. +UUID versions can also be restricted using a list of allowed versions. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Uuid` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\UuidValidator` +========== =================================================================== + +Basic Usage +----------- + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/File.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class File + { + #[Assert\Uuid] + protected string $identifier; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\File: + properties: + identifier: + - Uuid: ~ + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/File.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class File + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('identifier', new Assert\Uuid()); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This is not a valid UUID.`` + +This message is shown if the string is not a valid UUID. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. include:: /reference/constraints/_normalizer-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc + +``strict`` +~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``true`` + +If this option is set to ``true`` the constraint will check if the UUID is formatted per the +RFC's input format rules: ``216fff40-98d9-11e3-a5e2-0800200c9a66``. Setting this to ``false`` +will allow alternate input formats like: + +* ``216f-ff40-98d9-11e3-a5e2-0800-200c-9a66`` +* ``{216fff40-98d9-11e3-a5e2-0800200c9a66}`` +* ``216fff4098d911e3a5e20800200c9a66`` + +``versions`` +~~~~~~~~~~~~ + +**type**: ``int[]|int`` **default**: ``[1,2,3,4,5,6,7,8]`` + +This option can be used to only allow specific `UUID versions`_ (by default, all +of them are allowed). Valid versions are 1 - 8. Instead of using numeric values, +you can also use the following PHP constants to refer to each UUID version: + +* ``Uuid::V1_MAC`` +* ``Uuid::V2_DCE`` +* ``Uuid::V3_MD5`` +* ``Uuid::V4_RANDOM`` +* ``Uuid::V5_SHA1`` +* ``Uuid::V6_SORTABLE`` +* ``Uuid::V7_MONOTONIC`` +* ``Uuid::V8_CUSTOM`` + +.. _`Universally unique identifier (UUID)`: https://en.wikipedia.org/wiki/Universally_unique_identifier +.. _`RFC 4122`: https://tools.ietf.org/html/rfc4122 +.. _`UUID versions`: https://en.wikipedia.org/wiki/Universally_unique_identifier#Versions diff --git a/reference/constraints/Valid.rst b/reference/constraints/Valid.rst index f34908c6285..61a2c1d992c 100644 --- a/reference/constraints/Valid.rst +++ b/reference/constraints/Valid.rst @@ -2,53 +2,87 @@ Valid ===== This constraint is used to enable validation on objects that are embedded -as properties on an object being validated. This allows you to validate an -object and all sub-objects associated with it. +as properties on an object being validated. This allows you to validate +an object and all sub-objects associated with it. -+----------------+---------------------------------------------------------------------+ -| Applies to | :ref:`property or method` | -+----------------+---------------------------------------------------------------------+ -| Options | - `traverse`_ | -+----------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\Type` | -+----------------+---------------------------------------------------------------------+ +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Valid` +========== =================================================================== + +.. include:: /reference/forms/types/options/_error_bubbling_hint.rst.inc Basic Usage ----------- In the following example, create two classes ``Author`` and ``Address`` -that both have constraints on their properties. Furthermore, ``Author`` stores -an ``Address`` instance in the ``$address`` property. - -.. code-block:: php +that both have constraints on their properties. Furthermore, ``Author`` +stores an ``Address`` instance in the ``$address`` property:: - // src/Acme/HelloBundle/Entity/Address.php - namespace Amce\HelloBundle\Entity; + // src/Entity/Address.php + namespace App\Entity; class Address { - protected $street; - protected $zipCode; + protected string $street; + + protected string $zipCode; } .. code-block:: php - // src/Acme/HelloBundle/Entity/Author.php - namespace Amce\HelloBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; class Author { - protected $firstName; - protected $lastName; - protected $address; + protected string $firstName; + + protected string $lastName; + + protected Address $address; } .. configuration-block:: + .. code-block:: php-attributes + + // src/Entity/Address.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Address + { + #[Assert\NotBlank] + protected string $street; + + #[Assert\NotBlank] + #[Assert\Length(max: 5)] + protected string $zipCode; + } + + // src/Entity/Author.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + #[Assert\NotBlank] + #[Assert\Length(min: 4)] + protected string $firstName; + + #[Assert\NotBlank] + protected string $lastName; + + protected Address $address; + } + .. code-block:: yaml - # src/Acme/HelloBundle/Resources/config/validation.yml - Acme\HelloBundle\Entity\Address: + # config/validator/validation.yaml + App\Entity\Address: properties: street: - NotBlank: ~ @@ -57,7 +91,7 @@ an ``Address`` instance in the ``$address`` property. - Length: max: 5 - Acme\HelloBundle\Entity\Author: + App\Entity\Author: properties: firstName: - NotBlank: ~ @@ -66,186 +100,169 @@ an ``Address`` instance in the ``$address`` property. lastName: - NotBlank: ~ - .. code-block:: php-annotations - - // src/Acme/HelloBundle/Entity/Address.php - namespace Acme\HelloBundle\Entity; - - use Symfony\Component\Validator\Constraints as Assert; - - class Address - { - /** - * @Assert\NotBlank() - */ - protected $street; - - /** - * @Assert\NotBlank - * @Assert\Length(max = "5") - */ - protected $zipCode; - } - - // src/Acme/HelloBundle/Entity/Author.php - namespace Acme\HelloBundle\Entity; - - class Author - { - /** - * @Assert\NotBlank - * @Assert\Length(min = "4") - */ - protected $firstName; - - /** - * @Assert\NotBlank - */ - protected $lastName; - - protected $address; - } - .. code-block:: xml - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + .. code-block:: php - // src/Acme/HelloBundle/Entity/Address.php - namespace Acme\HelloBundle\Entity; + // src/Entity/Address.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Address { - protected $street; - protected $zipCode; + // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('street', new Assert\NotBlank()); $metadata->addPropertyConstraint('zipCode', new Assert\NotBlank()); - $metadata->addPropertyConstraint( - 'zipCode', - new Assert\Length(array("max" => 5))); + $metadata->addPropertyConstraint('zipCode', new Assert\Length(max: 5)); } } - // src/Acme/HelloBundle/Entity/Author.php - namespace Acme\HelloBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - protected $firstName; - protected $lastName; - protected $address; + // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', new Assert\NotBlank()); - $metadata->addPropertyConstraint('firstName', new Assert\Length(array("min" => 4))); + $metadata->addPropertyConstraint('firstName', new Assert\Length(min: 4)); $metadata->addPropertyConstraint('lastName', new Assert\NotBlank()); } } -With this mapping, it is possible to successfully validate an author with an -invalid address. To prevent that, add the ``Valid`` constraint to the ``$address`` -property. +With this mapping, it is possible to successfully validate an author with +an invalid address. To prevent that, add the ``Valid`` constraint to the +``$address`` property. .. configuration-block:: - .. code-block:: yaml - - # src/Acme/HelloBundle/Resources/config/validation.yml - Acme\HelloBundle\Author: - properties: - address: - - Valid: ~ - - .. code-block:: php-annotations + .. code-block:: php-attributes - // src/Acme/HelloBundle/Entity/Author.php - namespace Acme\HelloBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\Valid - */ - protected $address; + #[Assert\Valid] + protected Address $address; } + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Author: + properties: + address: + - Valid: ~ + .. code-block:: xml - - - - - - + + + + + + + + + + .. code-block:: php - // src/Acme/HelloBundle/Entity/Author.php - namespace Acme\HelloBundle\Entity; + // src/Entity/Author.php + namespace App\Entity; - use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - protected $address; + // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('address', new Assert\Valid()); } } -If you validate an author with an invalid address now, you can see that the -validation of the ``Address`` fields failed. +If you validate an author with an invalid address now, you can see that +the validation of the ``Address`` fields failed. - Acme\HelloBundle\Author.address.zipCode: - This value is too long. It should have 5 characters or less +.. code-block:: text + + App\Entity\Author.address.zipCode: + This value is too long. It should have 5 characters or less. + +.. tip:: + + If you also want to validate that the ``address`` property is an instance of + the ``App\Entity\Address`` class, add the :doc:`Type constraint `. Options ------- -traverse -~~~~~~~~ +.. include:: /reference/constraints/_groups-option.rst.inc + +.. note:: + + Unlike other constraints, the ``Valid`` constraint does not use the ``Default`` + group. This means that it will always be applied by default, **even** if you + specify a group when calling the validator. If you want to restrict the + constraint to a subset of groups, you have to define the ``groups`` option. + +.. include:: /reference/constraints/_payload-option.rst.inc + +``traverse`` +~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``true`` -If this constraint is applied to a property that holds an array of objects, -then each object in that array will be validated only if this option is set -to ``true``. +If this constraint is applied to a ``\Traversable``, then all containing values +will be validated if this option is set to ``true``. This option is ignored on +arrays: Arrays are traversed in either case. Keys are not validated. diff --git a/reference/constraints/Week.rst b/reference/constraints/Week.rst new file mode 100644 index 00000000000..b3c1b0ca122 --- /dev/null +++ b/reference/constraints/Week.rst @@ -0,0 +1,172 @@ +Week +==== + +.. versionadded:: 7.2 + + The ``Week`` constraint was introduced in Symfony 7.2. + +Validates that a given string (or an object implementing the ``Stringable`` PHP +interface) represents a valid week number according to the `ISO-8601`_ standard +(e.g. ``2025-W01``). + +========== ======================================================================= +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Week` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\WeekValidator` +========== ======================================================================= + +Basic Usage +----------- + +If you wanted to ensure that the ``startWeek`` property of an ``OnlineCourse`` +class is between the first and the twentieth week of the year 2022, you could do +the following: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/OnlineCourse.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class OnlineCourse + { + #[Assert\Week(min: '2022-W01', max: '2022-W20')] + protected string $startWeek; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\OnlineCourse: + properties: + startWeek: + - Week: + min: '2022-W01' + max: '2022-W20' + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/OnlineCourse.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class OnlineCourse + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('startWeek', new Assert\Week( + min: '2022-W01', + max: '2022-W20', + )); + } + } + +This constraint not only checks that the value matches the week number pattern, +but it also verifies that the specified week actually exists in the calendar. +According to the ISO-8601 standard, years can have either 52 or 53 weeks. For example, +``2022-W53`` is not valid because 2022 only had 52 weeks; but ``2020-W53`` is +valid because 2020 had 53 weeks. + +Options +------- + +``min`` +~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +The minimum week number that the value must match. + +``max`` +~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +The maximum week number that the value must match. + +.. include:: /reference/constraints/_groups-option.rst.inc + +``invalidFormatMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value does not represent a valid week in the ISO 8601 format.`` + +This is the message that will be shown if the value does not match the ISO 8601 +week format. + +``invalidWeekNumberMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The week "{{ value }}" is not a valid week.`` + +This is the message that will be shown if the value does not match a valid week +number. + +You can use the following parameters in this message: + +================ ================================================== +Parameter Description +================ ================================================== +``{{ value }}`` The value that was passed to the constraint +================ ================================================== + +``tooLowMessage`` +~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The value should not be before week "{{ min }}".`` + +This is the message that will be shown if the value is lower than the minimum +week number. + +You can use the following parameters in this message: + +================ ================================================== +Parameter Description +================ ================================================== +``{{ min }}`` The minimum week number +================ ================================================== + +``tooHighMessage`` +~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The value should not be after week "{{ max }}".`` + +This is the message that will be shown if the value is higher than the maximum +week number. + +You can use the following parameters in this message: + +================ ================================================== +Parameter Description +================ ================================================== +``{{ max }}`` The maximum week number +================ ================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`ISO-8601`: https://en.wikipedia.org/wiki/ISO_8601 diff --git a/reference/constraints/When.rst b/reference/constraints/When.rst new file mode 100644 index 00000000000..6eca8b4895f --- /dev/null +++ b/reference/constraints/When.rst @@ -0,0 +1,335 @@ +When +==== + +This constraint allows you to apply constraints validation only if the +provided expression returns true. See `Basic Usage`_ for an example. + +========== =================================================================== +Applies to :ref:`class ` + or :ref:`property/method ` +Options - `expression`_ + - `constraints`_ + _ `otherwise`_ + - `groups`_ + - `payload`_ + - `values`_ +Class :class:`Symfony\\Component\\Validator\\Constraints\\When` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\WhenValidator` +========== =================================================================== + +Basic Usage +----------- + +Imagine you have a class ``Discount`` with ``type`` and ``value`` +properties:: + + // src/Model/Discount.php + namespace App\Model; + + class Discount + { + private ?string $type; + + private ?int $value; + + // ... + + public function getType(): ?string + { + return $this->type; + } + + public function getValue(): ?int + { + return $this->value; + } + } + +To validate the object, you have some requirements: + +A) If ``type`` is ``percent``, then ``value`` must be less than or equal 100; +B) If ``type`` is not ``percent``, then ``value`` must be less than 9999; +C) No matter the value of ``type``, the ``value`` must be greater than 0. + +One way to accomplish this is with the When constraint: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/Discount.php + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + + class Discount + { + #[Assert\GreaterThan(0)] + #[Assert\When( + expression: 'this.getType() == "percent"', + constraints: [ + new Assert\LessThanOrEqual(100, message: 'The value should be between 1 and 100!') + ], + otherwise: [ + new Assert\LessThan(9999, message: 'The value should be less than 9999!') + ], + )] + private ?int $value; + + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Model\Discount: + properties: + value: + - GreaterThan: 0 + - When: + expression: "this.getType() == 'percent'" + constraints: + - LessThanOrEqual: + value: 100 + message: "The value should be between 1 and 100!" + otherwise: + - LessThan: + value: 9999 + message: "The value should be less than 9999!" + + .. code-block:: xml + + + + + + + 0 + + + + + + + + + + .. code-block:: php + + // src/Model/Discount.php + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Discount + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('value', new Assert\GreaterThan(0)); + $metadata->addPropertyConstraint('value', new Assert\When( + expression: 'this.getType() == "percent"', + constraints: [ + new Assert\LessThanOrEqual( + value: 100, + message: 'The value should be between 1 and 100!', + ), + ], + otherwise: [ + new Assert\LessThan( + value: 9999, + message: 'The value should be less than 9999!', + ), + ], + )); + } + + // ... + } + +The `expression`_ option is the expression that must return true in order +to trigger the validation of the attached constraints. To learn more about +the expression language syntax, see :doc:`/reference/formats/expression_language`. + +For more information about the expression and what variables are available +to you, see the `expression`_ option details below. + +Options +------- + +``expression`` +~~~~~~~~~~~~~~ + +**type**: ``string|Closure`` + +The condition evaluated to decide if the constraint is applied or not. It can be +defined as a closure or a string using the :doc:`expression language syntax `. +If the result is a falsey value (``false``, ``null``, ``0``, an empty string or +an empty array) the constraints defined in the ``constraints`` option won't be +applied but the constraints defined in ``otherwise`` option (if provided) will be applied. + +**When using an expression**, you access to the following variables: + +``this`` + The object being validated (e.g. an instance of Discount). +``value`` + The value of the property being validated (only available when + the constraint is applied to a property). +``context`` + The :class:`Symfony\\Component\\Validator\\Context\\ExecutionContextInterface` + object that provides information such as the currently validated class, the + name of the currently validated property, the list of violations, etc. + +.. versionadded:: 7.2 + + The ``context`` variable in expressions was introduced in Symfony 7.2. + +**When using a closure**, the first argument is the object being validated. + +.. versionadded:: 7.3 + + The support for closures in the ``expression`` option was introduced in Symfony 7.3 + and requires PHP 8.5. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/Discount.php + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Context\ExecutionContextInterface; + + class Discount + { + // either using an expression... + #[Assert\When( + expression: 'value == "percent"', + constraints: [new Assert\Callback('doComplexValidation')], + )] + + // ... or using a closure + #[Assert\When( + expression: static function (Discount $discount) { + return $discount->getType() === 'percent'; + }, + constraints: [new Assert\Callback('doComplexValidation')], + )] + private ?string $type; + + // ... + + public function doComplexValidation(ExecutionContextInterface $context, $payload): void + { + // ... + } + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Model\Discount: + properties: + type: + - When: + expression: "value == 'percent'" + constraints: + - Callback: doComplexValidation + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // src/Model/Discount.php + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Discount + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('type', new Assert\When( + expression: 'value == "percent"', + constraints: [ + new Assert\Callback('doComplexValidation'), + ], + )); + } + + public function doComplexValidation(ExecutionContextInterface $context, $payload): void + { + // ... + } + } + +You can also pass custom variables using the `values`_ option. + +``constraints`` +~~~~~~~~~~~~~~~ + +**type**: ``array|Constraint`` + +One or multiple constraints that are applied if the expression returns true. + +``otherwise`` +~~~~~~~~~~~~~ + +**type**: ``array|Constraint`` + +One or multiple constraints that are applied if the expression returns false. + +.. versionadded:: 7.3 + + The ``otherwise`` option was introduced in Symfony 7.3. + +.. include:: /reference/constraints/_groups-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc + +``values`` +~~~~~~~~~~ + +**type**: ``array`` **default**: ``[]`` + +The values of the custom variables used in the expression. Values can be of any +type (numeric, boolean, strings, null, etc.) diff --git a/reference/constraints/WordCount.rst b/reference/constraints/WordCount.rst new file mode 100644 index 00000000000..392f8a5bcb7 --- /dev/null +++ b/reference/constraints/WordCount.rst @@ -0,0 +1,150 @@ +WordCount +========= + +.. versionadded:: 7.2 + + The ``WordCount`` constraint was introduced in Symfony 7.2. + +Validates that a string (or an object implementing the ``Stringable`` PHP interface) +contains a given number of words. Internally, this constraint uses the +:phpclass:`IntlBreakIterator` class to count the words depending on your locale. + +========== ======================================================================= +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\WordCount` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\WordCountValidator` +========== ======================================================================= + +Basic Usage +----------- + +If you wanted to ensure that the ``content`` property of a ``BlogPostDTO`` +class contains between 100 and 200 words, you could do the following: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/BlogPostDTO.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class BlogPostDTO + { + #[Assert\WordCount(min: 100, max: 200)] + protected string $content; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\BlogPostDTO: + properties: + content: + - WordCount: + min: 100 + max: 200 + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/BlogPostDTO.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class BlogPostDTO + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('content', new Assert\WordCount( + min: 100, + max: 200, + )); + } + } + +Options +------- + +``min`` +~~~~~~~ + +**type**: ``integer`` **default**: ``null`` + +The minimum number of words that the value must contain. + +``max`` +~~~~~~~ + +**type**: ``integer`` **default**: ``null`` + +The maximum number of words that the value must contain. + +``locale`` +~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +The locale to use for counting the words by using the :phpclass:`IntlBreakIterator` +class. The default value (``null``) means that the constraint uses the current +user locale. + +.. include:: /reference/constraints/_groups-option.rst.inc + +``minMessage`` +~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words.`` + +This is the message that will be shown if the value does not contain at least +the minimum number of words. + +You can use the following parameters in this message: + +================ ================================================== +Parameter Description +================ ================================================== +``{{ min }}`` The minimum number of words +``{{ count }}`` The actual number of words +================ ================================================== + +``maxMessage`` +~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less.`` + +This is the message that will be shown if the value contains more than the +maximum number of words. + +You can use the following parameters in this message: + +================ ================================================== +Parameter Description +================ ================================================== +``{{ max }}`` The maximum number of words +``{{ count }}`` The actual number of words +================ ================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Yaml.rst b/reference/constraints/Yaml.rst new file mode 100644 index 00000000000..0d1564f4f8a --- /dev/null +++ b/reference/constraints/Yaml.rst @@ -0,0 +1,152 @@ +Yaml +==== + +Validates that a value has valid `YAML`_ syntax. + +.. versionadded:: 7.2 + + The ``Yaml`` constraint was introduced in Symfony 7.2. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Yaml` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\YamlValidator` +========== =================================================================== + +Basic Usage +----------- + +The ``Yaml`` constraint can be applied to a property or a "getter" method: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Report.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Report + { + #[Assert\Yaml( + message: "Your configuration doesn't have valid YAML syntax." + )] + private string $customConfiguration; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Report: + properties: + customConfiguration: + - Yaml: + message: Your configuration doesn't have valid YAML syntax. + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/Report.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Report + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('customConfiguration', new Assert\Yaml( + message: 'Your configuration doesn\'t have valid YAML syntax.', + )); + } + } + +Options +------- + +``flags`` +~~~~~~~~~ + +**type**: ``integer`` **default**: ``0`` + +This option enables optional features of the YAML parser when validating contents. +Its value is a combination of one or more of the :ref:`flags defined by the Yaml component `: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Report.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Yaml\Yaml; + + class Report + { + #[Assert\Yaml( + message: "Your configuration doesn't have valid YAML syntax.", + flags: Yaml::PARSE_CONSTANT | Yaml::PARSE_CUSTOM_TAGS | Yaml::PARSE_DATETIME, + )] + private string $customConfiguration; + } + + .. code-block:: php + + // src/Entity/Report.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + use Symfony\Component\Yaml\Yaml; + + class Report + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('customConfiguration', new Assert\Yaml( + message: 'Your configuration doesn\'t have valid YAML syntax.', + flags: Yaml::PARSE_CONSTANT | Yaml::PARSE_CUSTOM_TAGS | Yaml::PARSE_DATETIME, + )); + } + } + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not valid YAML.`` + +This message shown if the underlying data is not a valid YAML value. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ error }}`` The full error message from the YAML parser +``{{ line }}`` The line where the YAML syntax error happened +=============== ============================================================== + +.. include:: /reference/constraints/_groups-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`YAML`: https://en.wikipedia.org/wiki/YAML diff --git a/reference/constraints/_comparison-propertypath-option.rst.inc b/reference/constraints/_comparison-propertypath-option.rst.inc new file mode 100644 index 00000000000..0965b3cd847 --- /dev/null +++ b/reference/constraints/_comparison-propertypath-option.rst.inc @@ -0,0 +1,17 @@ +``propertyPath`` +~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +It defines the object property whose value is used to make the comparison. + +For example, if you want to compare the ``$endDate`` property of some object +with regard to the ``$startDate`` property of the same object, use +``propertyPath="startDate"`` in the comparison constraint of ``$endDate``. + +.. tip:: + + When using this option, its value is available in error messages as the + ``{{ compared_value_path }}`` placeholder. Although it's not intended to + include it in the error messages displayed to end users, it's useful when + using APIs for doing any mapping logic on client-side. diff --git a/reference/constraints/_comparison-value-option.rst.inc b/reference/constraints/_comparison-value-option.rst.inc new file mode 100644 index 00000000000..91ab28a2e94 --- /dev/null +++ b/reference/constraints/_comparison-value-option.rst.inc @@ -0,0 +1,7 @@ +``value`` +~~~~~~~~~ + +**type**: ``mixed`` + +This option is required. It defines the comparison value. It can be a +string, number or object. diff --git a/reference/constraints/_empty-values-are-valid.rst.inc b/reference/constraints/_empty-values-are-valid.rst.inc new file mode 100644 index 00000000000..46f81cb5305 --- /dev/null +++ b/reference/constraints/_empty-values-are-valid.rst.inc @@ -0,0 +1,6 @@ +.. note:: + + As with most of the other constraints, ``null`` and empty strings are + considered valid values. This is to allow them to be optional values. + If the value is mandatory, a common solution is to combine this constraint + with :doc:`NotBlank `. diff --git a/reference/constraints/_groups-option.rst.inc b/reference/constraints/_groups-option.rst.inc new file mode 100644 index 00000000000..e69e96df72e --- /dev/null +++ b/reference/constraints/_groups-option.rst.inc @@ -0,0 +1,7 @@ +``groups`` +~~~~~~~~~~ + +**type**: ``array`` | ``string`` **default**: ``null`` + +It defines the validation group or groups of this constraint. Read more +about :doc:`validation groups `. diff --git a/reference/constraints/_normalizer-option.rst.inc b/reference/constraints/_normalizer-option.rst.inc new file mode 100644 index 00000000000..dcbba1c2da8 --- /dev/null +++ b/reference/constraints/_normalizer-option.rst.inc @@ -0,0 +1,13 @@ +``normalizer`` +~~~~~~~~~~~~~~ + +**type**: a `PHP callable`_ **default**: ``null`` + +This option allows to define the PHP callable applied to the given value before +checking if it is valid. + +For example, you may want to pass the ``'trim'`` string to apply the +:phpfunction:`trim` PHP function in order to ignore leading and trailing +whitespace during validation. + +.. _`PHP callable`: https://www.php.net/callable diff --git a/reference/constraints/_null-values-are-valid.rst.inc b/reference/constraints/_null-values-are-valid.rst.inc new file mode 100644 index 00000000000..49b6a54faad --- /dev/null +++ b/reference/constraints/_null-values-are-valid.rst.inc @@ -0,0 +1,6 @@ +.. note:: + + As with most of the other constraints, ``null`` is + considered a valid value. This is to allow the use of optional values. + If the value is mandatory, a common solution is to combine this constraint + with :doc:`NotNull `. diff --git a/reference/constraints/_parameters-mime-types-message-option.rst.inc b/reference/constraints/_parameters-mime-types-message-option.rst.inc new file mode 100644 index 00000000000..0956b77a9c1 --- /dev/null +++ b/reference/constraints/_parameters-mime-types-message-option.rst.inc @@ -0,0 +1,10 @@ +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ file }}`` Absolute file path +``{{ name }}`` Base file name +``{{ type }}`` The MIME type of the given file +``{{ types }}`` The list of allowed MIME types +=============== ============================================================== diff --git a/reference/constraints/_payload-option.rst.inc b/reference/constraints/_payload-option.rst.inc new file mode 100644 index 00000000000..5121ba1ae51 --- /dev/null +++ b/reference/constraints/_payload-option.rst.inc @@ -0,0 +1,13 @@ +``payload`` +~~~~~~~~~~~ + +**type**: ``mixed`` **default**: ``null`` + +This option can be used to attach arbitrary domain-specific data to a constraint. +The configured payload is not used by the Validator component, but its processing +is completely up to you. + +For example, you may want to use +:doc:`several error levels ` to present failed +constraints differently in the front-end depending on the severity of the +error. diff --git a/reference/constraints/map.rst.inc b/reference/constraints/map.rst.inc index 3d10d637ccf..c2396ae3af7 100644 --- a/reference/constraints/map.rst.inc +++ b/reference/constraints/map.rst.inc @@ -4,27 +4,68 @@ Basic Constraints These are the basic constraints: use them to assert very basic things about the value of properties or the return value of methods on your object. -* :doc:`NotBlank ` +.. class:: ui-list-two-columns + * :doc:`Blank ` +* :doc:`IsFalse ` +* :doc:`IsNull ` +* :doc:`IsTrue ` +* :doc:`NotBlank ` * :doc:`NotNull ` -* :doc:`Null ` -* :doc:`True ` -* :doc:`False ` * :doc:`Type ` String Constraints ~~~~~~~~~~~~~~~~~~ +.. class:: ui-list-three-columns + +* :doc:`Charset ` +* :doc:`Cidr ` +* :doc:`CssColor ` * :doc:`Email ` +* :doc:`ExpressionSyntax ` +* :doc:`Hostname ` +* :doc:`Ip ` +* :doc:`Json ` * :doc:`Length ` -* :doc:`Url ` +* :doc:`MacAddress ` +* :doc:`NoSuspiciousCharacters ` +* :doc:`NotCompromisedPassword ` +* :doc:`PasswordStrength ` * :doc:`Regex ` -* :doc:`Ip ` +* :doc:`Slug ` +* :doc:`Twig ` +* :doc:`Ulid ` +* :doc:`Url ` +* :doc:`UserPassword ` +* :doc:`Uuid ` +* :doc:`WordCount ` +* :doc:`Yaml ` + +Comparison Constraints +~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: ui-list-three-columns + +* :doc:`DivisibleBy ` +* :doc:`EqualTo ` +* :doc:`GreaterThan ` +* :doc:`GreaterThanOrEqual ` +* :doc:`IdenticalTo ` +* :doc:`LessThan ` +* :doc:`LessThanOrEqual ` +* :doc:`NotEqualTo ` +* :doc:`NotIdenticalTo ` +* :doc:`Range ` +* :doc:`Unique ` Number Constraints ~~~~~~~~~~~~~~~~~~ -* :doc:`Range ` +* :doc:`Negative ` +* :doc:`NegativeOrZero ` +* :doc:`Positive ` +* :doc:`PositiveOrZero ` Date Constraints ~~~~~~~~~~~~~~~~ @@ -32,17 +73,16 @@ Date Constraints * :doc:`Date ` * :doc:`DateTime ` * :doc:`Time ` +* :doc:`Timezone ` +* :doc:`Week ` -Collection Constraints -~~~~~~~~~~~~~~~~~~~~~~ +Choice Constraints +~~~~~~~~~~~~~~~~~~ * :doc:`Choice ` -* :doc:`Collection ` -* :doc:`Count ` -* :doc:`UniqueEntity ` +* :doc:`Country ` * :doc:`Language ` * :doc:`Locale ` -* :doc:`Country ` File Constraints ~~~~~~~~~~~~~~~~ @@ -50,16 +90,42 @@ File Constraints * :doc:`File ` * :doc:`Image ` -Financial Constraints -~~~~~~~~~~~~~~~~~~~~~ +Financial and other Number Constraints +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: ui-list-two-columns +* :doc:`Bic ` * :doc:`CardScheme ` +* :doc:`Currency ` +* :doc:`Iban ` +* :doc:`Isbn ` +* :doc:`Isin ` +* :doc:`Issn ` * :doc:`Luhn ` +Doctrine Constraints +~~~~~~~~~~~~~~~~~~~~ + +* :doc:`DisableAutoMapping ` +* :doc:`EnableAutoMapping ` +* :doc:`UniqueEntity ` + Other Constraints ~~~~~~~~~~~~~~~~~ -* :doc:`Callback ` +.. class:: ui-list-three-columns + * :doc:`All ` -* :doc:`UserPassword ` +* :doc:`AtLeastOneOf ` +* :doc:`Callback ` +* :doc:`Cascade ` +* :doc:`Collection ` +* :doc:`Compound ` +* :doc:`Count ` +* :doc:`Expression ` +* :doc:`GroupSequence ` +* :doc:`Sequentially ` +* :doc:`Traverse ` * :doc:`Valid ` +* :doc:`When ` diff --git a/reference/dic_tags.rst b/reference/dic_tags.rst index 8da679a0d5a..866aac5774f 100644 --- a/reference/dic_tags.rst +++ b/reference/dic_tags.rst @@ -1,223 +1,581 @@ -The Dependency Injection Tags +Built-in Symfony Service Tags ============================= -Dependency Injection Tags are little strings that can be applied to a service -to "flag" it to be used in some special way. For example, if you have a service -that you would like to register as a listener to one of Symfony's core events, -you can flag it with the ``kernel.event_listener`` tag. - -You can learn a little bit more about "tags" by reading the ":ref:`book-service-container-tags`" -section of the Service Container chapter. - -Below is information about all of the tags available inside Symfony2. There -may also be tags in other bundles you use that aren't listed here. For example, -the AsseticBundle has several tags that aren't listed here. - -+-----------------------------------+---------------------------------------------------------------------------+ -| Tag Name | Usage | -+-----------------------------------+---------------------------------------------------------------------------+ -| `data_collector`_ | Create a class that collects custom data for the profiler | -+-----------------------------------+---------------------------------------------------------------------------+ -| `form.type`_ | Create a custom form field type | -+-----------------------------------+---------------------------------------------------------------------------+ -| `form.type_extension`_ | Create a custom "form extension" | -+-----------------------------------+---------------------------------------------------------------------------+ -| `form.type_guesser`_ | Add your own logic for "form type guessing" | -+-----------------------------------+---------------------------------------------------------------------------+ -| `kernel.cache_warmer`_ | Register your service to be called during the cache warming process | -+-----------------------------------+---------------------------------------------------------------------------+ -| `kernel.event_listener`_ | Listen to different events/hooks in Symfony | -+-----------------------------------+---------------------------------------------------------------------------+ -| `kernel.event_subscriber`_ | To subscribe to a set of different events/hooks in Symfony | -+-----------------------------------+---------------------------------------------------------------------------+ -| `kernel.fragment_renderer`_ | Add new HTTP content rendering strategies | -+-----------------------------------+---------------------------------------------------------------------------+ -| `monolog.logger`_ | Logging with a custom logging channel | -+-----------------------------------+---------------------------------------------------------------------------+ -| `monolog.processor`_ | Add a custom processor for logging | -+-----------------------------------+---------------------------------------------------------------------------+ -| `routing.loader`_ | Register a custom service that loads routes | -+-----------------------------------+---------------------------------------------------------------------------+ -| `security.voter`_ | Add a custom voter to Symfony's authorization logic | -+-----------------------------------+---------------------------------------------------------------------------+ -| `security.remember_me_aware`_ | To allow remember me authentication | -+-----------------------------------+---------------------------------------------------------------------------+ -| `security.listener.factory`_ | Necessary when creating a custom authentication system | -+-----------------------------------+---------------------------------------------------------------------------+ -| `serializer.encoder`_ | Register a new encoder in the ``serializer`` service | -+-----------------------------------+---------------------------------------------------------------------------+ -| `serializer.normalizer`_ | Register a new normalizer in the ``serializer`` service | -+-----------------------------------+---------------------------------------------------------------------------+ -| `swiftmailer.plugin`_ | Register a custom SwiftMailer Plugin | -+-----------------------------------+---------------------------------------------------------------------------+ -| `templating.helper`_ | Make your service available in PHP templates | -+-----------------------------------+---------------------------------------------------------------------------+ -| `translation.loader`_ | Register a custom service that loads translations | -+-----------------------------------+---------------------------------------------------------------------------+ -| `twig.extension`_ | Register a custom Twig Extension | -+-----------------------------------+---------------------------------------------------------------------------+ -| `twig.loader`_ | Register a custom service that loads Twig templates | -+-----------------------------------+---------------------------------------------------------------------------+ -| `validator.constraint_validator`_ | Create your own custom validation constraint | -+-----------------------------------+---------------------------------------------------------------------------+ -| `validator.initializer`_ | Register a service that initializes objects before validation | -+-----------------------------------+---------------------------------------------------------------------------+ +:doc:`Service tags ` are the mechanism used by the +:doc:`DependencyInjection component ` to flag +services that require special processing, like console commands or Twig extensions. -data_collector +This article shows the most common tags provided by Symfony components, but in +your application there could be more tags available provided by third-party bundles. + +Run this command to display tagged services in your application: + +.. code-block:: terminal + + $ php bin/console debug:container --tags + +To search for a specific tag, re-run this command with a search term: + +.. code-block:: terminal + + $ php bin/console debug:container --tag=form.type + +assets.package -------------- -**Purpose**: Create a class that collects custom data for the profiler +**Purpose**: Add an asset package to the application -For details on creating your own custom data collection, read the cookbook -article: :doc:`/cookbook/profiler/data_collector`. +This is an alternative way to declare an :ref:`asset package `. +The name of the package is set in this order: -.. _dic-tags-form-type: +* first, the ``package`` attribute of the tag; +* then, the value returned by the static method ``getDefaultPackageName()`` if defined; +* finally, the service name. -form.type ---------- +.. configuration-block:: -**Purpose**: Create a custom form field type + .. code-block:: yaml -For details on creating your own custom form type, read the cookbook article: -:doc:`/cookbook/form/create_custom_field_type`. + services: + App\Assets\AvatarPackage: + tags: + - { name: assets.package, package: avatars } -form.type_extension -------------------- + .. code-block:: xml -**Purpose**: Create a custom "form extension" + + + + + + + + + -Form type extensions are a way for you took "hook into" the creation of any -field in your form. For example, the addition of the CSRF token is done via -a form type extension (:class:`Symfony\\Component\\Form\\Extension\\Csrf\\Type\\FormTypeCsrfExtension`). + .. code-block:: php + + use App\Assets\AvatarPackage; -A form type extension can modify any part of any field in your form. To create -a form type extension, first create a class that implements the -:class:`Symfony\\Component\\Form\\FormTypeExtensionInterface` interface. -For simplicity, you'll often extend an -:class:`Symfony\\Component\\Form\\AbstractTypeExtension` class instead of -the interface directly:: + $container + ->register(AvatarPackage::class) + ->addTag('assets.package', ['package' => 'avatars']) + ; - // src/Acme/MainBundle/Form/Type/MyFormTypeExtension.php - namespace Acme\MainBundle\Form\Type; +Now you can use the ``avatars`` package in your templates: - use Symfony\Component\Form\AbstractTypeExtension; +.. code-block:: html+twig - class MyFormTypeExtension extends AbstractTypeExtension - { - // ... fill in whatever methods you want to override - // like buildForm(), buildView(), finishView(), setDefaultOptions() - } + -In order for Symfony to know about your form extension and use it, give it -the `form.type_extension` tag: +auto_alias +---------- + +**Purpose**: Define aliases based on the value of container parameters + +Consider the following configuration that defines three different but related +services: .. configuration-block:: .. code-block:: yaml services: - main.form.type.my_form_type_extension: - class: Acme\MainBundle\Form\Type\MyFormTypeExtension + app.mysql_lock: + class: App\Lock\MysqlLock + app.postgresql_lock: + class: App\Lock\PostgresqlLock + app.sqlite_lock: + class: App\Lock\SqliteLock + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Lock\MysqlLock; + use App\Lock\PostgresqlLock; + use App\Lock\SqliteLock; + + return function(ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set('app.mysql_lock', MysqlLock::class); + $services->set('app.postgresql_lock', PostgresqlLock::class); + $services->set('app.sqlite_lock', SqliteLock::class); + }; + +Instead of dealing with these three services, your application needs a generic +``app.lock`` service that will be an alias to one of these services, depending on +some configuration. Thanks to the ``auto_alias`` option, you can automatically create +that alias based on the value of a configuration parameter. + +Considering that a configuration parameter called ``database_type`` exists. Then, +the generic ``app.lock`` service can be defined as follows: + +.. configuration-block:: + + .. code-block:: yaml + + services: + app.mysql_lock: + # ... + app.postgresql_lock: + # ... + app.sqlite_lock: + # ... + app.lock: tags: - - { name: form.type_extension, alias: field } + - { name: auto_alias, format: "app.%database_type%_lock" } .. code-block:: xml - - - + + + + + + + + + + + + + .. code-block:: php + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Lock\MysqlLock; + use App\Lock\PostgresqlLock; + use App\Lock\SqliteLock; + + return function(ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set('app.mysql_lock', MysqlLock::class); + $services->set('app.postgresql_lock', PostgresqlLock::class); + $services->set('app.sqlite_lock', SqliteLock::class); + + $services->set('app.lock') + ->tag('auto_alias', ['format' => 'app.%database_type%_lock']) + ; + }; + +The ``format`` option defines the expression used to construct the name of the service +to alias. This expression can use any container parameter (as usual, +wrapping their names with ``%`` characters). + +.. note:: + + When using the ``auto_alias`` tag, it's not mandatory to define the aliased + services as private. However, doing that (like in the above example) makes + sense most of the times to prevent accessing those services directly instead + of using the generic service alias. + +console.command +--------------- + +**Purpose**: Add a command to the application + +For details on registering your own commands in the service container, read +:doc:`/console/commands_as_services`. + +container.hot_path +------------------ + +**Purpose**: Add to list of always needed services + +This tag identifies the services that are always needed. It is only applied to +a very short list of bootstrapping services (like ``router``, ``event_dispatcher``, +``http_kernel``, ``request_stack``, etc.). Then, it is propagated to all dependencies +of these services, with a special case for event listeners, where only listed events +are propagated to their related listeners. + +It will replace, in cache for generated service factories, the PHP autoload by +plain inlined ``include_once``. The benefit is a complete bypass of the autoloader +for services and their class hierarchy. The result is a significant performance improvement. + +Use this tag with great caution, you have to be sure that the tagged service is always used. + +.. _dic-tags-container-nopreload: + +container.no_preload +-------------------- + +**Purpose**: Remove a class from the list of classes preloaded by PHP + +Add this tag to a service and its class won't be preloaded when using +`PHP class preloading`_: + +.. configuration-block:: + + .. code-block:: yaml + + services: + App\SomeNamespace\SomeService: + tags: ['container.no_preload'] + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + use App\SomeNamespace\SomeService; + $container - ->register('main.form.type.my_form_type_extension', 'Acme\MainBundle\Form\Type\MyFormTypeExtension') - ->addTag('form.type_extension', array('alias' => 'field')) + ->register(SomeService::class) + ->addTag('container.no_preload') ; -The ``alias`` key of the tag is the type of field that this extension should -be applied to. For example, to apply the extension to any "field", use the -"field" value. +If you add some service tagged with ``container.no_preload`` as an argument of +another service, the ``container.no_preload`` tag is applied automatically to +that service too. + +.. _dic-tags-container-preload: + +container.preload +----------------- + +**Purpose**: Add some class to the list of classes preloaded by PHP + +When using `PHP class preloading`_, this tag allows you to define which PHP +classes should be preloaded. This can improve performance by making some of the +classes used by your service always available for all requests (until the server +is restarted): + +.. configuration-block:: + + .. code-block:: yaml + + services: + App\SomeNamespace\SomeService: + tags: + - { name: 'container.preload', class: 'App\SomeClass' } + - { name: 'container.preload', class: 'App\Some\OtherClass' } + # ... + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + use App\Some\OtherClass; + use App\SomeClass; + use App\SomeNamespace\SomeService; + + $container + ->register(SomeService::class) + ->addTag('container.preload', ['class' => SomeClass::class]) + ->addTag('container.preload', ['class' => OtherClass::class]) + // ... + ; + +controller.argument_value_resolver +---------------------------------- + +**Purpose**: Register a value resolver for controller arguments such as ``Request`` + +Value resolvers implement the +:class:`Symfony\\Component\\HttpKernel\\Controller\\ValueResolverInterface` +and are used to resolve argument values for controllers as described here: +:doc:`/controller/value_resolver`. + +data_collector +-------------- + +**Purpose**: Create a class that collects custom data for the profiler + +For details on creating your own custom data collection, read the +:ref:`profiler-data-collector` article. + +doctrine.event_listener +----------------------- + +**Purpose**: Add a Doctrine event listener + +For details on creating Doctrine event listeners, read the +:doc:`Doctrine events ` article. + +doctrine.event_subscriber +------------------------- + +**Purpose**: Add a Doctrine event subscriber + +For details on creating Doctrine event subscribers, read the +:doc:`Doctrine events ` article. + +.. _dic-tags-form-type: + +form.type +--------- + +**Purpose**: Create a custom form field type + +For details on creating your own custom form type, read the +:doc:`/form/create_custom_field_type` article. + +form.type_extension +------------------- + +**Purpose**: Create a custom "form extension" + +For details on creating Form type extensions, read the +:doc:`/form/create_form_type_extension` article. + +.. _reference-dic-type_guesser: form.type_guesser ----------------- **Purpose**: Add your own logic for "form type guessing" -This tag allows you to add your own logic to the :ref:`Form Guessing` +This tag allows you to add your own logic to the :ref:`form guessing ` process. By default, form guessing is done by "guessers" based on the validation -metadata and Doctrine metadata (if you're using Doctrine). +metadata and Doctrine metadata (if you're using Doctrine) or Propel metadata +(if you're using Propel). + +.. seealso:: + + For information on how to create your own type guesser, see + :doc:`/form/type_guesser`. + +kernel.cache_clearer +-------------------- + +**Purpose**: Register your service to be called during the cache clearing +process + +Cache clearing occurs whenever you call ``cache:clear`` command. If your +bundle caches files, you should add a custom cache clearer for clearing those +files during the cache clearing process. + +In order to register your custom cache clearer, first you must create a +service class:: -To add your own form type guesser, create a class that implements the -:class:`Symfony\\Component\\Form\\FormTypeGuesserInterface` interface. Next, -tag its service definition with ``form.type_guesser`` (it has no options). + // src/Cache/MyClearer.php + namespace App\Cache; -To see an example of how this class might look, see the ``ValidatorTypeGuesser`` -class in the ``Form`` component. + use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface; + + class MyClearer implements CacheClearerInterface + { + public function clear(string $cacheDirectory): void + { + // clear your cache + } + } + +If you're using the :ref:`default services.yaml configuration `, +your service will be automatically tagged with ``kernel.cache_clearer``. But, you +can also register it manually: + +.. configuration-block:: + + .. code-block:: yaml + + services: + App\Cache\MyClearer: + tags: [kernel.cache_clearer] + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + use App\Cache\MyClearer; + + $container + ->register(MyClearer::class) + ->addTag('kernel.cache_clearer') + ; kernel.cache_warmer ------------------- -**Purpose**: Register your service to be called during the cache warming process +**Purpose**: Register your service to be called during the cache warming +process Cache warming occurs whenever you run the ``cache:warmup`` or ``cache:clear`` -task (unless you pass ``--no-warmup`` to ``cache:clear``). The purpose is -to initialize any cache that will be needed by the application and prevent -the first user from any significant "cache hit" where the cache is generated -dynamically. +command (unless you pass ``--no-warmup`` to ``cache:clear``). It is also run +when handling the request, if it wasn't done by one of the commands yet. + +The purpose is to initialize any cache that will be needed by the application +and prevent the first user from any significant "cache hit" where the cache +is generated dynamically. To register your own cache warmer, first create a service that implements the :class:`Symfony\\Component\\HttpKernel\\CacheWarmer\\CacheWarmerInterface` interface:: - // src/Acme/MainBundle/Cache/MyCustomWarmer.php - namespace Acme\MainBundle\Cache; + // src/Cache/MyCustomWarmer.php + namespace App\Cache; + use App\Foo\Bar; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; class MyCustomWarmer implements CacheWarmerInterface { - public function warmUp($cacheDir) + public function warmUp(string $cacheDir, ?string $buildDir = null): array { - // do some sort of operations to "warm" your cache + // ... do some sort of operations to "warm" your cache + + $filesAndClassesToPreload = []; + $filesAndClassesToPreload[] = Bar::class; + + foreach (scandir($someCacheDir) as $file) { + if (!is_dir($file = $someCacheDir.'/'.$file)) { + $filesAndClassesToPreload[] = $file; + } + } + + return $filesAndClassesToPreload; } - public function isOptional() + public function isOptional(): bool { return true; } } -The ``isOptional`` method should return true if it's possible to use the -application without calling this cache warmer. In Symfony 2.0, optional warmers -are always executed anyways, so this function has no real effect. +The ``warmUp()`` method must return an array with the files and classes to +preload. Files must be absolute paths and classes must be fully-qualified class +names. The only restriction is that files must be stored in the cache directory. +If you don't need to preload anything, return an empty array. If read-only +artifacts need to be created, you can store them in a different directory +with the ``$buildDir`` parameter of the ``warmUp()`` method. + +The ``isOptional()`` method should return true if it's possible to use the +application without calling this cache warmer. In Symfony, optional warmers +are always executed by default (you can change this by using the +``--no-optional-warmers`` option when executing the command). -To register your warmer with Symfony, give it the kernel.cache_warmer tag: +If you're using the :ref:`default services.yaml configuration `, +your service will be automatically tagged with ``kernel.cache_warmer``. But, you +can also register it manually: .. configuration-block:: .. code-block:: yaml services: - main.warmer.my_custom_warmer: - class: Acme\MainBundle\Cache\MyCustomWarmer + App\Cache\MyCustomWarmer: tags: - { name: kernel.cache_warmer, priority: 0 } .. code-block:: xml - - - + + + + + + + + + .. code-block:: php + use App\Cache\MyCustomWarmer; + $container - ->register('main.warmer.my_custom_warmer', 'Acme\MainBundle\Cache\MyCustomWarmer') - ->addTag('kernel.cache_warmer', array('priority' => 0)) + ->register(MyCustomWarmer::class) + ->addTag('kernel.cache_warmer', ['priority' => 0]) ; -The ``priority`` value is optional, and defaults to 0. This value can be -from -255 to 255, and the warmers will be executed in the order of their -priority. +.. note:: + + The ``priority`` is optional and its value is a positive or negative integer + that defaults to ``0``. The higher the number, the earlier that warmers are + executed. + +.. warning:: + + If your cache warmer fails its execution because of any exception, Symfony + won't try to execute it again for the next requests. Therefore, your + application and/or bundles should be prepared for when the contents + generated by the cache warmer are not available. + +.. _core-cache-warmers: + +In addition to your own cache warmers, Symfony components and third-party +bundles define cache warmers too for their own purposes. You can list them all +with the following command: + +.. code-block:: terminal + + $ php bin/console debug:container --tag=kernel.cache_warmer .. _dic-tags-kernel-event-listener: @@ -226,95 +584,18 @@ kernel.event_listener **Purpose**: To listen to different events/hooks in Symfony -This tag allows you to hook your own classes into Symfony's process at different -points. - -For a full example of this listener, read the :doc:`/cookbook/service_container/event_listener` -cookbook entry. +During the execution of a Symfony application, different events are triggered +and you can also dispatch custom events. This tag allows you to *hook* your own +classes into any of those events. -For another practical example of a kernel listener, see the cookbook -article: :doc:`/cookbook/request/mime_type`. +For a full example of this listener, read the :doc:`/event_dispatcher` +article. Core Event Listener Reference ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -When adding your own listeners, it might be useful to know about the other -core Symfony listeners and their priorities. - -.. note:: - - All listeners listed here may not be listening depending on your environment, - settings and bundles. Additionally, third-party bundles will bring in - additional listener not listed here. - -kernel.request -.............. - -+-------------------------------------------------------------------------------------------+-----------+ -| Listener Class Name | Priority | -+-------------------------------------------------------------------------------------------+-----------+ -| :class:`Symfony\\Component\\HttpKernel\\EventListener\\ProfilerListener` | 1024 | -+-------------------------------------------------------------------------------------------+-----------+ -| :class:`Symfony\\Bundle\\FrameworkBundle\\EventListener\\TestSessionListener` | 192 | -+-------------------------------------------------------------------------------------------+-----------+ -| :class:`Symfony\\Bundle\\FrameworkBundle\\EventListener\\SessionListener` | 128 | -+-------------------------------------------------------------------------------------------+-----------+ -| :class:`Symfony\\Component\\HttpKernel\\EventListener\\RouterListener` | 32 | -+-------------------------------------------------------------------------------------------+-----------+ -| :class:`Symfony\\Component\\HttpKernel\\EventListener\\LocaleListener` | 16 | -+-------------------------------------------------------------------------------------------+-----------+ -| :class:`Symfony\\Component\\Security\\Http\\Firewall` | 8 | -+-------------------------------------------------------------------------------------------+-----------+ - -kernel.controller -................. - -+-------------------------------------------------------------------------------------------+----------+ -| Listener Class Name | Priority | -+-------------------------------------------------------------------------------------------+----------+ -| :class:`Symfony\\Bundle\\FrameworkBundle\\DataCollector\\RequestDataCollector` | 0 | -+-------------------------------------------------------------------------------------------+----------+ - -kernel.response -............... - -+-------------------------------------------------------------------------------------------+----------+ -| Listener Class Name | Priority | -+-------------------------------------------------------------------------------------------+----------+ -| :class:`Symfony\\Component\\HttpKernel\\EventListener\\EsiListener` | 0 | -+-------------------------------------------------------------------------------------------+----------+ -| :class:`Symfony\\Component\\HttpKernel\\EventListener\\ResponseListener` | 0 | -+-------------------------------------------------------------------------------------------+----------+ -| :class:`Symfony\\Bundle\\SecurityBundle\\EventListener\\ResponseListener` | 0 | -+-------------------------------------------------------------------------------------------+----------+ -| :class:`Symfony\\Component\\HttpKernel\\EventListener\\ProfilerListener` | -100 | -+-------------------------------------------------------------------------------------------+----------+ -| :class:`Symfony\\Bundle\\FrameworkBundle\\EventListener\\TestSessionListener` | -128 | -+-------------------------------------------------------------------------------------------+----------+ -| :class:`Symfony\\Bundle\\WebProfilerBundle\\EventListener\\WebDebugToolbarListener` | -128 | -+-------------------------------------------------------------------------------------------+----------+ -| :class:`Symfony\\Component\\HttpKernel\\EventListener\\StreamedResponseListener` | -1024 | -+-------------------------------------------------------------------------------------------+----------+ - -kernel.exception -................ - -+-------------------------------------------------------------------------------------------+----------+ -| Listener Class Name | Priority | -+-------------------------------------------------------------------------------------------+----------+ -| :class:`Symfony\\Component\\HttpKernel\\EventListener\\ProfilerListener` | 0 | -+-------------------------------------------------------------------------------------------+----------+ -| :class:`Symfony\\Component\\HttpKernel\\EventListener\\ExceptionListener` | -128 | -+-------------------------------------------------------------------------------------------+----------+ - -kernel.terminate -................ - -+-------------------------------------------------------------------------------------------+----------+ -| Listener Class Name | Priority | -+-------------------------------------------------------------------------------------------+----------+ -| :class:`Symfony\\Bundle\\SwiftmailerBundle\\EventListener\\EmailSenderListener` | 0 | -+-------------------------------------------------------------------------------------------+----------+ +For the reference of Event Listeners associated with each kernel event, +see the :doc:`Symfony Events Reference `. .. _dic-tags-kernel-event-subscriber: @@ -323,54 +604,111 @@ kernel.event_subscriber **Purpose**: To subscribe to a set of different events/hooks in Symfony -.. versionadded:: 2.1 - The ability to add kernel event subscribers is new to 2.1. +This is an alternative way to create an event listener, and is the recommended +way (instead of using ``kernel.event_listener``). See :ref:`events-subscriber`. + +kernel.fragment_renderer +------------------------ + +**Purpose**: Add a new HTTP content rendering strategy + +To add a new rendering strategy - in addition to the core strategies like +``EsiFragmentRenderer`` - create a class that implements +:class:`Symfony\\Component\\HttpKernel\\Fragment\\FragmentRendererInterface`, +register it as a service, then tag it with ``kernel.fragment_renderer``. + +kernel.locale_aware +------------------- + +**Purpose**: To access and use the current :ref:`locale ` + +Setting and retrieving the locale can be done via configuration or using +container parameters, listeners, route parameters or the current request. -To enable a custom subscriber, add it as a regular service in one of your -configuration, and tag it with ``kernel.event_subscriber``: +Thanks to the ``Translation`` contract, the locale can be set via services. + +To register your own locale aware service, first create a service that implements +the :class:`Symfony\\Contracts\\Translation\\LocaleAwareInterface` interface:: + + // src/Locale/MyCustomLocaleHandler.php + namespace App\Locale; + + use Symfony\Contracts\Translation\LocaleAwareInterface; + + class MyCustomLocaleHandler implements LocaleAwareInterface + { + public function setLocale(string $locale): void + { + $this->locale = $locale; + } + + public function getLocale(): string + { + return $this->locale; + } + } + +If you're using the :ref:`default services.yaml configuration `, +your service will be automatically tagged with ``kernel.locale_aware``. But, you +can also register it manually: .. configuration-block:: .. code-block:: yaml services: - kernel.subscriber.your_subscriber_name: - class: Fully\Qualified\Subscriber\Class\Name - tags: - - { name: kernel.event_subscriber } + App\Locale\MyCustomLocaleHandler: + tags: [kernel.locale_aware] .. code-block:: xml - - - + + + + + + + + + .. code-block:: php + use App\Locale\MyCustomLocaleHandler; + $container - ->register('kernel.subscriber.your_subscriber_name', 'Fully\Qualified\Subscriber\Class\Name') - ->addTag('kernel.event_subscriber') + ->register(LocaleHandler::class) + ->addTag('kernel.locale_aware') ; -.. note:: +kernel.reset +------------ - Your service must implement the :class:`Symfony\\Component\\EventDispatcher\\EventSubscriberInterface` - interface. +**Purpose**: Clean up services between requests -.. note:: +In all main requests (not :ref:`sub-requests `) except +the first one, Symfony looks for any service tagged with the ``kernel.reset`` tag +to reinitialize their state. This is done by calling to the method whose name is +configured in the ``method`` argument of the tag. - If your service is created by a factory, you **MUST** correctly set the ``class`` - parameter for this tag to work correctly. +This is mostly useful when running your projects in application servers that +reuse the Symfony application between requests to improve performance. This tag +is applied for example to the built-in :ref:`data collectors ` +of the profiler to delete all their information. -kernel.fragment_renderer ------------------------- +.. _dic_tags-mime: -**Purpose**: Add a new HTTP content rendering strategy. +mime.mime_type_guesser +---------------------- -To add a new rendering strategy - in addition to the core strategies like -``EsiFragmentRenderer`` - create a class that implements -:class:`Symfony\\Component\\HttpKernel\\Fragment\\FragmentRendererInterface`, -register it as a service, then tag it with ``kernel.fragment_renderer``. +**Purpose**: Add your own logic for guessing MIME types + +This tag is used to register your own :ref:`MIME type guessers ` +in case the guessers provided by the :doc:`Mime component ` +don't fit your needs. .. _dic_tags-monolog: @@ -388,29 +726,40 @@ channel when injecting the logger in a service. .. code-block:: yaml services: - my_service: - class: Fully\Qualified\Loader\Class\Name - arguments: ["@logger"] + App\Log\CustomLogger: + arguments: ['@logger'] tags: - - { name: monolog.logger, channel: acme } + - { name: monolog.logger, channel: app } .. code-block:: xml - - - - + + + + + + + + + + .. code-block:: php - $definition = new Definition('Fully\Qualified\Loader\Class\Name', array(new Reference('logger')); - $definition->addTag('monolog.logger', array('channel' => 'acme')); - $container->register('my_service', $definition); + use App\Log\CustomLogger; + use Symfony\Component\DependencyInjection\Reference; -.. note:: + $container->register(CustomLogger::class) + ->addArgument(new Reference('logger')) + ->addTag('monolog.logger', ['channel' => 'app']); + +.. tip:: - This works only when the logger service is a constructor argument, - not when it is injected through a setter. + You can create :doc:`custom channels ` and + even :ref:`autowire logging channels `. .. _dic_tags-monolog-processor: @@ -419,13 +768,13 @@ monolog.processor **Purpose**: Add a custom processor for logging -Monolog allows you to add processors in the logger or in the handlers to add -extra data in the records. A processor receives the record as an argument and -must return it after adding some extra data in the ``extra`` attribute of -the record. +Monolog allows you to add processors in the logger or in the handlers to +add extra data in the records. A processor receives the record as an argument +and must return it after adding some extra data in the ``extra`` attribute +of the record. -Let's see how you can use the built-in ``IntrospectionProcessor`` to add -the file, the line, the class and the method where the logger was triggered. +The built-in ``IntrospectionProcessor`` can be used to add the file, the +line, the class and the method where the logger was triggered. You can add a processor globally: @@ -434,26 +783,36 @@ You can add a processor globally: .. code-block:: yaml services: - my_service: - class: Monolog\Processor\IntrospectionProcessor - tags: - - { name: monolog.processor } + Monolog\Processor\IntrospectionProcessor: + tags: [monolog.processor] .. code-block:: xml - - - + + + + + + + + + .. code-block:: php - $definition = new Definition('Monolog\Processor\IntrospectionProcessor'); - $definition->addTag('monolog.processor'); - $container->register('my_service', $definition); + use Monolog\Processor\IntrospectionProcessor; + + $container + ->register(IntrospectionProcessor::class) + ->addTag('monolog.processor') + ; .. tip:: - If your service is not a callable (using ``__invoke``) you can add the + If your service is not a callable (using ``__invoke()``) you can add the ``method`` attribute in the tag to use a specific method. You can add also a processor for a specific handler by using the ``handler`` @@ -464,48 +823,70 @@ attribute: .. code-block:: yaml services: - my_service: - class: Monolog\Processor\IntrospectionProcessor + Monolog\Processor\IntrospectionProcessor: tags: - { name: monolog.processor, handler: firephp } .. code-block:: xml - - - + + + + + + + + + .. code-block:: php - $definition = new Definition('Monolog\Processor\IntrospectionProcessor'); - $definition->addTag('monolog.processor', array('handler' => 'firephp'); - $container->register('my_service', $definition); + use Monolog\Processor\IntrospectionProcessor; + + $container + ->register(IntrospectionProcessor::class) + ->addTag('monolog.processor', ['handler' => 'firephp']) + ; -You can also add a processor for a specific logging channel by using the ``channel`` -attribute. This will register the processor only for the ``security`` logging -channel used in the Security component: +You can also add a processor for a specific logging channel by using the +``channel`` attribute. This will register the processor only for the +``security`` logging channel used in the Security component: .. configuration-block:: .. code-block:: yaml services: - my_service: - class: Monolog\Processor\IntrospectionProcessor + Monolog\Processor\IntrospectionProcessor: tags: - { name: monolog.processor, channel: security } .. code-block:: xml - - - + + + + + + + + + .. code-block:: php - $definition = new Definition('Monolog\Processor\IntrospectionProcessor'); - $definition->addTag('monolog.processor', array('channel' => 'security'); - $container->register('my_service', $definition); + use Monolog\Processor\IntrospectionProcessor; + + $container + ->register(IntrospectionProcessor::class) + ->addTag('monolog.processor', ['channel' => 'security']) + ; .. note:: @@ -518,67 +899,72 @@ routing.loader **Purpose**: Register a custom service that loads routes To enable a custom routing loader, add it as a regular service in one -of your configuration, and tag it with ``routing.loader``: +of your configuration and tag it with ``routing.loader``: .. configuration-block:: .. code-block:: yaml services: - routing.loader.your_loader_name: - class: Fully\Qualified\Loader\Class\Name - tags: - - { name: routing.loader } + App\Routing\CustomLoader: + tags: [routing.loader] .. code-block:: xml - - - + + + + + + + + + .. code-block:: php + use App\Routing\CustomLoader; + $container - ->register('routing.loader.your_loader_name', 'Fully\Qualified\Loader\Class\Name') + ->register(CustomLoader::class) ->addTag('routing.loader') ; -For more information, see :doc:`/cookbook/routing/custom_route_loader`. +For more information, see :doc:`/routing/custom_route_loader`. -security.listener.factory -------------------------- - -**Purpose**: Necessary when creating a custom authentication system +routing.expression_language_provider +------------------------------------ -This tag is used when creating your own custom authentication system. For -details, see :doc:`/cookbook/security/custom_authentication_provider`. +**Purpose**: Register a provider for expression language functions in routing -security.remember_me_aware --------------------------- +This tag is used to automatically register +:ref:`expression function providers ` +for the routing expression component. Using these providers, you can add custom +functions to the routing expression language. -**Purpose**: To allow remember me authentication +security.expression_language_provider +------------------------------------- -This tag is used internally to allow remember-me authentication to work. If -you have a custom authentication method where a user can be remember-me authenticated, -then you may need to use this tag. +**Purpose**: Register a provider for expression language functions in security -If your custom authentication factory extends -:class:`Symfony\\Bundle\\SecurityBundle\\DependencyInjection\\Security\\Factory\\AbstractFactory` -and your custom authentication listener extends -:class:`Symfony\\Component\\Security\\Http\\Firewall\\AbstractAuthenticationListener`, -then your custom authentication listener will automatically have this tagged -applied and it will function automatically. +This tag is used to automatically register :ref:`expression function providers +` for the security expression +component. Using these providers, you can add custom functions to the security +expression language. security.voter -------------- **Purpose**: To add a custom voter to Symfony's authorization logic -When you call ``isGranted`` on Symfony's security context, a system of "voters" +When you call ``isGranted()`` on Symfony's authorization checker, a system of "voters" is used behind the scenes to determine if the user should have access. The ``security.voter`` tag allows you to add your own custom voter to that system. -For more information, read the cookbook article: :doc:`/cookbook/security/voters`. +For more information, read the :doc:`/security/voters` article. .. _reference-dic-tags-serializer-encoder: @@ -590,7 +976,7 @@ serializer.encoder The class that's tagged should implement the :class:`Symfony\\Component\\Serializer\\Encoder\\EncoderInterface` and :class:`Symfony\\Component\\Serializer\\Encoder\\DecoderInterface`. -For more details, see :doc:`/cookbook/serializer`. +For more details, see :doc:`/serializer`. .. _reference-dic-tags-serializer-normalizer: @@ -602,128 +988,256 @@ serializer.normalizer The class that's tagged should implement the :class:`Symfony\\Component\\Serializer\\Normalizer\\NormalizerInterface` and :class:`Symfony\\Component\\Serializer\\Normalizer\\DenormalizerInterface`. -For more details, see :doc:`/cookbook/serializer`. +For more details, see :doc:`/serializer`. -swiftmailer.plugin ------------------- +Run the following command to check the priorities of the default normalizers: -**Purpose**: Register a custom SwiftMailer Plugin +.. code-block:: terminal -If you're using a custom SwiftMailer plugin (or want to create one), you can -register it with SwiftMailer by creating a service for your plugin and tagging -it with ``swiftmailer.plugin`` (it has no options). + $ php bin/console debug:container --tag serializer.normalizer -A SwiftMailer plugin must implement the ``Swift_Events_EventListener`` interface. -For more information on plugins, see `SwiftMailer's Plugin Documentation`_. +.. _dic-tags-translation-loader: -Several SwiftMailer plugins are core to Symfony and can be activated via -different configuration. For details, see :doc:`/reference/configuration/swiftmailer`. +translation.loader +------------------ -templating.helper ------------------ +**Purpose**: To register a custom service that loads translations -**Purpose**: Make your service available in PHP templates +By default, translations are loaded from the filesystem in a variety of +different formats (YAML, XLIFF, PHP, etc). -To enable a custom template helper, add it as a regular service in one -of your configuration, tag it with ``templating.helper`` and define an -``alias`` attribute (the helper will be accessible via this alias in the -templates): +Now, register your loader as a service and tag it with ``translation.loader``: .. configuration-block:: .. code-block:: yaml services: - templating.helper.your_helper_name: - class: Fully\Qualified\Helper\Class\Name + App\Translation\MyCustomLoader: tags: - - { name: templating.helper, alias: alias_name } + - { name: translation.loader, alias: bin } .. code-block:: xml - - - + + + + + + + + + .. code-block:: php + use App\Translation\MyCustomLoader; + $container - ->register('templating.helper.your_helper_name', 'Fully\Qualified\Helper\Class\Name') - ->addTag('templating.helper', array('alias' => 'alias_name')) + ->register(MyCustomLoader::class) + ->addTag('translation.loader', ['alias' => 'bin']) ; -translation.loader ------------------- +The ``alias`` option is required and very important: it defines the file +"suffix" that will be used for the resource files that use this loader. +For example, suppose you have some custom ``bin`` format that you need to +load. If you have a ``bin`` file that contains French translations for +the ``messages`` domain, then you might have a file ``translations/messages.fr.bin``. -**Purpose**: To register a custom service that loads translations +When Symfony tries to load the ``bin`` file, it passes the path to your +custom loader as the ``$resource`` argument. You can then perform any logic +you need on that file in order to load your translations. + +If you're loading translations from a database, you'll still need a resource +file, but it might either be blank or contain a little bit of information +about loading those resources from the database. The file is key to trigger +the ``load()`` method on your custom loader. -By default, translations are loaded form the filesystem in a variety of different -formats (YAML, XLIFF, PHP, etc). If you need to load translations from some -other source, first create a class that implements the -:class:`Symfony\\Component\\Translation\\Loader\\LoaderInterface` interface:: +.. _reference-dic-tags-translation-extractor: + +translation.extractor +--------------------- - // src/Acme/MainBundle/Translation/MyCustomLoader.php - namespace Acme\MainBundle\Translation; +**Purpose**: To register a custom service that extracts messages from a +file - use Symfony\Component\Translation\Loader\LoaderInterface; +When executing the ``translation:extract`` command, it uses extractors to +extract translation messages from a file. By default, the Symfony Framework +has a :class:`Symfony\\Bridge\\Twig\\Translation\\TwigExtractor` to find and +extract translation keys from Twig templates. + +If you also want to find and extract translation keys from PHP files, install +the following dependency to activate the :class:`Symfony\\Component\\Translation\\Extractor\\PhpAstExtractor`: + +.. code-block:: terminal + + $ composer require nikic/php-parser + +You can create your own extractor by creating a class that implements +:class:`Symfony\\Component\\Translation\\Extractor\\ExtractorInterface` +and tagging the service with ``translation.extractor``. The tag has one +required option: ``alias``, which defines the name of the extractor:: + + // src/Acme/DemoBundle/Translation/FooExtractor.php + namespace Acme\DemoBundle\Translation; + + use Symfony\Component\Translation\Extractor\ExtractorInterface; use Symfony\Component\Translation\MessageCatalogue; - class MyCustomLoader implements LoaderInterface + class FooExtractor implements ExtractorInterface { - public function load($resource, $locale, $domain = 'messages') - { - $catalogue = new MessageCatalogue($locale); + protected string $prefix; - // some how load up some translations from the "resource" - // then set them into the catalogue - $catalogue->set('hello.world', 'Hello World!', $domain); + /** + * Extracts translation messages from a template directory to the catalog. + */ + public function extract(string $directory, MessageCatalogue $catalog): void + { + // ... + } - return $catalogue; + /** + * Sets the prefix that should be used for new found messages. + */ + public function setPrefix(string $prefix): void + { + $this->prefix = $prefix; } } -Your custom loader's ``load`` method is responsible for returning a -:Class:`Symfony\\Component\\Translation\\MessageCatalogue`. +.. configuration-block:: + + .. code-block:: yaml -Now, register your loader as a service and tag it with ``translation.loader``: + services: + App\Translation\CustomExtractor: + tags: + - { name: translation.extractor, alias: foo } + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + use App\Translation\CustomExtractor; + + $container->register(CustomExtractor::class) + ->addTag('translation.extractor', ['alias' => 'foo']); + +translation.dumper +------------------ + +**Purpose**: To register a custom service that dumps messages to a file + +After a :ref:`translation extractor ` +has extracted all messages from the templates, the dumpers are executed to dump +the messages to a translation file in a specific format. + +Symfony already comes with many dumpers: + +* :class:`Symfony\\Component\\Translation\\Dumper\\CsvFileDumper` +* :class:`Symfony\\Component\\Translation\\Dumper\\IcuResFileDumper` +* :class:`Symfony\\Component\\Translation\\Dumper\\IniFileDumper` +* :class:`Symfony\\Component\\Translation\\Dumper\\MoFileDumper` +* :class:`Symfony\\Component\\Translation\\Dumper\\PoFileDumper` +* :class:`Symfony\\Component\\Translation\\Dumper\\QtFileDumper` +* :class:`Symfony\\Component\\Translation\\Dumper\\XliffFileDumper` +* :class:`Symfony\\Component\\Translation\\Dumper\\YamlFileDumper` + +You can create your own dumper by extending +:class:`Symfony\\Component\\Translation\\Dumper\\FileDumper` or implementing +:class:`Symfony\\Component\\Translation\\Dumper\\DumperInterface` and tagging +the service with ``translation.dumper``. The tag has one option: ``alias`` +This is the name that's used to determine which dumper should be used. .. configuration-block:: .. code-block:: yaml services: - main.translation.my_custom_loader: - class: Acme\MainBundle\Translation\MyCustomLoader + App\Translation\JsonFileDumper: tags: - - { name: translation.loader, alias: bin } + - { name: translation.dumper, alias: json } .. code-block:: xml - - - + + + + + + + + + .. code-block:: php - $container - ->register('main.translation.my_custom_loader', 'Acme\MainBundle\Translation\MyCustomLoader') - ->addTag('translation.loader', array('alias' => 'bin')) - ; + use App\Translation\JsonFileDumper; -The ``alias`` option is required and very important: it defines the file -"suffix" that will be used for the resource files that use this loader. For -example, suppose you have some custom ``bin`` format that you need to load. -If you have a ``bin`` file that contains French translations for the ``messages`` -domain, then you might have a file ``app/Resources/translations/messages.fr.bin``. + $container->register(JsonFileDumper::class) + ->addTag('translation.dumper', ['alias' => 'json']); -When Symfony tries to load the ``bin`` file, it passes the path to your custom -loader as the ``$resource`` argument. You can then perform any logic you need -on that file in order to load your translations. +.. _reference-dic-tags-translation-provider-factory: -If you're loading translations from a database, you'll still need a resource -file, but it might either be blank or contain a little bit of information -about loading those resources from the database. The file is key to trigger -the ``load`` method on your custom loader. +translation.provider_factory +---------------------------- + +**Purpose**: to register a factory related to custom translation providers + +When creating custom :ref:`translation providers `, you +must register your factory as a service and tag it with ``translation.provider_factory``: + +.. configuration-block:: + + .. code-block:: yaml + + services: + App\Translation\CustomProviderFactory: + tags: + - { name: translation.provider_factory } + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + use App\Translation\CustomProviderFactory; + + $container + ->register(CustomProviderFactory::class) + ->addTag('translation.provider_factory') + ; .. _reference-dic-tags-twig-extension: @@ -733,95 +1247,154 @@ twig.extension **Purpose**: To register a custom Twig Extension To enable a Twig extension, add it as a regular service in one of your -configuration, and tag it with ``twig.extension``: +configuration and tag it with ``twig.extension``. If you're using the +:ref:`default services.yaml configuration `, +the service is auto-registered and auto-tagged. But, you can also register it manually: .. configuration-block:: .. code-block:: yaml services: - twig.extension.your_extension_name: - class: Fully\Qualified\Extension\Class\Name - tags: - - { name: twig.extension } + App\Twig\AppExtension: + tags: [twig.extension] + + # optionally you can define the priority of the extension (default = 0). + # Extensions with higher priorities are registered earlier. This is mostly + # useful to register late extensions that override other extensions. + App\Twig\AnotherExtension: + tags: [{ name: twig.extension, priority: -100 }] .. code-block:: xml - - - + + + + + + + + + + + + + .. code-block:: php + use App\Twig\AnotherExtension; + use App\Twig\AppExtension; + $container - ->register('twig.extension.your_extension_name', 'Fully\Qualified\Extension\Class\Name') + ->register(AppExtension::class) ->addTag('twig.extension') ; + $container + ->register(AnotherExtension::class) + ->addTag('twig.extension', ['priority' => -100]) + ; For information on how to create the actual Twig Extension class, see -`Twig's documentation`_ on the topic or read the cookbook article: -:doc:`/cookbook/templating/twig_extension` +`Twig's documentation`_ on the topic or read the +:ref:`templates-twig-extension` article. -Before writing your own extensions, have a look at the -`Twig official extension repository`_ which already includes several -useful extensions. For example ``Intl`` and its ``localizeddate`` filter -that formats a date according to user's locale. These official Twig extensions -also have to be added as regular services: +twig.loader +----------- + +**Purpose**: Register a custom service that loads Twig templates + +By default, Symfony uses only one `Twig Loader`_ - `FilesystemLoader`_. If you need +to load Twig templates from another resource, you can create a service for +the new loader and tag it with ``twig.loader``. + +If you use the :ref:`default services.yaml configuration `, +the service will be automatically tagged thanks to autoconfiguration. But, you can +also register it manually: .. configuration-block:: .. code-block:: yaml services: - twig.extension.intl: - class: Twig_Extensions_Extension_Intl + App\Twig\CustomLoader: tags: - - { name: twig.extension } + - { name: twig.loader, priority: 0 } .. code-block:: xml - - - + + + + + + + + + .. code-block:: php + use App\Twig\CustomLoader; + $container - ->register('twig.extension.intl', 'Twig_Extensions_Extension_Intl') - ->addTag('twig.extension') + ->register(CustomLoader::class) + ->addTag('twig.loader', ['priority' => 0]) ; -twig.loader ------------ +.. note:: -**Purpose**: Register a custom service that loads Twig templates + The ``priority`` is optional and its value is a positive or negative integer + that defaults to ``0``. Loaders with higher numbers are tried first. -By default, Symfony uses only one `Twig Loader`_ - -:class:`Symfony\\Bundle\\TwigBundle\\Loader\\FilesystemLoader`. If you need -to load Twig templates from another resource, you can create a service for -the new loader and tag it with ``twig.loader``: +.. _reference-dic-tags-twig-runtime: + +twig.runtime +------------ + +**Purpose**: To register a custom Lazy-Loaded Twig Extension + +:ref:`Lazy-Loaded Twig Extensions ` are defined as +regular services but they need to be tagged with ``twig.runtime``. If you're using the +:ref:`default services.yaml configuration `, +the service is auto-registered and auto-tagged. But, you can also register it manually: .. configuration-block:: .. code-block:: yaml services: - acme.demo_bundle.loader.some_twig_loader: - class: Acme\DemoBundle\Loader\SomeTwigLoader - tags: - - { name: twig.loader } + App\Twig\AppExtension: + tags: [twig.runtime] .. code-block:: xml - - - + + + + + + + + + .. code-block:: php + use App\Twig\AppExtension; + $container - ->register('acme.demo_bundle.loader.some_twig_loader', 'Acme\DemoBundle\Loader\SomeTwigLoader') - ->addTag('twig.loader') + ->register(AppExtension::class) + ->addTag('twig.runtime') ; validator.constraint_validator @@ -830,7 +1403,7 @@ validator.constraint_validator **Purpose**: Create your own custom validation constraint This tag allows you to create and register your own custom validation constraint. -For more information, read the cookbook article: :doc:`/cookbook/validation/custom_constraint`. +For more information, read the :doc:`/validation/custom_constraint` article. validator.initializer --------------------- @@ -841,17 +1414,17 @@ This tag provides a very uncommon piece of functionality that allows you to perform some sort of action on an object right before it's validated. For example, it's used by Doctrine to query for all of the lazily-loaded data on an object before it's validated. Without this, some data on a Doctrine -entity would appear to be "missing" when validated, even though this is not -really the case. +entity would appear to be "missing" when validated, even though this is +not really the case. If you do need to use this tag, just make a new class that implements the :class:`Symfony\\Component\\Validator\\ObjectInitializerInterface` interface. Then, tag it with the ``validator.initializer`` tag (it has no options). -For an example, see the ``EntityInitializer`` class inside the Doctrine Bridge. +For an example, see the ``DoctrineInitializer`` class inside the Doctrine +Bridge. -.. _`Twig's documentation`: http://twig.sensiolabs.org/doc/advanced.html#creating-an-extension -.. _`Twig official extension repository`: https://github.com/fabpot/Twig-extensions -.. _`KernelEvents`: https://github.com/symfony/symfony/blob/2.2/src/Symfony/Component/HttpKernel/KernelEvents.php -.. _`SwiftMailer's Plugin Documentation`: http://swiftmailer.org/docs/plugins.html -.. _`Twig Loader`: http://twig.sensiolabs.org/doc/api.html#loaders +.. _`FilesystemLoader`: https://github.com/twigphp/Twig/blob/3.x/src/Loader/FilesystemLoader.php +.. _`Twig's documentation`: https://twig.symfony.com/doc/3.x/advanced.html#creating-an-extension +.. _`Twig Loader`: https://twig.symfony.com/doc/3.x/api.html#loaders +.. _`PHP class preloading`: https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.preload diff --git a/reference/events.rst b/reference/events.rst new file mode 100644 index 00000000000..57806ee8f8d --- /dev/null +++ b/reference/events.rst @@ -0,0 +1,298 @@ +Built-in Symfony Events +======================= + +The Symfony framework is an HTTP Request-Response one. +During the handling of an HTTP request, the framework (or any +application using the :doc:`HttpKernel component `) +dispatches some :doc:`events ` which you can use to modify +how the request is handled and how the response is returned. + +Kernel Events +------------- + +Each event dispatched by the HttpKernel component is a subclass of +:class:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent`, which provides the +following information: + +:method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::getRequestType` + Returns the *type* of the request (``HttpKernelInterface::MAIN_REQUEST`` + or ``HttpKernelInterface::SUB_REQUEST``). + +:method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::getKernel` + Returns the Kernel handling the request. + +:method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::getRequest` + Returns the current ``Request`` being handled. + +:method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::isMainRequest` + Checks if this is a main request. + +.. _kernel-core-request: + +``kernel.request`` +~~~~~~~~~~~~~~~~~~ + +**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\RequestEvent` + +This event is dispatched very early in Symfony, before the controller is +determined. It's useful to add information to the Request or return a Response +early to stop the handling of the request. + +.. seealso:: + + Read more on the :ref:`kernel.request event `. + +Execute this command to find out which listeners are registered for this event and +their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher kernel.request + +``kernel.controller`` +~~~~~~~~~~~~~~~~~~~~~ + +**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\ControllerEvent` + +This event is dispatched after the controller has been resolved but before executing +it. It's useful to initialize things later needed by the +controller, such as :ref:`value resolvers `, and +even to change the controller entirely:: + + use Symfony\Component\HttpKernel\Event\ControllerEvent; + + public function onKernelController(ControllerEvent $event): void + { + // ... + + // the controller can be changed to any PHP callable + $event->setController($myCustomController); + } + +.. seealso:: + + Read more on the :ref:`kernel.controller event `. + +Execute this command to find out which listeners are registered for this event and +their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher kernel.controller + +``kernel.controller_arguments`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\ControllerArgumentsEvent` + +This event is dispatched just before a controller is called. It's useful to +configure the arguments that are going to be passed to the controller. +Typically, this is used to map URL routing parameters to their corresponding +named arguments; or pass the current request when the ``Request`` type-hint is +found:: + + use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; + + public function onKernelControllerArguments(ControllerArgumentsEvent $event): void + { + // ... + + // get controller and request arguments + $namedArguments = $event->getRequest()->attributes->all(); + $controllerArguments = $event->getArguments(); + + // set the controller arguments to modify the original arguments or add new ones + $event->setArguments($newArguments); + } + +Execute this command to find out which listeners are registered for this event and +their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher kernel.controller_arguments + +``kernel.view`` +~~~~~~~~~~~~~~~ + +**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\ViewEvent` + +This event is dispatched after the controller has been executed but *only* if +the controller does *not* return a :class:`Symfony\\Component\\HttpFoundation\\Response` +object. It's useful to transform the returned value (e.g. a string with some +HTML contents) into the ``Response`` object needed by Symfony:: + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Event\ViewEvent; + + public function onKernelView(ViewEvent $event): void + { + $value = $event->getControllerResult(); + $response = new Response(); + + // ... somehow customize the Response from the return value + + $event->setResponse($response); + } + +.. seealso:: + + Read more on the :ref:`kernel.view event `. + +Execute this command to find out which listeners are registered for this event and +their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher kernel.view + +``kernel.response`` +~~~~~~~~~~~~~~~~~~~ + +**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\ResponseEvent` + +This event is dispatched after the controller or any ``kernel.view`` listener +returns a ``Response`` object. It's useful to modify or replace the response +before sending it back (e.g. add/modify HTTP headers, add cookies, etc.):: + + use Symfony\Component\HttpKernel\Event\ResponseEvent; + + public function onKernelResponse(ResponseEvent $event): void + { + $response = $event->getResponse(); + + // ... modify the response object + } + +.. seealso:: + + Read more on the :ref:`kernel.response event `. + +Execute this command to find out which listeners are registered for this event and +their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher kernel.response + +``kernel.finish_request`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\FinishRequestEvent` + +This event is dispatched after the ``kernel.response`` event. It's useful to reset +the global state of the application (for example, the translator listener resets +the translator's locale to the one of the parent request):: + + use Symfony\Component\HttpKernel\Event\FinishRequestEvent; + + public function onKernelFinishRequest(FinishRequestEvent $event): void + { + if (null === $parentRequest = $this->requestStack->getParentRequest()) { + return; + } + + // reset the locale of the subrequest to the locale of the parent request + $this->setLocale($parentRequest); + } + +Execute this command to find out which listeners are registered for this event and +their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher kernel.finish_request + +``kernel.terminate`` +~~~~~~~~~~~~~~~~~~~~ + +**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\TerminateEvent` + +This event is dispatched after the response has been sent (after the execution +of the :method:`Symfony\\Component\\HttpKernel\\HttpKernel::handle` method). +It's useful to perform slow or complex tasks that don't need to be completed to +send the response (e.g. sending emails). + +.. seealso:: + + Read more on the :ref:`kernel.terminate event `. + +Execute this command to find out which listeners are registered for this event and +their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher kernel.terminate + +.. _kernel-kernel.exception: + +``kernel.exception`` +~~~~~~~~~~~~~~~~~~~~ + +**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` + +This event is dispatched as soon as an error occurs during the handling of the +HTTP request. It's useful to recover from errors or modify the exception details +sent as response:: + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Event\ExceptionEvent; + + public function onKernelException(ExceptionEvent $event): void + { + $exception = $event->getThrowable(); + $response = new Response(); + // setup the Response object based on the caught exception + $event->setResponse($response); + + // you can alternatively set a new Exception + // $exception = new \Exception('Some special exception'); + // $event->setThrowable($exception); + } + +.. note:: + + The TwigBundle registers an :class:`Symfony\\Component\\HttpKernel\\EventListener\\ErrorListener` + that forwards the ``Request`` to a given controller defined by the + ``exception_listener.controller`` parameter. + +Symfony uses the following logic to determine the HTTP status code of the +response: + +* If :method:`Symfony\\Component\\HttpFoundation\\Response::isClientError`, + :method:`Symfony\\Component\\HttpFoundation\\Response::isServerError` or + :method:`Symfony\\Component\\HttpFoundation\\Response::isRedirect` is true, + then the status code on your ``Response`` object is used; + +* If the original exception implements + :class:`Symfony\\Component\\HttpKernel\\Exception\\HttpExceptionInterface`, + then ``getStatusCode()`` is called on the exception and used (the headers + from ``getHeaders()`` are also added); + +* If both of the above aren't true, then a 500 status code is used. + +.. note:: + + If you want to overwrite the status code of the exception response, which + you should not without a good reason, call + ``ExceptionEvent::allowCustomResponseCode()`` first and then + set the status code on the response:: + + $event->allowCustomResponseCode(); + $response = new Response('No Content', 204); + $event->setResponse($response); + + The status code sent to the client in the above example will be ``204``. If + ``$event->allowCustomResponseCode()`` is omitted, then the kernel will set + an appropriate status code based on the type of exception thrown. + +.. seealso:: + + Read more on the :ref:`kernel.exception event `. + +Execute this command to find out which listeners are registered for this event and +their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher kernel.exception diff --git a/reference/formats/expression_language.rst b/reference/formats/expression_language.rst new file mode 100644 index 00000000000..dfed9c74398 --- /dev/null +++ b/reference/formats/expression_language.rst @@ -0,0 +1,511 @@ +The Expression Syntax +===================== + +The :doc:`ExpressionLanguage component ` uses a +specific syntax which is based on the expression syntax of Twig. In this document, +you can find all supported syntaxes. + +Supported Literals +------------------ + +The component supports: + +* **strings** - single and double quotes (e.g. ``'hello'``) +* **numbers** - integers (e.g. ``103``), decimals (e.g. ``9.95``), decimals + without leading zeros (e.g. ``.99``, equivalent to ``0.99``); all numbers + support optional underscores as separators to improve readability (e.g. + ``1_000_000``, ``3.14159_26535``) +* **arrays** - using JSON-like notation (e.g. ``[1, 2]``) +* **hashes** - using JSON-like notation (e.g. ``{ foo: 'bar' }``) +* **booleans** - ``true`` and ``false`` +* **null** - ``null`` +* **exponential** - also known as scientific (e.g. ``1.99E+3`` or ``1e-2``) +* **comments** - using ``/*`` and ``*/`` (e.g. ``/* this is a comment */``) + +.. versionadded:: 7.2 + + The support for comments inside expressions was introduced in Symfony 7.2. + +.. warning:: + + A backslash (``\``) must be escaped by 3 backslashes (``\\\\``) in a string + and 7 backslashes (``\\\\\\\\``) in a regex:: + + echo $expressionLanguage->evaluate('"\\\\"'); // prints \ + $expressionLanguage->evaluate('"a\\\\b" matches "/^a\\\\\\\\b$/"'); // returns true + + Control characters (e.g. ``\n``) in expressions are replaced with + whitespace. To avoid this, escape the sequence with a single backslash + (e.g. ``\\n``). + +.. _component-expression-objects: + +Working with Objects +-------------------- + +When passing objects into an expression, you can use different syntaxes to +access properties and call methods on the object. + +Accessing Public Properties +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Public properties on objects can be accessed by using the ``.`` syntax, similar +to JavaScript:: + + class Apple + { + public string $variety; + } + + $apple = new Apple(); + $apple->variety = 'Honeycrisp'; + + var_dump($expressionLanguage->evaluate( + 'fruit.variety', + [ + 'fruit' => $apple, + ] + )); + +This will print out ``Honeycrisp``. + +Calling Methods +~~~~~~~~~~~~~~~ + +The ``.`` syntax can also be used to call methods on an object, similar to +JavaScript:: + + class Robot + { + public function sayHi(int $times): string + { + $greetings = []; + for ($i = 0; $i < $times; $i++) { + $greetings[] = 'Hi'; + } + + return implode(' ', $greetings).'!'; + } + } + + $robot = new Robot(); + + var_dump($expressionLanguage->evaluate( + 'robot.sayHi(3)', + [ + 'robot' => $robot, + ] + )); + +This will print out ``Hi Hi Hi!``. + +.. _component-expression-null-safe-operator: + +Null-safe Operator +.................. + +Use the ``?.`` syntax to access properties and methods of objects that can be +``null`` (this is equivalent to the ``$object?->propertyOrMethod`` PHP null-safe +operator):: + + // these will throw an exception when `fruit` is `null` + $expressionLanguage->evaluate('fruit.color', ['fruit' => '...']) + $expressionLanguage->evaluate('fruit.getStock()', ['fruit' => '...']) + + // these will return `null` if `fruit` is `null` + $expressionLanguage->evaluate('fruit?.color', ['fruit' => '...']) + $expressionLanguage->evaluate('fruit?.getStock()', ['fruit' => '...']) + +.. _component-expression-null-coalescing-operator: + +Null-Coalescing Operator +........................ + +It returns the left-hand side if it exists and it's not ``null``; otherwise it +returns the right-hand side. Expressions can chain multiple coalescing operators: + +* ``foo ?? 'no'`` +* ``foo.baz ?? 'no'`` +* ``foo[3] ?? 'no'`` +* ``foo.baz ?? foo['baz'] ?? 'no'`` + +.. versionadded:: 7.2 + + Starting from Symfony 7.2, no exception is thrown when trying to access a + non-existent variable. This is the same behavior as the `null-coalescing operator in PHP`_. + +.. _component-expression-functions: + +Working with Functions +---------------------- + +You can also use registered functions in the expression by using the same +syntax as PHP and JavaScript. The ExpressionLanguage component comes with the +following functions by default: + +* ``constant()`` +* ``enum()`` +* ``min()`` +* ``max()`` + +``constant()`` function +~~~~~~~~~~~~~~~~~~~~~~~ + +This function will return the value of a PHP constant:: + + define('DB_USER', 'root'); + + var_dump($expressionLanguage->evaluate( + 'constant("DB_USER")' + )); + +This will print out ``root``. + +This also works with class constants:: + + namespace App\SomeNamespace; + + class Foo + { + public const API_ENDPOINT = '/api'; + } + + var_dump($expressionLanguage->evaluate( + 'constant("App\\\SomeNamespace\\\Foo::API_ENDPOINT")' + )); + +This will print out ``/api``. + +``enum()`` function +~~~~~~~~~~~~~~~~~~~ + +This function will return the case of an enumeration:: + + namespace App\SomeNamespace; + + enum Foo + { + case Bar; + } + + var_dump(App\Enum\Foo::Bar === $expressionLanguage->evaluate( + 'enum("App\\\SomeNamespace\\\Foo::Bar")' + )); + +This will print out ``true``. + +``min()`` function +~~~~~~~~~~~~~~~~~~ + +This function will return the lowest value of the given parameters. You can pass +different types of parameters (e.g. dates, strings, numeric values) and even mix +them (e.g. pass numeric values and strings). Internally it uses the :phpfunction:`min` +PHP function to find the lowest value:: + + var_dump($expressionLanguage->evaluate( + 'min(1, 2, 3)' + )); + +This will print out ``1``. + +``max()`` function +~~~~~~~~~~~~~~~~~~ + +This function will return the highest value of the given parameters. You can pass +different types of parameters (e.g. dates, strings, numeric values) and even mix +them (e.g. pass numeric values and strings). Internally it uses the :phpfunction:`max` +PHP function to find the highest value:: + + var_dump($expressionLanguage->evaluate( + 'max(1, 2, 3)' + )); + +This will print out ``3``. + +.. versionadded:: 7.1 + + The ``min()`` and ``max()`` functions were introduced in Symfony 7.1. + +.. tip:: + + To read how to register your own functions to use in an expression, see + ":ref:`expression-language-extending`". + +.. _component-expression-arrays: + +Working with Arrays +------------------- + +If you pass an array into an expression, use the ``[]`` syntax to access +array keys, similar to JavaScript:: + + $data = ['life' => 10, 'universe' => 10, 'everything' => 22]; + + var_dump($expressionLanguage->evaluate( + 'data["life"] + data["universe"] + data["everything"]', + [ + 'data' => $data, + ] + )); + +This will print out ``42``. + +Supported Operators +------------------- + +The component comes with a lot of operators: + +Arithmetic Operators +~~~~~~~~~~~~~~~~~~~~ + +* ``+`` (addition) +* ``-`` (subtraction) +* ``*`` (multiplication) +* ``/`` (division) +* ``%`` (modulus) +* ``**`` (pow) + +For example:: + + var_dump($expressionLanguage->evaluate( + 'life + universe + everything', + [ + 'life' => 10, + 'universe' => 10, + 'everything' => 22, + ] + )); + +This will print out ``42``. + +Bitwise Operators +~~~~~~~~~~~~~~~~~ + +* ``&`` (and) +* ``|`` (or) +* ``^`` (xor) +* ``~`` (not) +* ``<<`` (left shift) +* ``>>`` (right shift) + +.. versionadded:: 7.2 + + Support for the ``~``, ``<<`` and ``>>`` bitwise operators was introduced + in Symfony 7.2. + +Comparison Operators +~~~~~~~~~~~~~~~~~~~~ + +* ``==`` (equal) +* ``===`` (identical) +* ``!=`` (not equal) +* ``!==`` (not identical) +* ``<`` (less than) +* ``>`` (greater than) +* ``<=`` (less than or equal to) +* ``>=`` (greater than or equal to) +* ``matches`` (regex match) +* ``contains`` +* ``starts with`` +* ``ends with`` + +.. tip:: + + To test if a string does *not* match a regex, use the logical ``not`` + operator in combination with the ``matches`` operator:: + + $expressionLanguage->evaluate('not ("foo" matches "/bar/")'); // returns true + + You must use parentheses because the unary operator ``not`` has precedence + over the binary operator ``matches``. + +Examples:: + + $ret1 = $expressionLanguage->evaluate( + 'life == everything', + [ + 'life' => 10, + 'everything' => 22, + ] + ); + + $ret2 = $expressionLanguage->evaluate( + 'life > everything', + [ + 'life' => 10, + 'everything' => 22, + ] + ); + +Both variables would be set to ``false``. + +Logical Operators +~~~~~~~~~~~~~~~~~ + +* ``not`` or ``!`` +* ``and`` or ``&&`` +* ``or`` or ``||`` +* ``xor`` + +.. versionadded:: 7.2 + + Support for the ``xor`` logical operator was introduced in Symfony 7.2. + +For example:: + + $ret = $expressionLanguage->evaluate( + 'life < universe or life < everything', + [ + 'life' => 10, + 'universe' => 10, + 'everything' => 22, + ] + ); + +This ``$ret`` variable will be set to ``true``. + +String Operators +~~~~~~~~~~~~~~~~ + +* ``~`` (concatenation) + +For example:: + + var_dump($expressionLanguage->evaluate( + 'firstName~" "~lastName', + [ + 'firstName' => 'Arthur', + 'lastName' => 'Dent', + ] + )); + +This would print out ``Arthur Dent``. + +Array Operators +~~~~~~~~~~~~~~~ + +* ``in`` (contain) +* ``not in`` (does not contain) + +These operators are using strict comparison. For example:: + + class User + { + public string $group; + } + + $user = new User(); + $user->group = 'human_resources'; + + $inGroup = $expressionLanguage->evaluate( + 'user.group in ["human_resources", "marketing"]', + [ + 'user' => $user, + ] + ); + +The ``$inGroup`` would evaluate to ``true``. + +.. note:: + + The ``in`` and ``not in`` operators are using strict comparison. + +Numeric Operators +~~~~~~~~~~~~~~~~~ + +* ``..`` (range) + +For example:: + + class User + { + public int $age; + } + + $user = new User(); + $user->age = 34; + + $expressionLanguage->evaluate( + 'user.age in 18..45', + [ + 'user' => $user, + ] + ); + +This will evaluate to ``true``, because ``user.age`` is in the range from +``18`` to ``45``. + +Ternary Operators +~~~~~~~~~~~~~~~~~ + +* ``foo ? 'yes' : 'no'`` +* ``foo ?: 'no'`` (equal to ``foo ? foo : 'no'``) +* ``foo ? 'yes'`` (equal to ``foo ? 'yes' : ''``) + +Other Operators +~~~~~~~~~~~~~~~ + +* ``?.`` (:ref:`null-safe operator `) +* ``??`` (:ref:`null-coalescing operator `) + +Operators Precedence +~~~~~~~~~~~~~~~~~~~~ + +Operator precedence determines the order in which operations are processed in an +expression. For example, the result of the expression ``1 + 2 * 4`` is ``9`` +and not ``12`` because the multiplication operator (``*``) takes precedence over +the addition operator (``+``). + +To avoid ambiguities (or to alter the default order of operations) add +parentheses in your expressions (e.g. ``(1 + 2) * 4`` or ``1 + (2 * 4)``. + +The following table summarizes the operators and their associativity from the +**highest to the lowest precedence**: + ++-----------------------------------------------------------------+---------------+ +| Operators | Associativity | ++=================================================================+===============+ +| ``-`` , ``+``, ``~`` (unary operators that add the number sign) | none | ++-----------------------------------------------------------------+---------------+ +| ``**`` | right | ++-----------------------------------------------------------------+---------------+ +| ``*``, ``/``, ``%`` | left | ++-----------------------------------------------------------------+---------------+ +| ``not``, ``!`` | none | ++-----------------------------------------------------------------+---------------+ +| ``~`` | left | ++-----------------------------------------------------------------+---------------+ +| ``+``, ``-`` | left | ++-----------------------------------------------------------------+---------------+ +| ``..``, ``<<``, ``>>`` | left | ++-----------------------------------------------------------------+---------------+ +| ``==``, ``===``, ``!=``, ``!==``, | left | +| ``<``, ``>``, ``>=``, ``<=``, | | +| ``not in``, ``in``, ``contains``, | | +| ``starts with``, ``ends with``, ``matches`` | | ++-----------------------------------------------------------------+---------------+ +| ``&`` | left | ++-----------------------------------------------------------------+---------------+ +| ``^`` | left | ++-----------------------------------------------------------------+---------------+ +| ``|`` | left | ++-----------------------------------------------------------------+---------------+ +| ``and``, ``&&`` | left | ++-----------------------------------------------------------------+---------------+ +| ``xor`` | left | ++-----------------------------------------------------------------+---------------+ +| ``or``, ``||`` | left | ++-----------------------------------------------------------------+---------------+ + +Built-in Objects and Variables +------------------------------ + +When using this component inside a Symfony application, certain objects and +variables are automatically injected by Symfony so you can use them in your +expressions (e.g. the request, the current user, etc.): + +* :doc:`Variables available in security expressions `; +* :doc:`Variables available in service container expressions `; +* :ref:`Variables available in routing expressions `. + +.. _`null-coalescing operator in PHP`: https://www.php.net/manual/en/language.operators.comparison.php#language.operators.comparison.coalesce diff --git a/reference/formats/message_format.rst b/reference/formats/message_format.rst new file mode 100644 index 00000000000..fb0143228c1 --- /dev/null +++ b/reference/formats/message_format.rst @@ -0,0 +1,505 @@ +How to Translate Messages using the ICU MessageFormat +===================================================== + +Messages (i.e. strings) in applications are almost never completely static. +They contain variables or other complex logic like pluralization. To +handle this, the :doc:`Translator component ` supports the +`ICU MessageFormat`_ syntax. + +.. tip:: + + You can test out examples of the ICU MessageFormatter in this `online editor`_. + +Using the ICU Message Format +---------------------------- + +In order to use the ICU Message Format, the message domain has to be +suffixed with ``+intl-icu``: + +====================== =============================== +Normal file name ICU Message Format filename +====================== =============================== +``messages.en.yaml`` ``messages+intl-icu.en.yaml`` +``messages.fr_FR.xlf`` ``messages+intl-icu.fr_FR.xlf`` +``admin.en.yaml`` ``admin+intl-icu.en.yaml`` +====================== =============================== + +All messages in this file will now be processed by the +:phpclass:`MessageFormatter` during translation. + +.. _component-translation-placeholders: + +Message Placeholders +-------------------- + +The basic usage of the MessageFormat allows you to use placeholders (called +*arguments* in ICU MessageFormat) in your messages: + +.. configuration-block:: + + .. code-block:: yaml + + # translations/messages+intl-icu.en.yaml + say_hello: 'Hello {name}!' + + .. code-block:: xml + + + + + + + + say_hello + Hello {name}! + + + + + + .. code-block:: php + + // translations/messages+intl-icu.en.php + return [ + 'say_hello' => "Hello {name}!", + ]; + +.. warning:: + + In the previous translation format, placeholders were often wrapped in ``%`` + (e.g. ``%name%``). This ``%`` character is no longer valid with the ICU + MessageFormat syntax, so you must rename your parameters if you are upgrading + from the previous format. + +Everything within the curly braces (``{...}``) is processed by the formatter +and replaced by its placeholder:: + + // prints "Hello Fabien!" + echo $translator->trans('say_hello', ['name' => 'Fabien']); + + // prints "Hello Symfony!" + echo $translator->trans('say_hello', ['name' => 'Symfony']); + +Selecting Different Messages Based on a Condition +------------------------------------------------- + +The curly brace syntax allows to "modify" the output of the variable. One of +these functions is the ``select`` function. It acts like PHP's `switch statement`_ +and allows you to use different strings based on the value of the variable. A +typical usage of this is gender: + +.. configuration-block:: + + .. code-block:: yaml + + # translations/messages+intl-icu.en.yaml + + # the 'other' key is required, and is selected if no other case matches + invitation_title: >- + {organizer_gender, select, + female {{organizer_name} has invited you to her party!} + male {{organizer_name} has invited you to his party!} + multiple {{organizer_name} have invited you to their party!} + other {{organizer_name} has invited you to their party!} + } + + .. code-block:: xml + + + + + + + + invitation_title + + {organizer_gender, select, + female {{organizer_name} has invited you to her party!} + male {{organizer_name} has invited you to his party!} + multiple {{organizer_name} have invited you to their party!} + other {{organizer_name} has invited you to their party!} + } + + + + + + .. code-block:: php + + // translations/messages+intl-icu.en.php + return [ + // the 'other' key is required, and is selected if no other case matches + 'invitation_title' => '{organizer_gender, select, + female {{organizer_name} has invited you to her party!} + male {{organizer_name} has invited you to his party!} + multiple {{organizer_name} have invited you to their party!} + other {{organizer_name} has invited you to their party!} + }', + ]; + +This might look very complex. The basic syntax for all functions is +``{variable_name, function_name, function_statement}`` (where, as you see +later, ``function_statement`` is optional for some functions). In this case, +the function name is ``select`` and its statement contains the "cases" of this +select. This function is applied over the ``organizer_gender`` variable:: + + // prints "Ryan has invited you to his party!" + echo $translator->trans('invitation_title', [ + 'organizer_name' => 'Ryan', + 'organizer_gender' => 'male', + ]); + + // prints "John & Jane have invited you to their party!" + echo $translator->trans('invitation_title', [ + 'organizer_name' => 'John & Jane', + 'organizer_gender' => 'multiple', + ]); + + // prints "ACME Company has invited you to their party!" + echo $translator->trans('invitation_title', [ + 'organizer_name' => 'ACME Company', + 'organizer_gender' => 'not_applicable', + ]); + +The ``{...}`` syntax alternates between "literal" and "code" mode. This allows +you to use literal text in the select statements: + +#. The first ``{organizer_gender, select, ...}`` block starts the "code" mode, + which means ``organizer_gender`` is processed as a variable. +#. The inner ``{... has invited you to her party!}`` block brings you back in + "literal" mode, meaning the text is not processed. +#. Inside this block, ``{organizer_name}`` starts "code" mode again, allowing + ``organizer_name`` to be processed as a variable. + +.. tip:: + + While it might seem more logical to only put ``her``, ``his`` or ``their`` + in the switch statement, it is better to use "complex arguments" at the + outermost structure of the message. The strings are in this way better + readable for translators and, as you can see in the ``multiple`` case, other + parts of the sentence might be influenced by the variables. + +.. tip:: + + It's possible to translate ICU MessageFormat messages directly in code, + without having to define them in any file:: + + $invitation = '{organizer_gender, select, + female {{organizer_name} has invited you to her party!} + male {{organizer_name} has invited you to his party!} + multiple {{organizer_name} have invited you to their party!} + other {{organizer_name} has invited you to their party!} + }'; + + // prints "Ryan has invited you to his party!" + echo $translator->trans( + $invitation, + [ + 'organizer_name' => 'Ryan', + 'organizer_gender' => 'male', + ], + // if you prefer, the required "+intl-icu" suffix is also defined as a constant: + // Symfony\Component\Translation\MessageCatalogueInterface::INTL_DOMAIN_SUFFIX + 'messages+intl-icu' + ); + +.. _component-translation-pluralization: + +Pluralization +------------- + +Another interesting function is ``plural``. It allows you to +handle pluralization in your messages (e.g. ``There are 3 apples`` vs +``There is one apple``). The function looks very similar to the ``select`` function: + +.. configuration-block:: + + .. code-block:: yaml + + # translations/messages+intl-icu.en.yaml + num_of_apples: >- + {apples, plural, + =0 {There are no apples} + =1 {There is one apple...} + other {There are # apples!} + } + + .. code-block:: xml + + + + + + + + num_of_apples + {apples, plural, =0 {There are no apples} =1 {There is one apple...} other {There are # apples!}} + + + + + + .. code-block:: php + + // translations/messages+intl-icu.en.php + return [ + 'num_of_apples' => '{apples, plural, + =0 {There are no apples} + =1 {There is one apple...} + other {There are # apples!} + }', + ]; + +Pluralization rules are actually quite complex and differ for each language. +For instance, Russian uses different plural forms for numbers ending with 1; +numbers ending with 2, 3 or 4; numbers ending with 5, 6, 7, 8 or 9; and even +some exceptions to this! + +In order to properly translate this, the possible cases in the ``plural`` +function are also different for each language. For instance, Russian has +``one``, ``few``, ``many`` and ``other``, while English has only ``one`` and +``other``. The full list of possible cases can be found in Unicode's +`Language Plural Rules`_ document. By prefixing with ``=``, you can match exact +values (like ``0`` in the above example). + +Usage of this string is the same as with variables and select:: + + // prints "There is one apple..." + echo $translator->trans('num_of_apples', ['apples' => 1]); + + // prints "There are 23 apples!" + echo $translator->trans('num_of_apples', ['apples' => 23]); + +.. note:: + + You can also set an ``offset`` variable to determine whether the + pluralization should be offset (e.g. in sentences like ``You and # other people`` + / ``You and # other person``). + +.. tip:: + + When combining the ``select`` and ``plural`` functions, try to still have + ``select`` as outermost function: + + .. code-block:: text + + {gender_of_host, select, + female {{num_guests, plural, offset:1 + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to her party.} + =2 {{host} invites {guest} and one other person to her party.} + other {{host} invites {guest} and # other people to her party.} + }} + male {{num_guests, plural, offset:1 + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to his party.} + =2 {{host} invites {guest} and one other person to his party.} + other {{host} invites {guest} and # other people to his party.} + }} + other {{num_guests, plural, offset:1 + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to their party.} + =2 {{host} invites {guest} and one other person to their party.} + other {{host} invites {guest} and # other people to their party.} + }} + } + +.. sidebar:: Using Ranges in Messages + + The pluralization in the legacy Symfony syntax could be used with custom + ranges (e.g. have different messages for 0-12, 12-40 and 40+). The ICU + message format does not have this feature. Instead, this logic should be + moved to PHP code:: + + // Instead of + $message = $translator->trans('balance_message', $balance); + // with a message like: + // ]-Inf,0]Oops! I'm down|]0,1000]I still have money|]1000,Inf]I have lots of money + + // use three different messages for each range: + if ($balance < 0) { + $message = $translator->trans('no_money_message'); + } elseif ($balance < 1000) { + $message = $translator->trans('some_money_message'); + } else { + $message = $translator->trans('lots_of_money_message'); + } + +Additional Placeholder Functions +-------------------------------- + +Besides these, the ICU MessageFormat comes with a couple other interesting functions. + +Ordinal +~~~~~~~ + +Similar to ``plural``, ``selectordinal`` allows you to use numbers as ordinal scale: + +.. configuration-block:: + + .. code-block:: yaml + + # translations/messages+intl-icu.en.yaml + finish_place: >- + You finished {place, selectordinal, + one {#st} + two {#nd} + few {#rd} + other {#th} + }! + + # when only formatting the number as ordinal (like above), you can also + # use the `ordinal` function: + finish_place: You finished {place, ordinal}! + + .. code-block:: xml + + + + + + + + finish_place + You finished {place, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}! + + + + + finish_place + You finished {place, ordinal}! + + + + + + .. code-block:: php + + // translations/messages+intl-icu.en.php + return [ + 'finish_place' => 'You finished {place, selectordinal, + one {#st} + two {#nd} + few {#rd} + other {#th} + }!', + + // when only formatting the number as ordinal (like above), you can + // also use the `ordinal` function: + 'finish_place' => 'You finished {place, ordinal}!', + ]; + +.. code-block:: php + + // prints "You finished 1st!" + echo $translator->trans('finish_place', ['place' => 1]); + + // prints "You finished 9th!" + echo $translator->trans('finish_place', ['place' => 9]); + + // prints "You finished 23rd!" + echo $translator->trans('finish_place', ['place' => 23]); + +The possible cases for this are also shown in Unicode's `Language Plural Rules`_ document. + +Date and Time +~~~~~~~~~~~~~ + +The date and time function allows you to format dates in the target locale +using the :phpclass:`IntlDateFormatter`: + +.. configuration-block:: + + .. code-block:: yaml + + # translations/messages+intl-icu.en.yaml + published_at: 'Published at {publication_date, date} - {publication_date, time, short}' + + .. code-block:: xml + + + + + + + + published_at + Published at {publication_date, date} - {publication_date, time, short} + + + + + + .. code-block:: php + + // translations/messages+intl-icu.en.php + return [ + 'published_at' => 'Published at {publication_date, date} - {publication_date, time, short}', + ]; + +The "function statement" for the ``time`` and ``date`` functions can be one of +``short``, ``medium``, ``long`` or ``full``, which correspond to the +`constants defined by the IntlDateFormatter class`_:: + + // prints "Published at Jan 25, 2019 - 11:30 AM" + echo $translator->trans('published_at', ['publication_date' => new \DateTime('2019-01-25 11:30:00')]); + +Numbers +~~~~~~~ + +The ``number`` formatter allows you to format numbers using Intl's :phpclass:`NumberFormatter`: + +.. configuration-block:: + + .. code-block:: yaml + + # translations/messages+intl-icu.en.yaml + progress: '{progress, number, percent} of the work is done' + value_of_object: 'This artifact is worth {value, number, currency}' + + .. code-block:: xml + + + + + + + + progress + {progress, number, percent} of the work is done + + + + value_of_object + This artifact is worth {value, number, currency} + + + + + + .. code-block:: php + + // translations/messages+intl-icu.en.php + return [ + 'progress' => '{progress, number, percent} of the work is done', + 'value_of_object' => 'This artifact is worth {value, number, currency}', + ]; + +.. code-block:: php + + // prints "82% of the work is done" + echo $translator->trans('progress', ['progress' => 0.82]); + // prints "100% of the work is done" + echo $translator->trans('progress', ['progress' => 1]); + + // prints "This artifact is worth $9,988,776.65" + // if we would translate this to i.e. French, the value would be shown as + // "9 988 776,65 €" + echo $translator->trans('value_of_object', ['value' => 9988776.65]); + +.. _`online editor`: https://format-message.github.io/icu-message-format-for-translators/ +.. _`ICU MessageFormat`: https://unicode-org.github.io/icu/userguide/format_parse/messages/ +.. _`switch statement`: https://www.php.net/control-structures.switch +.. _`Language Plural Rules`: https://www.unicode.org/cldr/charts/43/supplemental/language_plural_rules.html +.. _`constants defined by the IntlDateFormatter class`: https://www.php.net/manual/en/class.intldateformatter.php diff --git a/reference/formats/xliff.rst b/reference/formats/xliff.rst new file mode 100644 index 00000000000..b5dc99b4186 --- /dev/null +++ b/reference/formats/xliff.rst @@ -0,0 +1,44 @@ +The XLIFF format +================ + +Most professional translation tools support XLIFF_. These files use the XML +format and are supported by Symfony by default. Besides supporting +:doc:`all Symfony translation features `, the XLIFF format also +has some specific features. + +Adding Notes to Translation Contents +------------------------------------ + +Sometimes translators need additional context to better decide how to translate +some content. This context can be provided with notes, which are a collection of +comments used to store end user readable information. The only format that +supports loading and dumping notes is XLIFF version 2. + +If the XLIFF 2.0 document contains ```` nodes, they are automatically +loaded/dumped inside a Symfony application: + +.. code-block:: xml + + + + + + + new + true + user login + + + original-content + translated-content + + + + + +.. versionadded:: 7.2 + + The support of attributes in the ```` element was introduced in Symfony 7.2. + +.. _XLIFF: https://docs.oasis-open.org/xliff/xliff-core/v2.1/xliff-core-v2.1.html diff --git a/reference/formats/yaml.rst b/reference/formats/yaml.rst new file mode 100644 index 00000000000..1884735bd82 --- /dev/null +++ b/reference/formats/yaml.rst @@ -0,0 +1,377 @@ +The YAML Format +--------------- + +The Symfony :doc:`Yaml Component ` implements a selected subset +of features defined in the `YAML 1.2 version specification`_. + +Scalars +~~~~~~~ + +The syntax for scalars is similar to the PHP syntax. + +Strings +....... + +Strings in YAML can be wrapped both in single and double quotes. In some cases, +they can also be unquoted: + +.. code-block:: yaml + + A string in YAML + + 'A single-quoted string in YAML' + + "A double-quoted string in YAML" + +Quoted styles are useful when a string starts or end with one or more relevant +spaces, because unquoted strings are trimmed on both end when parsing their +contents. Quotes are required when the string contains special or reserved characters. + +When using single-quoted strings, any single quote ``'`` inside its contents +must be doubled to escape it: + +.. code-block:: yaml + + 'A single quote '' inside a single-quoted string' + +Strings containing any of the following characters must be quoted: +``: { } [ ] , & * # ? | - < > = ! % @`` Although you can use double quotes, for +these characters it is more convenient to use single quotes, which avoids having +to escape any backslash ``\``. + +The double-quoted style provides a way to express arbitrary strings, by +using ``\`` to escape characters and sequences. For instance, it is very useful +when you need to embed a ``\n`` or a Unicode character in a string. + +.. code-block:: yaml + + "A double-quoted string in YAML\n" + +If the string contains any of the following control characters, it must be +escaped with double quotes: + +``\0``, ``\x01``, ``\x02``, ``\x03``, ``\x04``, ``\x05``, ``\x06``, ``\a``, +``\b``, ``\t``, ``\n``, ``\v``, ``\f``, ``\r``, ``\x0e``, ``\x0f``, ``\x10``, +``\x11``, ``\x12``, ``\x13``, ``\x14``, ``\x15``, ``\x16``, ``\x17``, ``\x18``, +``\x19``, ``\x1a``, ``\e``, ``\x1c``, ``\x1d``, ``\x1e``, ``\x1f``, ``\N``, +``\_``, ``\L``, ``\P`` + +Finally, there are other cases when the strings must be quoted, no matter if +you're using single or double quotes: + +* When the string is ``true`` or ``false`` (otherwise, it would be treated as a + boolean value); +* When the string is ``null`` or ``~`` (otherwise, it would be considered as a + ``null`` value); +* When the string looks like a number, such as integers (e.g. ``2``, ``14``, etc.), + floats (e.g. ``2.6``, ``14.9``) and exponential numbers (e.g. ``12e7``, etc.) + (otherwise, it would be treated as a numeric value); +* When the string looks like a date (e.g. ``2014-12-31``) (otherwise it would be + automatically converted into a Unix timestamp). + +When a string contains line breaks, you can use the literal style, indicated +by the pipe (``|``), to indicate that the string will span several lines. In +literals, newlines are preserved: + +.. code-block:: yaml + + | + \/ /| |\/| | + / / | | | |__ + +Alternatively, strings can be written with the folded style, denoted by ``>``, +where each line break is replaced by a space: + +.. code-block:: yaml + + > + This is a very long sentence + 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:: + + Notice the two spaces before each line in the previous examples. They + won't appear in the resulting PHP strings. + +Numbers +....... + +.. code-block:: yaml + + # an integer + 12 + +.. code-block:: yaml + + # an octal + 0o14 + +.. code-block:: yaml + + # an hexadecimal + 0xC + +.. code-block:: yaml + + # a float + 13.4 + +.. code-block:: yaml + + # an exponential number + 1.2e+34 + +.. code-block:: yaml + + # infinity + .inf + +Nulls +..... + +Nulls in YAML can be expressed with ``null`` or ``~``. + +Booleans +........ + +Booleans in YAML are expressed with ``true`` and ``false``. + +Dates +..... + +YAML uses the `ISO-8601`_ standard to express dates: + +.. code-block:: yaml + + 2001-12-14T21:59:43.10-05:00 + +.. code-block:: yaml + + # simple date + 2002-12-14 + +.. _yaml-format-collections: + +Collections +~~~~~~~~~~~ + +A YAML file is rarely used to describe a simple scalar. Most of the time, it +describes a collection. YAML collections can be a sequence (indexed arrays in PHP) +or a mapping of elements (associative arrays in PHP). + +Sequences use a dash followed by a space: + +.. code-block:: yaml + + - PHP + - Perl + - Python + +The previous YAML file is equivalent to the following PHP code:: + + ['PHP', 'Perl', 'Python']; + +Mappings use a colon followed by a space (``:`` ) to mark each key/value pair: + +.. code-block:: yaml + + PHP: 5.2 + MySQL: 5.1 + Apache: 2.2.20 + +which is equivalent to this PHP code:: + + ['PHP' => 5.2, 'MySQL' => 5.1, 'Apache' => '2.2.20']; + +.. note:: + + In a mapping, a key can be any valid scalar. + +The number of spaces between the colon and the value does not matter: + +.. code-block:: yaml + + PHP: 5.2 + MySQL: 5.1 + Apache: 2.2.20 + +YAML uses indentation with one or more spaces to describe nested collections: + +.. code-block:: yaml + + 'symfony 1.0': + PHP: 5.0 + Propel: 1.2 + 'symfony 1.2': + PHP: 5.2 + Propel: 1.3 + +The above YAML is equivalent to the following PHP code:: + + [ + 'symfony 1.0' => [ + 'PHP' => 5.0, + 'Propel' => 1.2, + ], + 'symfony 1.2' => [ + 'PHP' => 5.2, + 'Propel' => 1.3, + ], + ]; + +There is one important thing you need to remember when using indentation in a +YAML file: *Indentation must be done with one or more spaces, but never with +tabulators*. + +You can nest sequences and mappings as you like: + +.. code-block:: yaml + + 'Chapter 1': + - Introduction + - Event Types + 'Chapter 2': + - Introduction + - Helpers + +YAML can also use flow styles for collections, using explicit indicators +rather than indentation to denote scope. + +A sequence can be written as a comma separated list within square brackets +(``[]``): + +.. code-block:: yaml + + [PHP, Perl, Python] + +A mapping can be written as a comma separated list of key/values within curly +braces (``{}``): + +.. code-block:: yaml + + { PHP: 5.2, MySQL: 5.1, Apache: 2.2.20 } + +You can mix and match styles to achieve a better readability: + +.. code-block:: yaml + + 'Chapter 1': [Introduction, Event Types] + 'Chapter 2': [Introduction, Helpers] + +.. code-block:: yaml + + 'symfony 1.0': { PHP: 5.0, Propel: 1.2 } + 'symfony 1.2': { PHP: 5.2, Propel: 1.3 } + +Comments +~~~~~~~~ + +Comments can be added in YAML by prefixing them with a hash mark (``#``): + +.. code-block:: yaml + + # Comment on a line + "symfony 1.0": { PHP: 5.0, Propel: 1.2 } # Comment at the end of a line + "symfony 1.2": { PHP: 5.2, Propel: 1.3 } + +.. note:: + + Comments are ignored by the YAML parser and do not need to be indented + according to the current level of nesting in a collection. + +Explicit Typing +~~~~~~~~~~~~~~~ + +The YAML specification defines some tags to set the type of any data explicitly: + +.. code-block:: yaml + + data: + # this value is parsed as a string (it's not transformed into a DateTime) + start_date: !!str 2002-12-14 + + # this value is parsed as a float number (it will be 3.0 instead of 3) + price: !!float 3 + + # this value is parsed as binary data encoded in base64 + picture: !!binary | + R0lGODlhDAAMAIQAAP//9/X + 17unp5WZmZgAAAOfn515eXv + Pz7Y6OjuDg4J+fn5OTk6enp + 56enmleECcgggoBADs= + +Symfony Specific Features +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Yaml component provides some additional features that are not part of the +official YAML specification but are useful in Symfony applications: + +* ``!php/const`` allows to get the value of a PHP constant. This tag takes the + fully-qualified class name of the constant as its argument: + + .. code-block:: yaml + + data: + page_limit: !php/const App\Pagination\Paginator::PAGE_LIMIT + +* ``!php/object`` allows to pass the serialized representation of a PHP + object (created with the `serialize()`_ function), which will be deserialized + when parsing the YAML file: + + .. code-block:: yaml + + data: + my_object: !php/object 'O:8:"stdClass":1:{s:3:"bar";i:2;}' + +* ``!php/enum`` allows to use a PHP enum case. This tag takes the fully-qualified + class name of the enum case as its argument: + + .. code-block:: yaml + + data: + # You can use the typed enum case... + operator_type: !php/enum App\Operator\Enum\Type::Or + # ... or you can also use "->value" to directly use the value of a BackedEnum case + operator_type: !php/enum App\Operator\Enum\Type::Or->value + + This tag allows to omit the enum case and only provide the enum FQCN + to return an array of all available enum cases: + + .. code-block:: yaml + + data: + operator_types: !php/enum App\Operator\Enum\Type + + .. versionadded:: 7.1 + + The support for using the enum FQCN without specifying a case + was introduced in Symfony 7.1. + +Unsupported YAML Features +~~~~~~~~~~~~~~~~~~~~~~~~~ + +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``, ``!!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). + +.. _`YAML 1.2 version specification`: https://yaml.org/spec/1.2/spec.html +.. _`ISO-8601`: https://www.iso.org/iso-8601-date-and-time-format.html +.. _`serialize()`: https://www.php.net/manual/en/function.serialize.php diff --git a/reference/forms/twig_reference.rst b/reference/forms/twig_reference.rst deleted file mode 100644 index f81e7912082..00000000000 --- a/reference/forms/twig_reference.rst +++ /dev/null @@ -1,345 +0,0 @@ -.. index:: - single: Forms; Twig form function reference - -Twig Template Form Function and Variable Reference -================================================== - -When working with forms in a template, there are two powerful things at your -disposal: - -* :ref:`Functions` for rendering each part of a form -* :ref:`Variables` for getting *any* information about any field - -You'll use functions often to render your fields. Variables, on the other -hand, are less commonly-used, but infinitely powerful since you can access -a fields label, id attribute, errors, and anything else about the field. - -.. _reference-form-twig-functions: - -Form Rendering Functions ------------------------- - -This reference manual covers all the possible Twig functions available for -rendering forms. There are several different functions available, and each -is responsible for rendering a different part of a form (e.g. labels, errors, -widgets, etc). - -.. _reference-forms-twig-form: - -form(view, variables) ---------------------- - -Renders the HTML of a complete form. - -.. code-block:: jinja - - {# render the form and change the submission method #} - {{ form(form, {'method': 'GET'}) }} - -You will mostly use this helper for prototyping or if you use custom form -themes. If you need more flexibility in rendering the form, you should use -the other helpers to render individual parts of the form instead: - -.. code-block:: jinja - - {{ form_start(form) }} - {{ form_errors(form) }} - - {{ form_row(form.name) }} - {{ form_row(form.dueDate) }} - - - {{ form_end(form) }} - -.. _reference-forms-twig-start: - -form_start(view, variables) ---------------------------- - -Renders the start tag of a form. This helper takes care of printing the -configured method and target action of the form. It will also include the -correct ``enctype`` property if the form contains upload fields. - -.. code-block:: jinja - - {# render the start tag and change the submission method #} - {{ form_start(form, {'method': 'GET'}) }} - -.. _reference-forms-twig-end: - -form_end(view, variables) -------------------------- - -Renders the end tag of a form. - -.. code-block:: jinja - - {{ form_end(form) }} - -This helper also outputs ``form_rest()`` unless you set ``render_rest`` to -false: - -.. code-block:: jinja - - {# don't render unrendered fields #} - {{ form_end(form, {'render_rest': false}) }} - -.. _reference-forms-twig-label: - -form_label(view, label, variables) ----------------------------------- - -Renders the label for the given field. You can optionally pass the specific -label you want to display as the second argument. - -.. code-block:: jinja - - {{ form_label(form.name) }} - - {# The two following syntaxes are equivalent #} - {{ form_label(form.name, 'Your Name', {'label_attr': {'class': 'foo'}}) }} - {{ form_label(form.name, null, {'label': 'Your name', 'label_attr': {'class': 'foo'}}) }} - -See ":ref:`twig-reference-form-variables`" to learn about the ``variables`` -argument. - -.. _reference-forms-twig-errors: - -form_errors(view) ------------------ - -Renders any errors for the given field. - -.. code-block:: jinja - - {{ form_errors(form.name) }} - - {# render any "global" errors #} - {{ form_errors(form) }} - -.. _reference-forms-twig-widget: - -form_widget(view, variables) ----------------------------- - -Renders the HTML widget of a given field. If you apply this to an entire form -or collection of fields, each underlying form row will be rendered. - -.. code-block:: jinja - - {# render a widget, but add a "foo" class to it #} - {{ form_widget(form.name, {'attr': {'class': 'foo'}}) }} - -The second argument to ``form_widget`` is an array of variables. The most -common variable is ``attr``, which is an array of HTML attributes to apply -to the HTML widget. In some cases, certain types also have other template-related -options that can be passed. These are discussed on a type-by-type basis. -The ``attributes`` are not applied recursively to child fields if you're -rendering many fields at once (e.g. ``form_widget(form)``). - -See ":ref:`twig-reference-form-variables`" to learn more about the ``variables`` -argument. - -.. _reference-forms-twig-row: - -form_row(view, variables) -------------------------- - -Renders the "row" of a given field, which is the combination of the field's -label, errors and widget. - -.. code-block:: jinja - - {# render a field row, but display a label with text "foo" #} - {{ form_row(form.name, {'label': 'foo'}) }} - -The second argument to ``form_row`` is an array of variables. The templates -provided in Symfony only allow to override the label as shown in the example -above. - -See ":ref:`twig-reference-form-variables`" to learn about the ``variables`` -argument. - -.. _reference-forms-twig-rest: - -form_rest(view, variables) --------------------------- - -This renders all fields that have not yet been rendered for the given form. -It's a good idea to always have this somewhere inside your form as it'll -render hidden fields for you and make any fields you forgot to render more -obvious (since it'll render the field for you). - -.. code-block:: jinja - - {{ form_rest(form) }} - -.. _reference-forms-twig-enctype: - -form_enctype(view) ------------------- - -.. note:: - - This helper was deprecated in Symfony 2.3 and will be removed in Symfony 3.0. - You should use ``form_start()`` instead. - -If the form contains at least one file upload field, this will render the -required ``enctype="multipart/form-data"`` form attribute. It's always a -good idea to include this in your form tag: - -.. code-block:: html+jinja - -
          - -.. _`twig-reference-form-variables`: - -More about Form Variables -------------------------- - -.. tip:: - - For a full list of variables, see: :ref:`reference-form-twig-variables`. - -In almost every Twig function above, the final argument is an array of "variables" -that are used when rendering that one part of the form. For example, the -following would render the "widget" for a field, and modify its attributes -to include a special class: - -.. code-block:: jinja - - {# render a widget, but add a "foo" class to it #} - {{ form_widget(form.name, { 'attr': {'class': 'foo'} }) }} - -The purpose of these variables - what they do & where they come from - may -not be immediately clear, but they're incredibly powerful. Whenever you -render any part of a form, the block that renders it makes use of a number -of variables. By default, these blocks live inside `form_div_layout.html.twig`_. - -Look at the ``form_label`` as an example: - -.. code-block:: jinja - - {% block form_label %} - {% if not compound %} - {% set label_attr = label_attr|merge({'for': id}) %} - {% endif %} - {% if required %} - {% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %} - {% endif %} - {% if label is empty %} - {% set label = name|humanize %} - {% endif %} - {{ label|trans({}, translation_domain) }} - {% endblock form_label %} - -This block makes use of several variables: ``compound``, ``label_attr``, ``required``, -``label``, ``name`` and ``translation_domain``. -These variables are made available by the form rendering system. But more -importantly, these are the variables that you can override when calling ``form_label`` -(since in this example, you're rendering the label). - -The exact variables available to override depends on which part of the form -you're rendering (e.g. label versus widget) and which field you're rendering -(e.g. a ``choice`` widget has an extra ``expanded`` option). If you get comfortable -with looking through `form_div_layout.html.twig`_, you'll always be able -to see what options you have available. - -.. tip:: - - Behind the scenes, these variables are made available to the ``FormView`` - object of your form when the form component calls ``buildView`` and ``buildViewBottomUp`` - on each "node" of your form tree. To see what "view" variables a particularly - field has, find the source code for the form field (and its parent fields) - and look at the above two functions. - -.. note:: - - If you're rendering an entire form at once (or an entire embedded form), - the ``variables`` argument will only be applied to the form itself and - not its children. In other words, the following will **not** pass a "foo" - class attribute to all of the child fields in the form: - - .. code-block:: jinja - - {# does **not** work - the variables are not recursive #} - {{ form_widget(form, { 'attr': {'class': 'foo'} }) }} - -.. _reference-form-twig-variables: - -Form Variables Reference -~~~~~~~~~~~~~~~~~~~~~~~~ - -The following variables are common to every field type. Certain field types -may have even more variables and some variables here only really apply to -certain types. - -Assuming you have a ``form`` variable in your template, and you want to reference -the variables on the ``name`` field, accessing the variables is done by using -a public ``vars`` property on the :class:`Symfony\\Component\\Form\\FormView` -object: - -.. configuration-block:: - - .. code-block:: html+jinja - - - - .. code-block:: html+php - - - -.. versionadded:: 2.1 - The ``valid``, ``label_attr``, ``compound``, and ``disabled`` variables - are new in Symfony 2.1. - -+-----------------+-----------------------------------------------------------------------------------------+ -| Variable | Usage | -+=================+=========================================================================================+ -| ``id`` | The ``id`` HTML attribute to be rendered | -+-----------------+-----------------------------------------------------------------------------------------+ -| ``name`` | The name of the field (e.g. ``title``) - but not the ``name`` | -| | HTML attribute, which is ``full_name`` | -+-----------------+-----------------------------------------------------------------------------------------+ -| ``full_name`` | The ``name`` HTML attribute to be rendered | -+-----------------+-----------------------------------------------------------------------------------------+ -| ``errors`` | An array of any errors attached to *this* specific field (e.g. ``form.title.errors``). | -| | Note that you can't use ``form.errors`` to determine if a form is valid, | -| | since this only returns "global" errors: some individual fields may have errors | -| | Instead, use the ``valid`` option | -+-----------------+-----------------------------------------------------------------------------------------+ -| ``valid`` | Returns ``true`` or ``false`` depending on whether the whole form is valid | -+-----------------+-----------------------------------------------------------------------------------------+ -| ``value`` | The value that will be used when rendering (commonly the ``value`` HTML attribute) | -+-----------------+-----------------------------------------------------------------------------------------+ -| ``read_only`` | If ``true``, ``readonly="readonly"`` is added to the field | -+-----------------+-----------------------------------------------------------------------------------------+ -| ``disabled`` | If ``true``, ``disabled="disabled"`` is added to the field | -+-----------------+-----------------------------------------------------------------------------------------+ -| ``required`` | If ``true``, a ``required`` attribute is added to the field to activate HTML5 | -| | validation. Additionally, a ``required`` class is added to the label. | -+-----------------+-----------------------------------------------------------------------------------------+ -| ``max_length`` | Adds a ``maxlength`` HTML attribute to the element | -+-----------------+-----------------------------------------------------------------------------------------+ -| ``pattern`` | Adds a ``pattern`` HTML attribute to the element | -+-----------------+-----------------------------------------------------------------------------------------+ -| ``label`` | The string label that will be rendered | -+-----------------+-----------------------------------------------------------------------------------------+ -| ``multipart`` | If ``true``, ``form_enctype`` will render ``enctype="multipart/form-data"``. | -| | This only applies to the root form element. | -+-----------------+-----------------------------------------------------------------------------------------+ -| ``attr`` | A key-value array that will be rendered as HTML attributes on the field | -+-----------------+-----------------------------------------------------------------------------------------+ -| ``label_attr`` | A key-value array that will be rendered as HTML attributes on the label | -+-----------------+-----------------------------------------------------------------------------------------+ -| ``compound`` | Whether or not a field is actually a holder for a group of children fields | -| | (for example, a ``choice`` field, which is actually a group of checkboxes | -+-----------------+-----------------------------------------------------------------------------------------+ - -.. _`form_div_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig diff --git a/reference/forms/types.rst b/reference/forms/types.rst index 36b471c9e6f..26668d6d78a 100644 --- a/reference/forms/types.rst +++ b/reference/forms/types.rst @@ -1,49 +1,13 @@ -.. index:: - single: Forms; Types Reference - Form Types Reference ==================== -.. toctree:: - :maxdepth: 1 - :hidden: - - types/birthday - types/checkbox - types/choice - types/collection - types/country - types/date - types/datetime - types/email - types/entity - types/file - types/field - types/form - types/hidden - types/integer - types/language - types/locale - types/money - types/number - types/password - types/percent - types/radio - types/repeated - types/search - types/text - types/textarea - types/time - types/timezone - types/url - A form is composed of *fields*, each of which are built with the help of -a field *type* (e.g. a ``text`` type, ``choice`` type, etc). Symfony2 comes +a field *type* (e.g. ``TextType``, ``ChoiceType``, etc). Symfony comes standard with a large list of field types that can be used in your application. Supported Field Types --------------------- -The following field types are natively available in Symfony2: +The following field types are natively available in Symfony: .. include:: /reference/forms/types/map.rst.inc diff --git a/reference/forms/types/birthday.rst b/reference/forms/types/birthday.rst index 203a1ebe656..383dbf890f2 100644 --- a/reference/forms/types/birthday.rst +++ b/reference/forms/types/birthday.rst @@ -1,85 +1,107 @@ -.. index:: - single: Forms; Fields; birthday +BirthdayType Field +================== -birthday Field Type -=================== +A :doc:`DateType ` field that specializes in handling +birth date data. -A :doc:`date` field that specializes in handling -birthdate data. - -Can be rendered as a single text box, three text boxes (month, day, and year), +Can be rendered as a single text box, three text boxes (month, day and year), or three select boxes. -This type is essentially the same as the :doc:`date` +This type is essentially the same as the :doc:`DateType ` type, but with a more appropriate default for the `years`_ option. The `years`_ option defaults to 120 years ago to the current year. -+----------------------+-------------------------------------------------------------------------------+ -| Underlying Data Type | can be ``DateTime``, ``string``, ``timestamp``, or ``array`` | -| | (see the :ref:`input option `) | -+----------------------+-------------------------------------------------------------------------------+ -| Rendered as | can be three select boxes or 1 or 3 text boxes, based on the `widget`_ option | -+----------------------+-------------------------------------------------------------------------------+ -| Overridden Options | - `years`_ | -+----------------------+-------------------------------------------------------------------------------+ -| Inherited Options | - `widget`_ | -| | - `input`_ | -| | - `months`_ | -| | - `days`_ | -| | - `format`_ | -| | - `data_timezone`_ | -| | - `user_timezone`_ | -| | - `invalid_message`_ | -| | - `invalid_message_parameters`_ | -| | - `read_only`_ | -| | - `disabled`_ | -| | - `inherit_data`_ | -+----------------------+-------------------------------------------------------------------------------+ -| Parent type | :doc:`date` | -+----------------------+-------------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\BirthdayType` | -+----------------------+-------------------------------------------------------------------------------+ ++---------------------------+-------------------------------------------------------------------------------+ +| Underlying Data Type | can be ``DateTime``, ``string``, ``timestamp``, or ``array`` | +| | (see the :ref:`input option `) | ++---------------------------+-------------------------------------------------------------------------------+ +| Rendered as | can be three select boxes or 1 or 3 text boxes, based on the `widget`_ option | ++---------------------------+-------------------------------------------------------------------------------+ +| Default invalid message | Please enter a valid birthdate. | ++---------------------------+-------------------------------------------------------------------------------+ +| Parent type | :doc:`DateType ` | ++---------------------------+-------------------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\BirthdayType` | ++---------------------------+-------------------------------------------------------------------------------+ + +.. include:: /reference/forms/types/options/_debug_form.rst.inc Overridden Options ------------------ -years -~~~~~ +.. include:: /reference/forms/types/options/invalid_message.rst.inc + +``years`` +~~~~~~~~~ **type**: ``array`` **default**: 120 years ago to the current year -List of years available to the year field type. This option is only +List of years available to the year field type. This option is only relevant when the ``widget`` option is set to ``choice``. -Inherited options +Inherited Options ----------------- -These options inherit from the :doc:`date` type: +These options inherit from the :doc:`DateType `: -.. include:: /reference/forms/types/options/date_widget.rst.inc - -.. include:: /reference/forms/types/options/date_input.rst.inc - -.. include:: /reference/forms/types/options/months.rst.inc +.. include:: /reference/forms/types/options/choice_translation_domain_disabled.rst.inc .. include:: /reference/forms/types/options/days.rst.inc +``placeholder`` +~~~~~~~~~~~~~~~ + +**type**: ``string`` | ``array`` + +If your widget option is set to ``choice``, then this field will be represented +as a series of ``select`` boxes. When the placeholder value is a string, +it will be used as the **blank value** of all select boxes:: + + $builder->add('birthdate', BirthdayType::class, [ + 'placeholder' => 'Select a value', + ]); + +Alternatively, you can use an array that configures different placeholder +values for the year, month and day fields:: + + $builder->add('birthdate', BirthdayType::class, [ + 'placeholder' => [ + 'year' => 'Year', 'month' => 'Month', 'day' => 'Day', + ], + ]); + .. include:: /reference/forms/types/options/date_format.rst.inc -.. include:: /reference/forms/types/options/data_timezone.rst.inc +.. include:: /reference/forms/types/options/date_input.rst.inc -.. include:: /reference/forms/types/options/user_timezone.rst.inc +.. include:: /reference/forms/types/options/date_input_format.rst.inc -These options inherit from the :doc:`date` type: +.. include:: /reference/forms/types/options/model_timezone.rst.inc -.. include:: /reference/forms/types/options/invalid_message.rst.inc +.. include:: /reference/forms/types/options/months.rst.inc -.. include:: /reference/forms/types/options/invalid_message_parameters.rst.inc +.. include:: /reference/forms/types/options/view_timezone.rst.inc + +.. include:: /reference/forms/types/options/date_widget.rst.inc + +These options inherit from the :doc:`FormType `: + +.. include:: /reference/forms/types/options/attr.rst.inc -.. include:: /reference/forms/types/options/read_only.rst.inc +.. include:: /reference/forms/types/options/data.rst.inc .. include:: /reference/forms/types/options/disabled.rst.inc -These options inherit from the :doc:`date` type: +.. include:: /reference/forms/types/options/help.rst.inc + +.. include:: /reference/forms/types/options/help_attr.rst.inc + +.. include:: /reference/forms/types/options/help_html.rst.inc .. include:: /reference/forms/types/options/inherit_data.rst.inc + +.. include:: /reference/forms/types/options/invalid_message_parameters.rst.inc + +.. include:: /reference/forms/types/options/mapped.rst.inc + +.. include:: /reference/forms/types/options/row_attr.rst.inc diff --git a/reference/forms/types/button.rst b/reference/forms/types/button.rst new file mode 100644 index 00000000000..a83cb0a09b6 --- /dev/null +++ b/reference/forms/types/button.rst @@ -0,0 +1,83 @@ +ButtonType Field +================ + +A simple, non-responsive button. + ++----------------------+----------------------------------------------------------------------+ +| Rendered as | ``button`` tag | ++----------------------+----------------------------------------------------------------------+ +| Parent type | none | ++----------------------+----------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\ButtonType` | ++----------------------+----------------------------------------------------------------------+ + +.. include:: /reference/forms/types/options/_debug_form.rst.inc + +Inherited Options +----------------- + +The following options are defined in the +:class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\BaseType` class. +The ``BaseType`` class is the parent class for both the ``button`` type +and the :doc:`FormType `, but it is not part +of the form type tree (i.e. it cannot be used as a form type on its own). + +``attr`` +~~~~~~~~ + +**type**: ``array`` **default**: ``[]`` + +If you want to add extra attributes to the HTML representation of the button, +you can use ``attr`` option. It's an associative array with HTML attribute +as a key. This can be useful when you need to set a custom class for the button:: + + use Symfony\Component\Form\Extension\Core\Type\ButtonType; + // ... + + $builder->add('save', ButtonType::class, [ + 'attr' => ['class' => 'save'], + ]); + +.. include:: /reference/forms/types/options/button_disabled.rst.inc + +.. include:: /reference/forms/types/options/button_label.rst.inc + +.. include:: /reference/forms/types/options/label_html.rst.inc + +.. include:: /reference/forms/types/options/button_translation_domain.rst.inc + +label_translation_parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``array`` **default**: ``[]`` + +The content of the `label`_ option is translated before displaying it, so it +can contain :ref:`translation placeholders `. +This option defines the values used to replace those placeholders. + +Given this translation message: + +.. code-block:: yaml + + # translations/messages.en.yaml + form.order.submit_to_company: 'Send an order to %company%' + +You can specify the placeholder values as follows:: + + use Symfony\Component\Form\Extension\Core\Type\ButtonType; + // ... + + $builder->add('send', ButtonType::class, [ + 'label' => 'form.order.submit_to_company', + 'label_translation_parameters' => [ + '%company%' => 'ACME Inc.', + ], + ]); + +The ``label_translation_parameters`` option of buttons is merged with the same +option of its parents, so buttons can reuse and/or override any of the parent +placeholders. + +.. include:: /reference/forms/types/options/attr_translation_parameters.rst.inc + +.. include:: /reference/forms/types/options/row_attr.rst.inc diff --git a/reference/forms/types/checkbox.rst b/reference/forms/types/checkbox.rst index dec426115e1..2299220c5b6 100644 --- a/reference/forms/types/checkbox.rst +++ b/reference/forms/types/checkbox.rst @@ -1,61 +1,95 @@ -.. index:: - single: Forms; Fields; checkbox - -checkbox Field Type -=================== - -Creates a single input checkbox. This should always be used for a field that -has a Boolean value: if the box is checked, the field will be set to true, -if the box is unchecked, the value will be set to false. - -+-------------+------------------------------------------------------------------------+ -| Rendered as | ``input`` ``checkbox`` field | -+-------------+------------------------------------------------------------------------+ -| Options | - `value`_ | -+-------------+------------------------------------------------------------------------+ -| Inherited | - `required`_ | -| options | - `label`_ | -| | - `read_only`_ | -| | - `disabled`_ | -| | - `error_bubbling`_ | -+-------------+------------------------------------------------------------------------+ -| Parent type | :doc:`field` | -+-------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\CheckboxType` | -+-------------+------------------------------------------------------------------------+ +CheckboxType Field +================== + +Creates a single input checkbox. This should always be used for a field +that has a boolean value: if the box is checked, the field will be set to +true, if the box is unchecked, the value will be set to false. Optionally +you can specify an array of values that, if submitted, will be evaluated +to "false" as well (this differs from what HTTP defines, but can be handy +if you want to handle submitted values like "0" or "false"). + ++---------------------------+------------------------------------------------------------------------+ +| Rendered as | ``input`` ``checkbox`` field | ++---------------------------+------------------------------------------------------------------------+ +| Default invalid message | The checkbox has an invalid value. | ++---------------------------+------------------------------------------------------------------------+ +| Parent type | :doc:`FormType ` | ++---------------------------+------------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\CheckboxType` | ++---------------------------+------------------------------------------------------------------------+ + +.. include:: /reference/forms/types/options/_debug_form.rst.inc Example Usage ------------- .. code-block:: php - $builder->add('public', 'checkbox', array( - 'label' => 'Show this entry publicly?', - 'required' => false, - )); + use Symfony\Component\Form\Extension\Core\Type\CheckboxType; + // ... + + $builder->add('public', CheckboxType::class, [ + 'label' => 'Show this entry publicly?', + 'required' => false, + ]); Field Options ------------- -value -~~~~~ +false_values +~~~~~~~~~~~~ -**type**: ``mixed`` **default**: ``1`` +**type**: ``array`` **default**: ``[null]`` -The value that's actually used as the value for the checkbox. This does -not affect the value that's set on your object. +An array of values to be interpreted as ``false``. -Inherited options ------------------ +.. include:: /reference/forms/types/options/value.rst.inc -These options inherit from the :doc:`field` type: +Overridden Options +------------------ -.. include:: /reference/forms/types/options/required.rst.inc +.. include:: /reference/forms/types/options/checkbox_compound.rst.inc -.. include:: /reference/forms/types/options/label.rst.inc +.. include:: /reference/forms/types/options/checkbox_empty_data.rst.inc + +.. include:: /reference/forms/types/options/invalid_message.rst.inc + +Inherited Options +----------------- + +These options inherit from the :doc:`FormType `: + +.. include:: /reference/forms/types/options/attr.rst.inc -.. include:: /reference/forms/types/options/read_only.rst.inc +.. include:: /reference/forms/types/options/data.rst.inc .. include:: /reference/forms/types/options/disabled.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc + +.. include:: /reference/forms/types/options/error_mapping.rst.inc + +.. include:: /reference/forms/types/options/help.rst.inc + +.. include:: /reference/forms/types/options/help_attr.rst.inc + +.. include:: /reference/forms/types/options/help_html.rst.inc + +.. include:: /reference/forms/types/options/label.rst.inc + +.. include:: /reference/forms/types/options/label_attr.rst.inc + +.. include:: /reference/forms/types/options/label_html.rst.inc + +.. include:: /reference/forms/types/options/label_format.rst.inc + +.. include:: /reference/forms/types/options/mapped.rst.inc + +.. include:: /reference/forms/types/options/required.rst.inc + +.. include:: /reference/forms/types/options/row_attr.rst.inc + +Form Variables +-------------- + +.. include:: /reference/forms/types/variables/check_or_radio_table.rst.inc diff --git a/reference/forms/types/choice.rst b/reference/forms/types/choice.rst index 55e257dce8c..9f61fb768bd 100644 --- a/reference/forms/types/choice.rst +++ b/reference/forms/types/choice.rst @@ -1,75 +1,143 @@ -.. index:: - single: Forms; Fields; choice - -choice Field Type -================= +ChoiceType Field (select drop-downs, radio buttons & checkboxes) +================================================================ A multi-purpose field used to allow the user to "choose" one or more options. It can be rendered as a ``select`` tag, radio buttons, or checkboxes. -To use this field, you must specify *either* the ``choice_list`` or ``choices`` -option. - -+-------------+-----------------------------------------------------------------------------+ -| Rendered as | can be various tags (see below) | -+-------------+-----------------------------------------------------------------------------+ -| Options | - `choices`_ | -| | - `choice_list`_ | -| | - `multiple`_ | -| | - `expanded`_ | -| | - `preferred_choices`_ | -| | - `empty_value`_ | -+-------------+-----------------------------------------------------------------------------+ -| Inherited | - `required`_ | -| options | - `label`_ | -| | - `read_only`_ | -| | - `disabled`_ | -| | - `error_bubbling`_ | -| | - `inherit_data`_ | -| | - `by_reference`_ | -| | - `empty_data`_ | -+-------------+-----------------------------------------------------------------------------+ -| Parent type | :doc:`form` (if expanded), ``field`` otherwise | -+-------------+-----------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\ChoiceType` | -+-------------+-----------------------------------------------------------------------------+ +To use this field, you must specify *either* ``choices`` or ``choice_loader`` option. + ++---------------------------+----------------------------------------------------------------------+ +| Rendered as | can be various tags (see below) | ++---------------------------+----------------------------------------------------------------------+ +| Default invalid message | The selected choice is invalid. | ++---------------------------+----------------------------------------------------------------------+ +| Parent type | :doc:`FormType ` | ++---------------------------+----------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\ChoiceType` | ++---------------------------+----------------------------------------------------------------------+ + +.. include:: /reference/forms/types/options/_debug_form.rst.inc Example Usage ------------- -The easiest way to use this field is to specify the choices directly via the -``choices`` option. The key of the array becomes the value that's actually -set on your underlying object (e.g. ``m``), while the value is what the -user sees on the form (e.g. ``Male``). +The easiest way to use this field is to define the ``choices`` option to specify +the choices as an associative array where the keys are the labels displayed to +end users and the array values are the internal values used in the form field:: + + use Symfony\Component\Form\Extension\Core\Type\ChoiceType; + // ... + + $builder->add('isAttending', ChoiceType::class, [ + 'choices' => [ + 'Maybe' => null, + 'Yes' => true, + 'No' => false, + ], + ]); + +This will create a ``select`` drop-down like this: + +.. image:: /_images/reference/form/choice-example1.png + :alt: A choice list form input with the options "Maybe", "Yes" and "No". + +If the user selects ``No``, the form will return ``false`` for this field. Similarly, +if the starting data for this field is ``true``, then ``Yes`` will be auto-selected. +In other words, the **choice** of each item is the value you want to get/set in PHP +code, while the **key** is the **label** that will be shown to the user. + +Advanced Example (with Objects!) +-------------------------------- + +This field has a *lot* of options and most control how the field is displayed. In +this example, the underlying data is some ``Category`` object that has a ``getName()`` +method:: + + use App\Entity\Category; + use Symfony\Component\Form\Extension\Core\Type\ChoiceType; + // ... + + $builder->add('category', ChoiceType::class, [ + 'choices' => [ + new Category('Cat1'), + new Category('Cat2'), + new Category('Cat3'), + new Category('Cat4'), + ], + // "name" is a property path, meaning Symfony will look for a public + // property or a public method like "getName()" to define the input + // string value that will be submitted by the form + 'choice_value' => 'name', + // a callback to return the label for a given choice + // if a placeholder is used, its empty value (null) may be passed but + // its label is defined by its own "placeholder" option + 'choice_label' => function (?Category $category): string { + return $category ? strtoupper($category->getName()) : ''; + }, + // returns the html attributes for each option input (may be radio/checkbox) + 'choice_attr' => function (?Category $category): array { + return $category ? ['class' => 'category_'.strtolower($category->getName())] : []; + }, + // every option can use a string property path or any callable that get + // passed each choice as argument, but it may not be needed + 'group_by' => function (): string { + // randomly assign things into 2 groups + return rand(0, 1) === 1 ? 'Group A' : 'Group B'; + }, + // a callback to return whether a category is preferred + 'preferred_choices' => function (?Category $category): bool { + return $category && 100 < $category->getArticleCounts(); + }, + ]); + +You can also customize the `choice_name`_ of each choice. You can learn more +about all of these options in the sections below. + +.. warning:: + + The *placeholder* is a specific field, when the choices are optional the + first item in the list must be empty, so the user can unselect. + Be sure to always handle the empty choice ``null`` when using callbacks. + +.. _forms-reference-choice-tags: -.. code-block:: php +.. include:: /reference/forms/types/options/select_how_rendered.rst.inc - $builder->add('gender', 'choice', array( - 'choices' => array('m' => 'Male', 'f' => 'Female'), - 'required' => false, - )); +Customizing each Option's Text (Label) +-------------------------------------- -By setting ``multiple`` to true, you can allow the user to choose multiple -values. The widget will be rendered as a multiple ``select`` tag or a series -of checkboxes depending on the ``expanded`` option: +Normally, the array key of each item in the ``choices`` option is used as the +text that's shown to the user. But that can be completely customized via the +`choice_label`_ option. Check it out for more details. -.. code-block:: php +.. _form-choices-simple-grouping: - $builder->add('availability', 'choice', array( - 'choices' => array( - 'morning' => 'Morning', - 'afternoon' => 'Afternoon', - 'evening' => 'Evening', - ), - 'multiple' => true, - )); +Grouping Options +---------------- -You can also use the ``choice_list`` option, which takes an object that can -specify the choices for your widget. +You can group the ``