diff --git a/.doctor-rst.yaml b/.doctor-rst.yaml index 35125df61d6..c7a17edd06c 100644 --- a/.doctor-rst.yaml +++ b/.doctor-rst.yaml @@ -12,10 +12,16 @@ rules: 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: @@ -27,20 +33,24 @@ rules: 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_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: ~ @@ -62,7 +72,6 @@ rules: valid_use_statements: ~ versionadded_directive_should_have_version: ~ yaml_instead_of_yml_suffix: ~ - yarn_dev_option_at_the_end: ~ # master versionadded_directive_major_version: @@ -86,14 +95,12 @@ whitelist: regex: - '/FOSUserBundle(.*)\.yml/' - '/(.*)\.orm\.yml/' # currently DoctrineBundle only supports .yml - - /docker-compose\.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:: 1.9.0' # Encore - - '.. versionadded:: 1.11' # Messenger (Middleware / DoctrineBundle) - '.. versionadded:: 1.18' # Flex in setup/upgrade_minor.rst - '.. versionadded:: 1.0.0' # Encore - '.. versionadded:: 5.1' # Private Services @@ -103,5 +110,7 @@ whitelist: - '.. 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)' diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 17cec7af7c3..f32043e4523 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,6 +4,6 @@ If your pull request fixes a BUG, use the oldest maintained branch that contains the bug (see https://symfony.com/releases for the list of maintained branches). If your pull request documents a NEW FEATURE, use the same Symfony branch where -the feature was introduced (and `6.x` for features of unreleased versions). +the feature was introduced (and `7.x` for features of unreleased versions). --> diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5364a72a526..4d67a5c084c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,7 +21,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Set-up PHP" uses: shivammathur/setup-php@v2 @@ -57,7 +57,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Create cache dir" run: mkdir .cache @@ -73,71 +73,74 @@ jobs: key: ${{ runner.os }}-doctor-rst-${{ steps.extract_base_branch.outputs.branch }} - name: "Run DOCtor-RST" - uses: docker://oskarstark/doctor-rst:1.48.4 + uses: docker://oskarstark/doctor-rst:1.63.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@v3 - with: - path: 'docs' - - - name: Set-up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: 8.1 - 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` + - name: Checkout code + uses: actions/checkout@v4 + with: + path: 'docs' + + - name: Set-up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + 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/README.markdown b/README.md similarity index 87% rename from README.markdown rename to README.md index 8424f980f7e..ed323a8ee83 100644 --- a/README.markdown +++ b/README.md @@ -26,8 +26,9 @@ 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 `5.4` branch as the base of your pull requests, unless you are -documenting a feature that was introduced *after* Symfony 5.4 (e.g. in Symfony 6.2). +> [!IMPORTANT] +> Use `5.4` branch as the base of your pull requests, unless you are documenting a +> feature that was introduced *after* Symfony 5.4 (e.g. in Symfony 7.1). Build Documentation Locally --------------------------- diff --git a/_build/build.php b/_build/build.php index 897fd8dac20..be2fb062a77 100755 --- a/_build/build.php +++ b/_build/build.php @@ -52,7 +52,14 @@ foreach (new RegexIterator($iterator, '/^.+\.html$/i', RegexIterator::GET_MATCH) as $match) { $htmlFilePath = array_shift($match); $htmlContents = file_get_contents($htmlFilePath); - file_put_contents($htmlFilePath, str_replace('', '', $htmlContents)); + + $htmlRelativeFilePath = str_replace($outputDir.'/', '', $htmlFilePath); + $subdirLevel = substr_count($htmlRelativeFilePath, '/'); + $baseHref = str_repeat('../', $subdirLevel); + + $htmlContents = str_replace('', '', $htmlContents); + $htmlContents = str_replace('=8.1", "symfony/console": "^6.2", "symfony/process": "^6.2", - "symfony-tools/docs-builder": "^0.20" + "symfony-tools/docs-builder": "^0.21" } } diff --git a/_build/composer.lock b/_build/composer.lock index d863be84ad9..89a4e7da3c6 100644 --- a/_build/composer.lock +++ b/_build/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1c3437f0f5d5b44eb1a339dd720bbc38", + "content-hash": "8a771cef10c68c570bff7875e4bdece3", "packages": [ { "name": "doctrine/deprecations", @@ -466,16 +466,16 @@ }, { "name": "symfony-tools/docs-builder", - "version": "v0.20.5", + "version": "v0.21.0", "source": { "type": "git", "url": "https://github.com/symfony-tools/docs-builder.git", - "reference": "11d9d81e3997e771ad1a57eabaa51fc22c500b35" + "reference": "7ab92db15e9be7d6af51b86db87c7e41a14ba18b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony-tools/docs-builder/zipball/11d9d81e3997e771ad1a57eabaa51fc22c500b35", - "reference": "11d9d81e3997e771ad1a57eabaa51fc22c500b35", + "url": "https://api.github.com/repos/symfony-tools/docs-builder/zipball/7ab92db15e9be7d6af51b86db87c7e41a14ba18b", + "reference": "7ab92db15e9be7d6af51b86db87c7e41a14ba18b", "shasum": "" }, "require": { @@ -514,9 +514,9 @@ "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/v0.20.5" + "source": "https://github.com/symfony-tools/docs-builder/tree/v0.21.0" }, - "time": "2023-04-28T09:41:45+00:00" + "time": "2023-07-11T15:21:07+00:00" }, { "name": "symfony/console", diff --git a/_images/contributing/docs-github-edit-page.png b/_images/contributing/docs-github-edit-page.png index 9ea6c15421a..b739497f70f 100644 Binary files a/_images/contributing/docs-github-edit-page.png and b/_images/contributing/docs-github-edit-page.png differ diff --git a/_images/doctrine/mapping_relations.png b/_images/doctrine/mapping_relations.png deleted file mode 100644 index a679f9cb317..00000000000 Binary files a/_images/doctrine/mapping_relations.png and /dev/null 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.png b/_images/doctrine/mapping_relations_proxy.png deleted file mode 100644 index 935153291d4..00000000000 Binary files a/_images/doctrine/mapping_relations_proxy.png and /dev/null differ 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.png b/_images/doctrine/mapping_single_entity.png deleted file mode 100644 index 6f88c6cacfa..00000000000 Binary files a/_images/doctrine/mapping_single_entity.png and /dev/null differ 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.png b/_images/form/data-transformer-types.png deleted file mode 100644 index 950acd39ea7..00000000000 Binary files a/_images/form/data-transformer-types.png and /dev/null differ 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_prepopulation_workflow.svg b/_images/form/form_prepopulation_workflow.svg index 1db13f94c72..c908f5c5a76 100644 --- a/_images/form/form_prepopulation_workflow.svg +++ b/_images/form/form_prepopulation_workflow.svg @@ -1,54 +1,253 @@ - - - - - - New form - - - - - - Prepopulated form - - - - - - - - - - Model data - - - - - - POST_SET_DATA - - - - - - PRE_SET_DATA - - - - - - setData($data) - - - - - - - - - - normalization - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/form/form_submission_workflow.svg b/_images/form/form_submission_workflow.svg index b58e11190a1..d6d138ee61a 100644 --- a/_images/form/form_submission_workflow.svg +++ b/_images/form/form_submission_workflow.svg @@ -1,76 +1,334 @@ - - - - - - denormalization - - - - - - normalization - - - - - - New form - - - - - - Prepopulated form - - - - - - Submitted form - - - - - - - - - - - - - - Request data - - - - - - handleRequest($request) - - - - - - - - - - PRE_SUBMIT - - - - - - SUBMIT - - - - - - POST_SUBMIT - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/form/form_workflow.svg b/_images/form/form_workflow.svg index a256c2073ef..2dbacbbf096 100644 --- a/_images/form/form_workflow.svg +++ b/_images/form/form_workflow.svg @@ -1,66 +1,263 @@ - - - - - - New form - - - - - - Prepopulated form - - - - - - Submitted form - - - - - - - - - - - - - - - - - - Model data - - - - - - Request data - - - - - - setData($data) - - - - - - handleRequest($request) - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/xkcd-full.png b/_images/http/xkcd-full.png deleted file mode 100644 index d5b01ea32b9..00000000000 Binary files a/_images/http/xkcd-full.png and /dev/null differ 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.png b/_images/http/xkcd-request.png deleted file mode 100644 index 310713d304c..00000000000 Binary files a/_images/http/xkcd-request.png and /dev/null differ 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/mercure/discovery.png b/_images/mercure/discovery.png deleted file mode 100644 index 0ef38271de6..00000000000 Binary files a/_images/mercure/discovery.png and /dev/null 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/schema.png b/_images/mercure/schema.png deleted file mode 100644 index 4616046e5cc..00000000000 Binary files a/_images/mercure/schema.png and /dev/null differ diff --git a/_images/sources/README.md b/_images/sources/README.md index a07bd5180fe..467d4024010 100644 --- a/_images/sources/README.md +++ b/_images/sources/README.md @@ -39,7 +39,9 @@ Use the following snippet to embed the diagram in the docs: ``` .. raw:: html - + ``` ### Reasoning 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_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/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/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/best_practices.rst b/best_practices.rst index 02315856d00..cc38287365e 100644 --- a/best_practices.rst +++ b/best_practices.rst @@ -51,6 +51,7 @@ self-explanatory and not coupled to Symfony: │ └─ console ├─ config/ │ ├─ packages/ + │ ├─ routes/ │ └─ services.yaml ├─ migrations/ ├─ public/ @@ -108,6 +109,10 @@ Define these options as :ref:`parameters ` in the :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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -155,6 +160,8 @@ 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/bundles.rst b/bundles.rst index 34b6e644d46..02db1dd5d23 100644 --- a/bundles.rst +++ b/bundles.rst @@ -6,7 +6,7 @@ The Bundle System .. caution:: In Symfony versions prior to 4.0, it was recommended to organize your own - application code using bundles. This is no longer recommended and bundles + 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 @@ -84,7 +84,7 @@ between all Symfony bundles. It follows a set of conventions, but is flexible to be adjusted if needed: ``Controller/`` - Contains the controllers of the bundle (e.g. ``RandomController.php``). + the controllers of the bundle (e.g. ``RandomController.php``). ``DependencyInjection/`` Holds certain Dependency Injection Extension classes, which may import service @@ -98,9 +98,9 @@ to be adjusted if needed: Holds templates organized by controller name (e.g. ``Random/index.html.twig``). ``Resources/public/`` - Contains web assets (images, stylesheets, etc) and is copied or symbolically - linked into the project ``public/`` directory via the ``assets:install`` console - command. + 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. ``Tests/`` Holds all tests for the bundle. diff --git a/bundles/best_practices.rst b/bundles/best_practices.rst index e622cfd243f..d2819e42fdb 100644 --- a/bundles/best_practices.rst +++ b/bundles/best_practices.rst @@ -63,6 +63,7 @@ The following is the recommended directory structure of an AcmeBlogBundle: .. code-block:: text / + ├── assets/ ├── config/ ├── docs/ │ └─ index.md @@ -121,7 +122,8 @@ Doctrine ORM entities ``src/Entity/`` Doctrine ODM documents ``src/Document/`` Event Listeners ``src/EventListener/`` Configuration (routes, services, etc.) ``config/`` -Web Assets (CSS, JS, images) ``public/`` +Web Assets (compiled CSS and JS, images) ``public/`` +Web Asset sources (``.scss``, ``.ts``, Stimulus) ``assets/`` Translation files ``translations/`` Validation (when not using annotations) ``config/validation/`` Serialization (when not using annotations) ``config/serialization/`` @@ -480,6 +482,13 @@ 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: @@ -535,16 +544,12 @@ Resources --------- If the bundle references any resources (config files, translation files, etc.), -don't use physical paths (e.g. ``__DIR__/config/services.xml``) but logical -paths (e.g. ``@AcmeBlogBundle/config/services.xml``). - -The logical paths are required because of the bundle overriding mechanism that -lets you override any resource/file of any bundle. See :ref:`http-kernel-resource-locator` -for more details about transforming physical paths into logical paths. +you can use physical paths (e.g. ``__DIR__/config/services.xml``). -Beware that templates use a simplified version of the logical path shown above. -For example, an ``index.html.twig`` template located in the ``templates/Default/`` -directory of the AcmeBlogBundle, is referenced as ``@AcmeBlog/Default/index.html.twig``. +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 ---------- diff --git a/bundles/override.rst b/bundles/override.rst index a524780baa9..1e4926a1c76 100644 --- a/bundles/override.rst +++ b/bundles/override.rst @@ -5,14 +5,6 @@ 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. -.. tip:: - - The bundle overriding mechanism means that you cannot use physical paths to - refer to bundle's resources (e.g. ``__DIR__/config/services.xml``). Always - use logical paths in your bundles (e.g. ``@FooBundle/Resources/config/services.xml``) - and call the :ref:`locateResource() method ` - to turn them into physical paths when needed. - .. _override-templates: Templates diff --git a/cache.rst b/cache.rst index b0d65f52740..c073a98387f 100644 --- a/cache.rst +++ b/cache.rst @@ -50,6 +50,8 @@ of: 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 @@ -119,8 +121,14 @@ The Cache component comes with a series of adapters pre-configured: ``cache.adapter.redis_tag_aware`` has been introduced in Symfony 5.2. -Some of these adapters could be configured via shortcuts. Using these shortcuts -will create pools with service IDs that follow the pattern ``cache.[type]``. +.. 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:: @@ -131,16 +139,16 @@ will create pools with service IDs that follow the pattern ``cache.[type]``. cache: directory: '%kernel.cache_dir%/pools' # Only used with cache.adapter.filesystem - # service: cache.doctrine_dbal default_doctrine_dbal_provider: 'doctrine.dbal.default_connection' - # service: cache.psr6 default_psr6_provider: 'app.my_psr6_service' - # service: cache.redis default_redis_provider: 'redis://localhost' - # service: cache.memcached default_memcached_provider: 'memcached://localhost' - # service: cache.pdo - default_pdo_provider: 'pgsql:host=localhost' + default_pdo_provider: 'app.my_pdo_service' + + services: + app.my_pdo_service: + class: \PDO + arguments: ['pgsql:host=localhost'] .. code-block:: xml @@ -155,43 +163,44 @@ will create pools with service IDs that follow the pattern ``cache.[type]``. https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" > - + + + + pgsql:host=localhost + + .. code-block:: php // config/packages/cache.php + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework, ContainerConfigurator $container) { $framework->cache() // Only used with cache.adapter.filesystem ->directory('%kernel.cache_dir%/pools') - // Service: cache.doctrine_dbal + ->defaultDoctrineDbalProvider('doctrine.dbal.default_connection') - // Service: cache.psr6 ->defaultPsr6Provider('app.my_psr6_service') - // Service: cache.redis ->defaultRedisProvider('redis://localhost') - // Service: cache.memcached ->defaultMemcachedProvider('memcached://localhost') - // Service: cache.pdo - ->defaultPdoProvider('pgsql:host=localhost') + ->defaultPdoProvider('app.my_pdo_service') + ; + + $container->services() + ->set('app.my_pdo_service', \PDO::class) + ->args(['pgsql:host=localhost']) ; }; @@ -472,7 +481,6 @@ and use that when configuring the pool. ->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') @@ -564,7 +572,7 @@ 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 key could be invalidated with one function call:: +the same tag could be invalidated with one function call:: use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; @@ -610,8 +618,7 @@ to enable this feature. This could be added by using the following configuration cache: pools: my_cache_pool: - adapter: cache.adapter.redis - tags: true + adapter: cache.adapter.redis_tag_aware .. code-block:: xml @@ -786,7 +793,7 @@ Then, register the ``SodiumMarshaller`` service using this key: - ['%env(base64:CACHE_DECRYPTION_KEY)%'] # use multiple keys in order to rotate them #- ['%env(base64:CACHE_DECRYPTION_KEY)%', '%env(base64:OLD_CACHE_DECRYPTION_KEY)%'] - - '@Symfony\Component\Cache\Marshaller\SodiumMarshaller.inner' + - '@.inner' .. code-block:: xml @@ -809,7 +816,7 @@ Then, register the ``SodiumMarshaller`` service using this key: - + @@ -826,7 +833,7 @@ Then, register the ``SodiumMarshaller`` service using this key: ->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(SodiumMarshaller::class.'.inner')); + ->addArgument(new Reference('.inner')); .. caution:: @@ -837,3 +844,148 @@ 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 +------------------------------------- + +.. versionadded:: 5.2 + + The feature to compute cache values asynchronously was introduced in Symfony 5.2. + +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\Annotation\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 index df4a6aa3121..e515b41395c 100644 --- a/components/asset.rst +++ b/components/asset.rst @@ -211,19 +211,19 @@ every day:: class DateVersionStrategy implements VersionStrategyInterface { - private $version; + private string $version; public function __construct() { $this->version = date('Ymd'); } - public function getVersion(string $path) + public function getVersion(string $path): string { return $this->version; } - public function applyVersion(string $path) + public function applyVersion(string $path): string { return sprintf('%s?v=%s', $path, $this->getVersion($path)); } diff --git a/components/cache.rst b/components/cache.rst index ff650ee13c3..857282eb1d0 100644 --- a/components/cache.rst +++ b/components/cache.rst @@ -90,6 +90,8 @@ generate and return the value:: Use cache tags to delete more than one key at the time. Read more at :doc:`/components/cache/cache_invalidation`. +.. _cache_stampede-prevention: + Stampede Prevention ~~~~~~~~~~~~~~~~~~~ @@ -139,7 +141,6 @@ The following cache adapters are available: cache/adapters/* - .. _cache-component-psr6-caching: Generic Caching (PSR-6) diff --git a/components/cache/adapters/couchbasebucket_adapter.rst b/components/cache/adapters/couchbasebucket_adapter.rst index 5312371a2bb..172a8fe0f19 100644 --- a/components/cache/adapters/couchbasebucket_adapter.rst +++ b/components/cache/adapters/couchbasebucket_adapter.rst @@ -39,7 +39,6 @@ the second and third parameters:: $defaultLifetime ); - Configure the Connection ------------------------ @@ -71,7 +70,6 @@ helper method allows creating and configuring a `Couchbase Bucket`_ class instan 'couchbase:?host[localhost]&host[localhost:12345]' ); - Configure the Options --------------------- diff --git a/components/cache/adapters/couchbasecollection_adapter.rst b/components/cache/adapters/couchbasecollection_adapter.rst index 66586c816ee..296b7065f1d 100644 --- a/components/cache/adapters/couchbasecollection_adapter.rst +++ b/components/cache/adapters/couchbasecollection_adapter.rst @@ -36,7 +36,6 @@ the second and third parameters:: $defaultLifetime ); - Configure the Connection ------------------------ @@ -68,7 +67,6 @@ helper method allows creating and configuring a `Couchbase Collection`_ class in 'couchbase:?host[localhost]&host[localhost:12345]' ); - Configure the Options --------------------- diff --git a/components/cache/adapters/filesystem_adapter.rst b/components/cache/adapters/filesystem_adapter.rst index 4c447b3de82..26ef48af27c 100644 --- a/components/cache/adapters/filesystem_adapter.rst +++ b/components/cache/adapters/filesystem_adapter.rst @@ -63,6 +63,5 @@ adapter offers better read performance when using tag-based invalidation:: $cache = new FilesystemTagAwareAdapter(); - .. _`tmpfs`: https://wiki.archlinux.org/index.php/tmpfs .. _`RAM disk solutions`: https://en.wikipedia.org/wiki/List_of_RAM_drive_software diff --git a/components/cache/adapters/pdo_adapter.rst b/components/cache/adapters/pdo_adapter.rst index 34815a51eb0..9cfbfd7bdfa 100644 --- a/components/cache/adapters/pdo_adapter.rst +++ b/components/cache/adapters/pdo_adapter.rst @@ -41,7 +41,7 @@ your code. .. deprecated:: 5.4 Using :class:`Symfony\\Component\\Cache\\Adapter\\PdoAdapter` with a - :class:`Doctrine\\DBAL\\Connection` or a DBAL URL is deprecated since Symfony 5.4 + ``Doctrine\DBAL\Connection`` or a DBAL URL is deprecated since Symfony 5.4 and will be removed in Symfony 6.0. Use :class:`Symfony\\Component\\Cache\\Adapter\\DoctrineDbalAdapter` instead. diff --git a/components/cache/adapters/redis_adapter.rst b/components/cache/adapters/redis_adapter.rst index dc711d6b8e0..2b00058c6bd 100644 --- a/components/cache/adapters/redis_adapter.rst +++ b/components/cache/adapters/redis_adapter.rst @@ -44,7 +44,7 @@ as the second and third parameters:: Configure the Connection ------------------------ -The :method:`Symfony\\Component\\Cache\\Adapter\\RedisAdapter::createConnection` +The :method:`Symfony\\Component\\Cache\\Traits\\RedisTrait::createConnection` helper method allows creating and configuring the Redis client class instance using a `Data Source Name (DSN)`_:: @@ -55,24 +55,18 @@ helper method allows creating and configuring the Redis client class instance us 'redis://localhost' ); -The DSN can specify either an IP/host (and an optional port) or a socket path, as well as a -password and a database index. To enable TLS for connections, the scheme ``redis`` must be -replaced by ``rediss`` (the second ``s`` means "secure"). +The DSN can specify either an IP/host (and an optional port) or a socket path, as +well as a database index. To enable TLS for connections, the scheme ``redis`` must +be replaced by ``rediss`` (the second ``s`` means "secure"). .. note:: - A `Data Source Name (DSN)`_ for this adapter must use either one of the following formats. + A `Data Source Name (DSN)`_ for this adapter must use the following format. .. code-block:: text redis[s]://[pass@][ip|host|socket[:port]][/db-index] - .. code-block:: text - - redis[s]:[[user]:pass@]?[ip|host|socket[:port]][¶ms] - - Values for placeholders ``[user]``, ``[:port]``, ``[/db-index]`` and ``[¶ms]`` are optional. - Below are common examples of valid DSNs showing a combination of available values:: use Symfony\Component\Cache\Adapter\RedisAdapter; @@ -89,11 +83,8 @@ Below are common examples of valid DSNs showing a combination of available value // socket "/var/run/redis.sock" and auth "bad-pass" RedisAdapter::createConnection('redis://bad-pass@/var/run/redis.sock'); - // host "redis1" (docker container) with alternate DSN syntax and selecting database index "3" - RedisAdapter::createConnection('redis:?host[redis1:6379]&dbindex=3'); - - // providing credentials with alternate DSN syntax - RedisAdapter::createConnection('redis:default:verysecurepassword@?host[redis1:6379]&dbindex=3'); + // a single DSN can define multiple servers using the following syntax: + // host[hostname-or-IP:port] (where port is optional). Sockets must include a trailing ':' // a single DSN can also define multiple servers RedisAdapter::createConnection( @@ -108,16 +99,6 @@ parameter to set the name of your service group:: 'redis:?host[redis1:26379]&host[redis2:26379]&host[redis3:26379]&redis_sentinel=mymaster' ); - // providing credentials - RedisAdapter::createConnection( - 'redis:default:verysecurepassword@?host[redis1:26379]&host[redis2:26379]&host[redis3:26379]&redis_sentinel=mymaster' - ); - - // providing credentials and selecting database index "3" - RedisAdapter::createConnection( - 'redis:default:verysecurepassword@?host[redis1:26379]&host[redis2:26379]&host[redis3:26379]&redis_sentinel=mymaster&dbindex=3' - ); - .. note:: See the :class:`Symfony\\Component\\Cache\\Traits\\RedisTrait` for more options @@ -195,7 +176,7 @@ Available Options ``redis_cluster`` (type: ``bool``, default: ``false``) Enables or disables redis cluster. The actual value passed is irrelevant as long as it passes loose comparison - checks: `redis_cluster=1` will suffice. + checks: ``redis_cluster=1`` will suffice. ``redis_sentinel`` (type: ``string``, default: ``null``) Specifies the master name connected to the sentinels. @@ -250,8 +231,8 @@ Read more about this topic in the official `Redis LRU Cache Documentation`_. .. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name .. _`Redis server`: https://redis.io/ .. _`Redis`: https://github.com/phpredis/phpredis -.. _`RedisArray`: https://github.com/phpredis/phpredis/blob/master/arrays.markdown#readme -.. _`RedisCluster`: https://github.com/phpredis/phpredis/blob/master/cluster.markdown#readme +.. _`RedisArray`: https://github.com/phpredis/phpredis/blob/develop/arrays.md +.. _`RedisCluster`: https://github.com/phpredis/phpredis/blob/develop/cluster.md .. _`Predis`: https://packagist.org/packages/predis/predis .. _`Predis Connection Parameters`: https://github.com/nrk/predis/wiki/Connection-Parameters#list-of-connection-parameters .. _`TCP-keepalive`: https://redis.io/topics/clients#tcp-keepalive diff --git a/components/cache/cache_items.rst b/components/cache/cache_items.rst index 9f020a39de9..475a9c59367 100644 --- a/components/cache/cache_items.rst +++ b/components/cache/cache_items.rst @@ -12,9 +12,8 @@ Cache Item Keys and Values The **key** of a cache item is a plain string which acts as its identifier, so it must be unique for each cache pool. You can freely choose the keys, but they should only contain letters (A-Z, a-z), numbers (0-9) and the -``_`` and ``.`` symbols. Other common symbols (such as ``{``, ``}``, ``(``, -``)``, ``/``, ``\``, ``@`` and ``:``) are reserved by the PSR-6 standard for future -uses. +``_`` and ``.`` symbols. Other common symbols (such as ``{ } ( ) / \ @ :``) are +reserved by the PSR-6 standard for future uses. The **value** of a cache item can be any data represented by a type which is serializable by PHP, such as basic types (string, integer, float, boolean, null), diff --git a/components/cache/cache_pools.rst b/components/cache/cache_pools.rst index c92a22a136b..3a0897defcf 100644 --- a/components/cache/cache_pools.rst +++ b/components/cache/cache_pools.rst @@ -25,7 +25,6 @@ ready to use in your applications. adapters/* - Using the Cache Contracts ------------------------- diff --git a/components/config.rst b/components/config.rst index 579d5b3149d..9de03f1f869 100644 --- a/components/config.rst +++ b/components/config.rst @@ -1,9 +1,17 @@ The Config Component ==================== - The Config component provides several classes to help you find, load, - combine, fill and validate configuration values of any kind, whatever - their source may be (YAML, XML, INI files, or for instance a database). +The Config component provides utilities to define and manage the configuration +options of PHP applications. It allows you to: + +* Define a configuration structure, its validation rules, default values and documentation; +* Support different configuration formats (YAML, XML, INI, etc.); +* Merge multiple configurations from different sources into a single configuration. + +.. note:: + + You don't have to use this component to configure Symfony applications. + Instead, read the docs about :doc:`how to configure Symfony applications `. Installation ------------ diff --git a/components/config/definition.rst b/components/config/definition.rst index 437c93322d6..c076838d1f9 100644 --- a/components/config/definition.rst +++ b/components/config/definition.rst @@ -81,7 +81,7 @@ reflect the real structure of the configuration values:: ->defaultTrue() ->end() ->scalarNode('default_connection') - ->defaultValue('default') + ->defaultValue('mysql') ->end() ->end() ; diff --git a/components/console/events.rst b/components/console/events.rst index dc03a8a88d9..92659aac82a 100644 --- a/components/console/events.rst +++ b/components/console/events.rst @@ -17,7 +17,8 @@ the wheel, it uses the Symfony EventDispatcher component to do the work:: .. caution:: Console events are only triggered by the main command being executed. - Commands called by the main command will not trigger any event. + Commands called by the main command will not trigger any event, unless + run by the application itself, see :doc:`/console/calling_commands`. The ``ConsoleEvents::COMMAND`` Event ------------------------------------ @@ -151,6 +152,8 @@ Listeners receive a It is then dispatched just after the ``ConsoleEvents::ERROR`` event. The exit code received in this case is the exception code. +.. _console_signal-event: + The ``ConsoleEvents::SIGNAL`` Event ----------------------------------- @@ -171,10 +174,10 @@ Listeners receive a use Symfony\Component\Console\Event\ConsoleSignalEvent; $dispatcher->addListener(ConsoleEvents::SIGNAL, function (ConsoleSignalEvent $event) { - + // gets the signal number $signal = $event->getHandlingSignal(); - + if (\SIGINT === $signal) { echo "bye bye!"; } @@ -183,7 +186,8 @@ Listeners receive a .. tip:: All the available signals (``SIGINT``, ``SIGQUIT``, etc.) are defined as - `constants of the PCNTL PHP extension`_. + `constants of the PCNTL PHP extension`_. The extension has to be installed + for these constants to be available. If you use the Console component inside a Symfony application, commands can handle signals themselves. To do so, implement the diff --git a/components/console/helpers/cursor.rst b/components/console/helpers/cursor.rst index e6d3ea3c78e..b070fd31dd6 100644 --- a/components/console/helpers/cursor.rst +++ b/components/console/helpers/cursor.rst @@ -11,7 +11,7 @@ cursor position in a console command. This allows you to write on any position of the output: .. image:: /_images/components/console/cursor.gif - :align: center + :alt: A command outputs on various positions on the screen, eventually drawing the letters "SF". .. code-block:: php diff --git a/components/console/helpers/debug_formatter.rst b/components/console/helpers/debug_formatter.rst index a5567fe63ed..711d0bd5356 100644 --- a/components/console/helpers/debug_formatter.rst +++ b/components/console/helpers/debug_formatter.rst @@ -8,7 +8,7 @@ the results of running ``figlet symfony``, it might output something like this: .. image:: /_images/components/console/debug_formatter.png - :align: center + :alt: Console output, with the first line showing "RUN Running figlet", followed by lines showing the output of the command prefixed with "OUT" and "RES Finished the command" as last line in the output. Using the debug_formatter ------------------------- diff --git a/components/console/helpers/formatterhelper.rst b/components/console/helpers/formatterhelper.rst index 4e3f11940fd..3cb87c4c307 100644 --- a/components/console/helpers/formatterhelper.rst +++ b/components/console/helpers/formatterhelper.rst @@ -13,7 +13,13 @@ in the default helper set and you can get it by calling The methods return a string, which you'll usually render to the console by passing it to the -:method:`OutputInterface::writeln ` method. +:method:`OutputInterface::writeln ` +method. + +.. note:: + + As an alternative, consider using the + :ref:`SymfonyStyle ` to display stylized blocks. Print Messages in a Section --------------------------- @@ -58,8 +64,9 @@ block will be formatted with more padding (one blank line above and below the messages and 2 spaces on the left and right). The exact "style" you use in the block is up to you. In this case, you're using -the pre-defined ``error`` style, but there are other styles, or you can create -your own. See :doc:`/console/coloring`. +the pre-defined ``error`` style, but there are other styles (``info``, +``comment``, ``question``), or you can create your own. +See :doc:`/console/coloring`. Print Truncated Messages ------------------------ @@ -81,7 +88,7 @@ And the output will be: This is... -The message is truncated to the given length, then the suffix is appended to end +The message is truncated to the given length, then the suffix is appended to the end of that string. Negative String Length @@ -103,7 +110,7 @@ Custom Suffix By default, the ``...`` suffix is used. If you wish to use a different suffix, pass it as the third argument to the method. -The suffix is always appended, unless truncate length is longer than a message +The suffix is always appended, unless truncated length is longer than a message and a suffix length. If you don't want to use suffix at all, pass an empty string:: @@ -113,3 +120,28 @@ If you don't want to use suffix at all, pass an empty string:: $truncatedMessage = $formatter->truncate('test', 10); // result: test // because length of the "test..." string is shorter than 10 + +Formatting Time +--------------- + +Sometimes you want to format seconds to time. This is possible with the +:method:`Symfony\\Component\\Console\\Helper\\Helper::formatTime` method. +The first argument is the seconds to format and the second argument is the +precision (default ``1``) of the result:: + + Helper::formatTime(42); // 42 secs + Helper::formatTime(125); // 2 mins + Helper::formatTime(125, 2); // 2 mins, 5 secs + Helper::formatTime(172799, 4); // 1 day, 23 hrs, 59 mins, 59 secs + +Formatting Memory +----------------- + +Sometimes you want to format memory to GiB, MiB, KiB and B. This is possible with the +:method:`Symfony\\Component\\Console\\Helper\\Helper::formatMemory` method. +The only argument is the memory size to format:: + + Helper::formatMemory(512); // 512 B + Helper::formatMemory(1024); // 1 KiB + Helper::formatMemory(1024 * 1024); // 1.0 MiB + Helper::formatMemory(1024 * 1024 * 1024); // 1 GiB diff --git a/components/console/helpers/processhelper.rst b/components/console/helpers/processhelper.rst index ef462cef731..875b48ab3f8 100644 --- a/components/console/helpers/processhelper.rst +++ b/components/console/helpers/processhelper.rst @@ -1,11 +1,12 @@ Process Helper ============== -The Process Helper shows processes as they're running and reports -useful information about process status. +The Process Helper shows processes as they're running and reports useful +information about process status. -To display process details, use the :class:`Symfony\\Component\\Console\\Helper\\ProcessHelper` -and run your command with verbosity. For example, running the following code with +To display process details, use the +:class:`Symfony\\Component\\Console\\Helper\\ProcessHelper` and run your command +with verbosity. For example, running the following code with a very verbose verbosity (e.g. ``-vv``):: use Symfony\Component\Process\Process; @@ -18,14 +19,25 @@ a very verbose verbosity (e.g. ``-vv``):: will result in this output: .. image:: /_images/components/console/process-helper-verbose.png + :alt: Console output showing two lines: "RUN 'figlet' 'Symfony'" and "RES Command ran successfully". It will result in more detailed output with debug verbosity (e.g. ``-vvv``): .. image:: /_images/components/console/process-helper-debug.png + :alt: In between the command line and the result line, the command's output is now shown prefixed by "OUT". In case the process fails, debugging is easier: .. image:: /_images/components/console/process-helper-error-debug.png + :alt: The last line shows "RES 127 Command dit not run successfully", and the output lines show more the error information from the command. + +.. note:: + + By default, the process helper uses the error output (``stderr``) as + its default output. This behavior can be changed by passing an instance of + :class:`Symfony\\Component\\Console\\Output\\StreamOutput` to the + :method:`Symfony\\Component\\Console\\Helper\\ProcessHelper::run` + method. Arguments --------- diff --git a/components/console/helpers/progressbar.rst b/components/console/helpers/progressbar.rst index 3e43452a737..4c5cb6da56b 100644 --- a/components/console/helpers/progressbar.rst +++ b/components/console/helpers/progressbar.rst @@ -5,6 +5,12 @@ When executing longer-running commands, it may be helpful to show progress information, which updates as your command runs: .. image:: /_images/components/console/progressbar.gif + :alt: Console output showing a progress bar advance to 100%, with the estimated time left, the memory usage and a special message that changes when the bar closes completion. + +.. note:: + + As an alternative, consider using the + :ref:`SymfonyStyle ` to display a progress bar. To display progress details, use the :class:`Symfony\\Component\\Console\\Helper\\ProgressBar`, pass it a total @@ -38,6 +44,14 @@ number of units, and advance the progress as the command executes:: ``$progress->advance()`` with a negative value. For example, if you call ``$progress->advance(-2)`` then it will regress the progress bar 2 steps. +.. note:: + + By default, the progress bar helper uses the error output (``stderr``) as + its default output. This behavior can be changed by passing an instance of + :class:`Symfony\\Component\\Console\\Output\\StreamOutput` to the + :class:`Symfony\\Component\\Console\\Helper\\ProgressBar` + constructor. + Instead of advancing the bar by a number of steps (with the :method:`Symfony\\Component\\Console\\Helper\\ProgressBar::advance` method), you can also set the current progress by calling the diff --git a/components/console/helpers/questionhelper.rst b/components/console/helpers/questionhelper.rst index 01c7174e723..d5d08f863b8 100644 --- a/components/console/helpers/questionhelper.rst +++ b/components/console/helpers/questionhelper.rst @@ -15,6 +15,11 @@ first argument, an :class:`Symfony\\Component\\Console\\Output\\OutputInterface` instance as the second argument and a :class:`Symfony\\Component\\Console\\Question\\Question` as last argument. +.. note:: + + As an alternative, consider using the + :ref:`SymfonyStyle ` to ask questions. + Asking the User for Confirmation -------------------------------- @@ -47,7 +52,9 @@ the following to your command:: } In this case, the user will be asked "Continue with this action?". If the user -answers with ``y`` it returns ``true`` or ``false`` if they answer with ``n``. +answers with ``y`` (or any word, expression starting with ``y`` due to default +answer regex, e.g ``yeti``) it returns ``true`` or ``false`` otherwise, e.g. ``n``. + The second argument to :method:`Symfony\\Component\\Console\\Question\\ConfirmationQuestion::__construct` is the default value to return if the user doesn't enter any valid input. If @@ -67,6 +74,14 @@ the second argument is not provided, ``true`` is assumed. The regex defaults to ``/^y/i``. +.. note:: + + By default, the question helper uses the error output (``stderr``) as + its default output. This behavior can be changed by passing an instance of + :class:`Symfony\\Component\\Console\\Output\\StreamOutput` to the + :method:`Symfony\\Component\\Console\\Helper\\QuestionHelper::ask` + method. + Asking the User for Information ------------------------------- @@ -82,9 +97,9 @@ if you want to know a bundle name, you can add this to your command:: $question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle'); $bundleName = $helper->ask($input, $output, $question); - + // ... do something with the bundleName - + return Command::SUCCESS; } @@ -98,8 +113,10 @@ Let the User Choose from a List of Answers If you have a predefined set of answers the user can choose from, you could use a :class:`Symfony\\Component\\Console\\Question\\ChoiceQuestion` -which makes sure that the user can only enter a valid string -from a predefined list:: +which makes sure that the user can only enter a valid string or the index +of the choice from a predefined list. In the example below, typing ``blue`` +or ``1`` is the same choice for the user. A default value is set with ``0`` +but ``red`` could be set instead (could be more explicit):: use Symfony\Component\Console\Question\ChoiceQuestion; @@ -120,7 +137,7 @@ from a predefined list:: $output->writeln('You have just selected: '.$color); // ... do something with the color - + return Command::SUCCESS; } @@ -162,12 +179,14 @@ this use :method:`Symfony\\Component\\Console\\Question\\ChoiceQuestion::setMult $colors = $helper->ask($input, $output, $question); $output->writeln('You have just selected: ' . implode(', ', $colors)); - + return Command::SUCCESS; } Now, when the user enters ``1,2``, the result will be: -``You have just selected: blue, yellow``. +``You have just selected: blue, yellow``. The user can also enter strings +(e.g. ``blue,yellow``) and even mix strings and the index of the choices +(e.g. ``blue,2``). If the user does not enter anything, the result will be: ``You have just selected: red, blue``. @@ -191,9 +210,9 @@ will be autocompleted as the user types:: $question->setAutocompleterValues($bundles); $bundleName = $helper->ask($input, $output, $question); - + // ... do something with the bundleName - + return Command::SUCCESS; } @@ -230,9 +249,9 @@ provide a callback function to dynamically generate suggestions:: $question->setAutocompleterCallback($callback); $filePath = $helper->ask($input, $output, $question); - + // ... do something with the filePath - + return Command::SUCCESS; } @@ -254,9 +273,9 @@ You can also specify if you want to not trim the answer by setting it directly w $question->setTrimmable(false); // if the users inputs 'elsa ' it will not be trimmed and you will get 'elsa ' as value $name = $helper->ask($input, $output, $question); - + // ... do something with the name - + return Command::SUCCESS; } @@ -285,9 +304,9 @@ the response to a question should allow multiline answers by passing ``true`` to $question->setMultiline(true); $answer = $helper->ask($input, $output, $question); - + // ... do something with the answer - + return Command::SUCCESS; } @@ -313,9 +332,9 @@ convenient for passwords:: $question->setHiddenFallback(false); $password = $helper->ask($input, $output, $question); - + // ... do something with the password - + return Command::SUCCESS; } @@ -347,7 +366,7 @@ convenient for passwords:: QuestionHelper::disableStty(); // ... - + return Command::SUCCESS; } @@ -376,9 +395,9 @@ method:: }); $bundleName = $helper->ask($input, $output, $question); - + // ... do something with the bundleName - + return Command::SUCCESS; } @@ -420,9 +439,9 @@ method:: $question->setMaxAttempts(2); $bundleName = $helper->ask($input, $output, $question); - + // ... do something with the bundleName - + return Command::SUCCESS; } @@ -482,9 +501,9 @@ You can also use a validator with a hidden question:: $question->setMaxAttempts(20); $password = $helper->ask($input, $output, $question); - + // ... do something with the password - + return Command::SUCCESS; } diff --git a/components/console/helpers/table.rst b/components/console/helpers/table.rst index e3bb282ed7e..171412511aa 100644 --- a/components/console/helpers/table.rst +++ b/components/console/helpers/table.rst @@ -14,6 +14,11 @@ When building a console application it may be useful to display tabular data: | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+--------------------------+------------------+ +.. note:: + + As an alternative, consider using the + :ref:`SymfonyStyle ` to display a table. + To display a table, use :class:`Symfony\\Component\\Console\\Helper\\Table`, set the headers, set the rows and then render the table:: @@ -38,7 +43,7 @@ set the headers, set the rows and then render the table:: ]) ; $table->render(); - + return Command::SUCCESS; } } @@ -194,7 +199,7 @@ You can also set the style to ``box``:: which outputs: -.. code-block:: text +.. code-block:: terminal ┌───────────────┬──────────────────────────┬──────────────────┐ │ ISBN │ Title │ Author │ @@ -212,7 +217,7 @@ You can also set the style to ``box-double``:: which outputs: -.. code-block:: text +.. code-block:: terminal ╔═══════════════╤══════════════════════════╤══════════════════╗ ║ ISBN │ Title │ Author ║ @@ -414,7 +419,7 @@ The only requirement to append rows is that the table must be rendered inside a $table->render(); $table->appendRow(['Symfony']); - + return Command::SUCCESS; } } @@ -427,3 +432,24 @@ This will display the following table in the terminal: | Love | | Symfony | +---------+ + +.. tip:: + + You can create multiple lines using the :method:`Symfony\\Component\\Console\\Helper\\Table::addRows` method:: + + // ... + $table->addRows([ + ['Hello', 'World'], + ['Love', 'Symfony'], + ]); + $table->render(); + // ... + + This will display: + + .. code-block:: terminal + + +-------+---------+ + | Hello | World | + | Love | Symfony | + +-------+---------+ diff --git a/components/console/usage.rst b/components/console/usage.rst index a38b06c2cc4..d7725e8926e 100644 --- a/components/console/usage.rst +++ b/components/console/usage.rst @@ -104,7 +104,7 @@ If you do not provide a console name then it will just output: .. code-block:: text - console tool + Console Tool You can force turning on ANSI output coloring with: diff --git a/components/dependency_injection/compilation.rst b/components/dependency_injection/compilation.rst index edaa8be8f47..beedbf33853 100644 --- a/components/dependency_injection/compilation.rst +++ b/components/dependency_injection/compilation.rst @@ -150,7 +150,7 @@ will look like this:: ], ] -Whilst you can manually manage merging the different files, it is much better +While you can manually manage merging the different files, it is much better to use :doc:`the Config component ` to merge and validate the config values. Using the configuration processing you could access the config value this way:: diff --git a/components/event_dispatcher.rst b/components/event_dispatcher.rst index cc4367f8723..c3bf0bae1b2 100644 --- a/components/event_dispatcher.rst +++ b/components/event_dispatcher.rst @@ -28,7 +28,7 @@ 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 throws an +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 @@ -69,17 +69,6 @@ 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. -Naming Conventions -.................. - -The unique event name can be any string, but optionally follows a few -naming conventions: - -* Use only lowercase letters, numbers, dots (``.``) and underscores (``_``); -* Prefix names with a namespace followed by a dot (e.g. ``order.*``, ``user.*``); -* End names with a verb that indicates what action has been taken (e.g. - ``order.placed``). - Event Names and Event Objects ............................. @@ -259,7 +248,7 @@ system flexible and decoupled. Creating an Event Class ....................... -Suppose you want to create a new event - ``order.placed`` - that is dispatched +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:: @@ -270,19 +259,12 @@ order. Start by creating this custom event class and documenting it:: use Symfony\Contracts\EventDispatcher\Event; /** - * The order.placed event is dispatched each time an order is created - * in the system. + * This event is dispatched each time an order + * is placed in the system. */ - class OrderPlacedEvent extends Event + final class OrderPlacedEvent extends Event { - public const NAME = 'order.placed'; - - protected $order; - - public function __construct(Order $order) - { - $this->order = $order; - } + public function __construct(private Order $order) {} public function getOrder(): Order { @@ -292,15 +274,6 @@ order. Start by creating this custom event class and documenting it:: Each listener now has access to the order via the ``getOrder()`` method. -.. 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. - Dispatch the Event .................. @@ -318,12 +291,38 @@ of the event to dispatch:: // creates the OrderPlacedEvent and dispatches it $event = new OrderPlacedEvent($order); - $dispatcher->dispatch($event, OrderPlacedEvent::NAME); + $dispatcher->dispatch($event); Notice that the special ``OrderPlacedEvent`` object is created and passed to -the ``dispatch()`` method. Now, any listener to the ``order.placed`` +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 @@ -340,7 +339,7 @@ events it should subscribe to. It implements the 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 ``order.placed`` events:: +``kernel.response`` and ``OrderPlacedEvent::class`` events:: namespace Acme\Store\Event; @@ -358,7 +357,7 @@ Take the following example of a subscriber that subscribes to the ['onKernelResponsePre', 10], ['onKernelResponsePost', -10], ], - OrderPlacedEvent::NAME => 'onStoreOrder', + OrderPlacedEvent::class => 'onPlacedOrder', ]; } @@ -372,8 +371,9 @@ Take the following example of a subscriber that subscribes to the // ... } - public function onStoreOrder(OrderPlacedEvent $event) + public function onPlacedOrder(OrderPlacedEvent $event): void { + $order = $event->getOrder(); // ... } } @@ -417,14 +417,14 @@ inside a listener via the use Acme\Store\Event\OrderPlacedEvent; - public function onStoreOrder(OrderPlacedEvent $event) + public function onPlacedOrder(OrderPlacedEvent $event): void { // ... $event->stopPropagation(); } -Now, any listeners to ``order.placed`` that have not yet been called will +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 @@ -480,9 +480,8 @@ Learn More .. toctree:: :maxdepth: 1 - :glob: - event_dispatcher + /components/event_dispatcher/generic_event * :ref:`The kernel.event_listener tag ` * :ref:`The kernel.event_subscriber tag ` diff --git a/components/event_dispatcher/generic_event.rst b/components/event_dispatcher/generic_event.rst index dbc37cbe752..8fba7c41940 100644 --- a/components/event_dispatcher/generic_event.rst +++ b/components/event_dispatcher/generic_event.rst @@ -99,4 +99,3 @@ Filtering data:: $event['data'] = strtolower($event['data']); } } - diff --git a/components/expression_language.rst b/components/expression_language.rst index 192d5c2d134..1ddd0fddb30 100644 --- a/components/expression_language.rst +++ b/components/expression_language.rst @@ -22,8 +22,8 @@ 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 some examples, the Symfony Framework -uses expressions in security, for validation rules and in route matching. +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*. @@ -43,9 +43,10 @@ way without using PHP and without introducing security problems: # Send an alert when product.stock < 15 -Expressions can be seen as a very restricted PHP sandbox and are immune to -external injections as you must explicitly declare which variables are available -in an expression. +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 ----- @@ -111,13 +112,6 @@ expressions (e.g. the request, the current user, etc.): * :doc:`Variables available in service container expressions `; * :ref:`Variables available in routing expressions `. -.. caution:: - - When using variables in expressions, avoid passing untrusted data into the - array of variables. If you can't avoid that, sanitize non-alphanumeric - characters in untrusted data to prevent malicious users from injecting - control characters and altering the expression. - .. _expression-language-caching: Caching @@ -195,7 +189,7 @@ It's difficult to manipulate or inspect the expressions created with the Express 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, a ExpressionLanguage AST is a set of +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 @@ -358,7 +352,7 @@ or by using the second argument of the constructor:: class ExpressionLanguage extends BaseExpressionLanguage { - public function __construct(CacheItemPoolInterface $cache = null, array $providers = []) + public function __construct(?CacheItemPoolInterface $cache = null, array $providers = []) { // prepends the default provider to let users override it array_unshift($providers, new StringExpressionLanguageProvider()); diff --git a/components/filesystem.rst b/components/filesystem.rst index 70fdf10d63e..600fdf3ae9e 100644 --- a/components/filesystem.rst +++ b/components/filesystem.rst @@ -435,7 +435,7 @@ 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:: - Path::getLongestCommonBasePath( + $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', @@ -444,17 +444,14 @@ to check a list of paths for a common base path:: ); // => /var/www/vhosts/project/httpdocs -Use this path together with :method:`Symfony\\Component\\Filesystem\\Path::makeRelative` -to shorten the stored paths:: - - $bp = '/var/www/vhosts/project/httpdocs'; +Use this common base path to shorten the stored paths:: return [ - $bp.'/config/config.yaml', - $bp.'/config/routing.yaml', - $bp.'/config/services.yaml', - $bp.'/images/banana.gif', - $bp.'/uploads/images/nicer-banana.gif', + $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 diff --git a/components/finder.rst b/components/finder.rst index 35041ddb2b1..c696d7290ab 100644 --- a/components/finder.rst +++ b/components/finder.rst @@ -127,6 +127,30 @@ If you want to follow `symbolic links`_, use the ``followLinks()`` method:: $finder->files()->followLinks(); +Note that this method follows links but it doesn't resolve them. Consider +the following structure of files of directories: + +.. code-block:: text + + ├── 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 ~~~~~~~~~~~~~~~~~~~~~ diff --git a/components/form.rst b/components/form.rst index 78a8b652773..f8af0c71090 100644 --- a/components/form.rst +++ b/components/form.rst @@ -511,7 +511,7 @@ done by passing a special form "view" object to your template (notice the {{ form_end(form) }} .. image:: /_images/form/simple-form.png - :align: center + :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 diff --git a/components/http_foundation.rst b/components/http_foundation.rst index 68d686ff211..f1adc0effcd 100644 --- a/components/http_foundation.rst +++ b/components/http_foundation.rst @@ -353,6 +353,24 @@ analysis purposes. Use the ``anonymize()`` method from the $anonymousIpv6 = IpUtils::anonymize($ipv6); // $anonymousIpv6 = '2a01:198:603:10::' +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 + Accessing other Data ~~~~~~~~~~~~~~~~~~~~ @@ -718,7 +736,7 @@ class, which can make this even easier:: The ``JsonResponse`` class sets the ``Content-Type`` header to ``application/json`` and encodes your data to JSON when needed. -.. caution:: +.. danger:: To avoid XSSI `JSON Hijacking`_, you should pass an associative array as the outermost array to ``JsonResponse`` and not an indexed array so @@ -729,6 +747,16 @@ The ``JsonResponse`` class sets the ``Content-Type`` header to 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 ~~~~~~~~~~~~~~ @@ -829,8 +857,9 @@ Learn More /session /http_cache/* -.. _nginx: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/ +.. _nginx: https://mattbrictson.com/blog/accelerated-rails-downloads .. _Apache: https://tn123.org/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 diff --git a/components/http_kernel.rst b/components/http_kernel.rst index 604ca684264..3a367347a8d 100644 --- a/components/http_kernel.rst +++ b/components/http_kernel.rst @@ -3,8 +3,8 @@ 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), - a micro-framework (Silex) or an advanced CMS system (Drupal). + component. It's flexible enough to create a full-stack framework (Symfony) + or an advanced CMS (Drupal). Installation ------------ @@ -15,8 +15,10 @@ Installation .. include:: /components/require_autoload.rst.inc -The Workflow of a Request -------------------------- +.. _the-workflow-of-a-request: + +The Request-Response Lifecycle +------------------------------ .. seealso:: @@ -26,11 +28,10 @@ The Workflow of a Request :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 workflow in Symfony applications: +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**; @@ -67,14 +68,16 @@ that system:: Internally, :method:`HttpKernel::handle() ` - the concrete implementation of :method:`HttpKernelInterface::handle() ` - -defines a workflow that starts with a :class:`Symfony\\Component\\HttpFoundation\\Request` +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 workflow are the key to understanding how the kernel +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 @@ -488,8 +491,8 @@ as possible to the client (e.g. sending emails). .. _component-http-kernel-kernel-exception: -Handling Exceptions: the ``kernel.exception`` Event -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +9) Handling Exceptions: the ``kernel.exception`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Typical Purposes**: Handle some type of exception and create an appropriate ``Response`` to return for the exception @@ -504,7 +507,9 @@ 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 @@ -663,7 +668,9 @@ your controller). .. raw:: html - + To execute a sub request, use ``HttpKernel::handle()``, but change the second argument as follows:: @@ -703,25 +710,31 @@ look like this:: // ... } +.. 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. The key feature of the bundles is that they allow to override any -resource used by the application (config files, templates, controllers, -translation files, etc.) - -This overriding mechanism works because resources are referenced not by their -physical path but by their logical path. For example, the ``services.xml`` file -stored in the ``Resources/config/`` directory of a bundle called FooBundle is -referenced as ``@FooBundle/Resources/config/services.xml``. This logical path -will work when the application overrides that file and even if you change the -directory of FooBundle. - -The HttpKernel component provides a method called :method:`Symfony\\Component\\HttpKernel\\Kernel::locateResource` -which can be used to transform logical paths into physical paths:: +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'); diff --git a/components/lock.rst b/components/lock.rst index ea7a66f0432..e97d66862f2 100644 --- a/components/lock.rst +++ b/components/lock.rst @@ -108,12 +108,10 @@ to handle the rest of the job:: use App\Lock\RefreshTaxonomy; use Symfony\Component\Lock\Key; - use Symfony\Component\Lock\Lock; $key = new Key('article.'.$article->getId()); - $lock = new Lock( + $lock = $factory->createLockFromKey( $key, - $this->store, 300, // ttl false // autoRelease ); @@ -124,7 +122,7 @@ to handle the rest of the job:: .. note:: Don't forget to set the ``autoRelease`` argument to ``false`` in the - ``Lock`` constructor to avoid releasing the lock when the destructor is + ``Lock`` instantiation to avoid releasing the lock when the destructor is called. Not all stores are compatible with serialization and cross-process locking: for @@ -145,9 +143,9 @@ pass ``true`` as the argument of the ``acquire()`` method. This is called a lock is acquired:: use Symfony\Component\Lock\LockFactory; - use Symfony\Component\Lock\Store\RedisStore; + use Symfony\Component\Lock\Store\FlockStore; - $store = new RedisStore(new \Predis\Client('tcp://localhost:6379')); + $store = new FlockStore('/var/stores'); $factory = new LockFactory($store); $lock = $factory->createLock('pdf-creation'); @@ -269,6 +267,11 @@ Lock will be released automatically as soon as one process finishes:: } // ... +.. 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:: @@ -397,20 +400,20 @@ Locks are created and managed in ``Stores``, which are classes that implement The component includes the following built-in store types: -========================================================== ====== ======== ======== ======= -Store Scope Blocking Expiring Sharing -========================================================== ====== ======== ======== ======= -:ref:`FlockStore ` local yes no yes -:ref:`MemcachedStore ` remote no yes no -:ref:`MongoDbStore ` remote no yes no -:ref:`PdoStore ` remote no yes no -:ref:`DoctrineDbalStore ` remote no yes no -:ref:`PostgreSqlStore ` remote yes no yes -:ref:`DoctrineDbalPostgreSqlStore ` remote yes no yes -:ref:`RedisStore ` remote no yes yes -:ref:`SemaphoreStore ` local yes no no -:ref:`ZookeeperStore ` remote no no no -========================================================== ====== ======== ======== ======= +========================================================== ====== ======== ======== ======= ============= +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:: @@ -785,7 +788,7 @@ Using the above methods, a robust code would be:: $lock->refresh(); } - // Perform the task whose duration MUST be less than 5 minutes + // Perform the task whose duration MUST be less than 5 seconds } .. caution:: @@ -838,7 +841,7 @@ 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. -.. caution:: +.. danger:: Do not store locks on a volatile file system if they have to be reused in several requests. @@ -871,7 +874,7 @@ 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. -.. caution:: +.. danger:: The method ``flush()`` must not be called, or locks should be stored in a dedicated Memcached service away from Cache. @@ -944,7 +947,7 @@ have synchronized clocks. PostgreSqlStore ~~~~~~~~~~~~~~~ -The PdoStore relies on the `Advisory Locks`_ properties of the PostgreSQL +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. @@ -979,7 +982,7 @@ be lost without notifying the running processes. When the Redis service is shared and used for multiple usages, locks could be removed by mistake. -.. caution:: +.. danger:: The command ``FLUSHDB`` must not be called, or locks should be stored in a dedicated Redis service away from Cache. @@ -1074,3 +1077,4 @@ are still running. .. _`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/messenger.rst b/components/messenger.rst index 263a4dd1cca..e26e7838107 100644 --- a/components/messenger.rst +++ b/components/messenger.rst @@ -27,7 +27,9 @@ Concepts .. raw:: html - + **Sender**: Responsible for serializing and sending messages to *something*. This @@ -140,30 +142,42 @@ through the transport layer, use the ``SerializerStamp`` stamp:: 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 :doc:`/messenger/dispatch_after_current_bus`. -#. :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\\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 :doc:`/messenger/dispatch_after_current_bus`. +* :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. + +.. 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. .. versionadded:: 5.2 - The ``ErrorDetailsStamp`` stamp was introduced in Symfony 5.2. + The ``ErrorDetailsStamp`` stamp and the ``FlattenExceptionNormalizer`` + were introduced in Symfony 5.2. 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:: @@ -288,17 +302,23 @@ do is to write your own CSV receiver:: { private $serializer; private $filePath; + private $connection; public function __construct(SerializerInterface $serializer, string $filePath) { $this->serializer = $serializer; $this->filePath = $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 []; } @@ -324,7 +344,9 @@ do is to write your own CSV receiver:: public function reject(Envelope $envelope): void { // In the case of a custom connection - $this->connection->reject($this->findCustomStamp($envelope)->getId()); + $id = /* get the message id thanks to information or stamps present in the envelope */; + + $this->connection->reject($id); } } diff --git a/components/options_resolver.rst b/components/options_resolver.rst index 5f9cd2659a9..c01f727139a 100644 --- a/components/options_resolver.rst +++ b/components/options_resolver.rst @@ -801,11 +801,14 @@ method:: ->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 "hostname" is deprecated, use "host" instead.' + 'The option "%name%" is deprecated, use "host" instead.' ) ; @@ -819,9 +822,13 @@ method:: 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\\Options::offsetGet` method + :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 diff --git a/components/phpunit_bridge.rst b/components/phpunit_bridge.rst index 288989dcd87..b1965cca0d6 100644 --- a/components/phpunit_bridge.rst +++ b/components/phpunit_bridge.rst @@ -215,6 +215,8 @@ message, enclosed with ``/``. For example, with: `PHPUnit`_ will stop your test suite once a deprecation notice is triggered whose message contains the ``"foobar"`` string. +.. _making-tests-fail: + Making Tests Fail ~~~~~~~~~~~~~~~~~ @@ -325,6 +327,10 @@ 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. + .. versionadded:: 5.1 The ``quiet`` option was introduced in Symfony 5.1. diff --git a/components/process.rst b/components/process.rst index 12ee096df4e..163df6d9fdb 100644 --- a/components/process.rst +++ b/components/process.rst @@ -10,7 +10,6 @@ Installation $ composer require symfony/process - .. include:: /components/require_autoload.rst.inc Usage @@ -113,6 +112,12 @@ You can configure the options passed to the ``other_options`` argument of // this option allows a subprocess to continue running after the main script exited $process->setOptions(['create_new_console' => true]); +.. caution:: + + 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. + Using Features From the OS Shell -------------------------------- @@ -251,7 +256,7 @@ are done doing other stuff:: **synchronously** inside this event. Be aware that ``kernel.terminate`` is called only if you use PHP-FPM. -.. caution:: +.. 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 @@ -571,3 +576,4 @@ whether `TTY`_ is supported on the current operating system:: .. _`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 index 68bf5fc9e97..78b125cd391 100644 --- a/components/property_access.rst +++ b/components/property_access.rst @@ -186,7 +186,6 @@ method:: // instead of throwing an exception the following code returns null $value = $propertyAccessor->getValue($person, 'birthday'); - .. _components-property-access-magic-get: Magic ``__get()`` Method @@ -205,12 +204,22 @@ The ``getValue()`` method can also use the magic ``__get()`` method:: { return $this->children[$id]; } + + public function __isset($id): bool + { + return isset($this->children[$id]); + } } $person = new Person(); var_dump($propertyAccessor->getValue($person, 'Wouter')); // [...] +.. caution:: + + When implementing the magic ``__get()`` method, you also need to implement + ``__isset()``. + .. versionadded:: 5.2 The magic ``__get()`` method can be disabled since in Symfony 5.2. @@ -406,20 +415,21 @@ 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 PeopleList + class Team { // ... - public function joinPeople(string $people): void + public function joinTeam(string $person): void { - $this->peoples[] = $people; + $this->team[] = $person; } - public function leavePeople(string $people): void + public function leaveTeam(string $person): void { - foreach ($this->peoples as $id => $item) { - if ($people === $item) { - unset($this->peoples[$id]); + foreach ($this->team as $id => $item) { + if ($person === $item) { + unset($this->team[$id]); + break; } } @@ -429,12 +439,12 @@ Sometimes, adder and remover methods don't use the standard ``add`` or ``remove` use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyAccess\PropertyAccessor; - $list = new PeopleList(); + $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, 'peoples', ['kevin', 'wouter']); + $propertyAccessor->setValue($person, 'team', ['kevin', 'wouter']); - var_dump($person->getPeoples()); // ['kevin', 'wouter'] + var_dump($person->getTeam()); // ['kevin', 'wouter'] Instead of calling ``add()`` and ``remove()``, the PropertyAccess component will call ``join()`` and ``leave()`` methods. diff --git a/components/runtime.rst b/components/runtime.rst index c1588bac187..eba9e39661d 100644 --- a/components/runtime.rst +++ b/components/runtime.rst @@ -2,8 +2,8 @@ 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, etc. without any changes. + to make sure the application can run with runtimes like `PHP-PM`_, `ReactPHP`_, + `Swoole`_, etc. without any changes. .. versionadded:: 5.3 @@ -101,6 +101,23 @@ Use the ``APP_RUNTIME`` environment variable or by specifying the } } +If modifying the runtime class isn't enough, you can create your own runtime template: + +.. code-block:: json + + { + "require": { + "...": "..." + }, + "extra": { + "runtime": { + "autoload_template": "resources/runtime/autoload_runtime.template" + } + } + } + +Symfony provides a `runtime template file`_ that you can use to create your own. + Using the Runtime ----------------- @@ -139,7 +156,7 @@ The following arguments are supported by the ``SymfonyRuntime``: :class:`Symfony\\Component\\Console\\Application` An application for creating CLI applications. -:class:`Symfony\\Component\\Command\\Command` +:class:`Symfony\\Component\\Console\\Command\\Command` For creating one line command CLI applications (using ``Command::setCode()``). @@ -483,5 +500,8 @@ The end user will now be able to create front controller like:: return new SomeCustomPsr15Application(); }; +.. _PHP-PM: https://github.com/php-pm/php-pm +.. _Swoole: https://openswoole.com/ .. _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/serializer.rst b/components/serializer.rst index 29c008ce2c4..0da80f10e0e 100644 --- a/components/serializer.rst +++ b/components/serializer.rst @@ -8,15 +8,20 @@ In order to do so, the Serializer component follows the following schema. .. raw:: html - + -As you can see in the picture above, an array is used as an intermediary between -objects and serialized contents. 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. +When (de)serializing objects, the Serializer uses an array as the intermediary +between objects and serialized contents. 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. The Serializer deals with calling the normalizers and encoders +when serializing objects or deserializing formats. -Serialization is a complex topic. This component may not cover all your use cases out of the box, -but it can be useful for developing tools to serialize and deserialize your objects. +Serialization is a complex topic. This component may not cover all your use +cases out of the box, but it can be useful for developing tools to +serialize and deserialize your objects. Installation ------------ @@ -113,7 +118,7 @@ exists in your project:: $this->sportsperson = $sportsperson; } - public function setCreatedAt(\DateTime $createdAt = null): void + public function setCreatedAt(?\DateTime $createdAt = null): void { $this->createdAt = $createdAt; } @@ -228,7 +233,7 @@ data. Context ------- -Many Serializer features can be configured :doc:`using a context `. +Many Serializer features can be configured :ref:`using a context `. .. _component-serializer-attributes-groups: @@ -383,6 +388,7 @@ Then, create your groups definition: You are now able to serialize only attributes in the groups you want:: + use Acme\MyObj; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; @@ -399,7 +405,7 @@ You are now able to serialize only attributes in the groups you want:: $obj2 = $serializer->denormalize( ['foo' => 'foo', 'anotherProperty' => 'anotherProperty', 'bar' => 'bar'], - 'MyObj', + MyObj::class, null, ['groups' => ['group1', 'group3']] ); @@ -408,7 +414,7 @@ You are now able to serialize only attributes in the groups you want:: // To get all groups, use the special value `*` in `groups` $obj3 = $serializer->denormalize( ['foo' => 'foo', 'anotherProperty' => 'anotherProperty', 'bar' => 'bar'], - 'MyObj', + MyObj::class, null, ['groups' => ['*']] ); @@ -459,15 +465,15 @@ It is also possible to serialize only a set of specific attributes:: Only attributes that are not ignored (see below) are available. If some serialization groups are set, only attributes allowed by those groups can be used. -As for groups, attributes can be selected during both the serialization and deserialization process. +As for groups, attributes can be selected during both the serialization and deserialization processes. .. _serializer_ignoring-attributes: Ignoring Attributes ------------------- -All attributes are included by default when serializing objects. There are two -options to ignore some of those attributes. +All accessible attributes are included by default when serializing objects. +There are two options to ignore some of those attributes. Option 1: Using ``@Ignore`` Annotation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -601,7 +607,7 @@ A custom name converter can handle such cases:: public function denormalize(string $propertyName) { // removes 'org_' prefix - return 'org_' === substr($propertyName, 0, 4) ? substr($propertyName, 4) : $propertyName; + return str_starts_with($propertyName, 'org_') ? substr($propertyName, 4) : $propertyName; } } @@ -793,8 +799,8 @@ When serializing, you can set a callback to format a specific object property:: $encoder = new JsonEncoder(); // all callback parameters are optional (you can omit the ones you don't use) - $dateCallback = function ($innerObject, $outerObject, string $attributeName, string $format = null, array $context = []) { - return $innerObject instanceof \DateTime ? $innerObject->format(\DateTime::ISO8601) : ''; + $dateCallback = function ($attributeValue, $object, string $attributeName, ?string $format = null, array $context = []) { + return $attributeValue instanceof \DateTime ? $attributeValue->format(\DateTime::ATOM) : ''; }; $defaultContext = [ @@ -1099,7 +1105,8 @@ Option Description D with a ``\t`` character ``as_collection`` Always returns results as a collection, even if only ``true`` one line is decoded. -``no_headers`` Disables header in the encoded CSV ``false`` +``no_headers`` Setting to ``false`` will use first row as headers. ``false`` + ``true`` generate numeric headers. ``output_utf8_bom`` Outputs special `UTF-8 BOM`_ along with encoded data ``false`` ======================= ===================================================== ========================== @@ -1157,6 +1164,23 @@ the key ``#comment`` can be used for encoding XML comments:: You can pass the context key ``as_collection`` in order to have the results always as a collection. +.. note:: + + You may need to add some attributes on the root node:: + + $encoder = new XmlEncoder(); + $encoder->encode([ + '@attribute1' => 'foo', + '@attribute2' => 'bar', + '#' => ['foo' => ['@bar' => 'value', '#' => 'baz']] + ], 'xml'); + + // will return: + // + // + // baz + // + .. tip:: XML comments are ignored by default when decoding contents, but this @@ -1582,7 +1606,7 @@ having unique identifiers:: $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); // all callback parameters are optional (you can omit the ones you don't use) - $maxDepthHandler = function ($innerObject, $outerObject, string $attributeName, string $format = null, array $context = []) { + $maxDepthHandler = function ($innerObject, $outerObject, string $attributeName, ?string $format = null, array $context = []) { return '/foos/'.$innerObject->id; }; diff --git a/components/string.rst b/components/string.rst index 2990cd36e48..48b9a592aac 100644 --- a/components/string.rst +++ b/components/string.rst @@ -48,7 +48,7 @@ The following image displays the bytes, code points and grapheme clusters for the same word written in English (``hello``) and Hindi (``नमस्ते``): .. image:: /_images/components/string/bytes-points-graphemes.png - :align: center + :alt: Each letter in "hello" is made up of one byte, one code point and one grapheme cluster. In the Hindi translation, the first two letters ("नम") take up three bytes, one code point and one grapheme cluster. The last letters ("स्ते") each take up six bytes, two code points and one grapheme cluster. Usage ----- diff --git a/components/validator/resources.rst b/components/validator/resources.rst index 0eb5bc71e86..4baf4fbdd65 100644 --- a/components/validator/resources.rst +++ b/components/validator/resources.rst @@ -148,7 +148,7 @@ 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 :class:`Psr\\Cache\\CacheItemPoolInterface`):: +implement the PSR-6 interface ``Psr\Cache\CacheItemPoolInterface``):: use Symfony\Component\Validator\Validation; diff --git a/components/var_dumper.rst b/components/var_dumper.rst index e8a4d18d0c7..b6cb8c4b346 100644 --- a/components/var_dumper.rst +++ b/components/var_dumper.rst @@ -354,6 +354,7 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/01-simple.png + :alt: Dump output showing the array with length five and all keys and values. .. note:: @@ -371,6 +372,7 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/02-multi-line-str.png + :alt: Dump output showing the string on multiple lines in between three quotes. .. code-block:: php @@ -385,10 +387,11 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/03-object.png + :alt: Dump output showing the PropertyExample object and all three properties with their values. .. note:: - `#14` is the internal object handle. It allows comparing two + ``#14`` is the internal object handle. It allows comparing two consecutive dumps of the same object. .. code-block:: php @@ -403,6 +406,7 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/04-dynamic-property.png + :alt: Dump output showing the DynamicPropertyExample object and both declared and undeclared properties with their values. .. code-block:: php @@ -415,6 +419,7 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/05-soft-ref.png + :alt: Dump output showing the "aCircularReference" property value referencing the parent object, instead of showing all properties again. .. code-block:: php @@ -428,6 +433,7 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/06-constants.png + :alt: Dump output with the "E_WARNING" constant shown as value of "severity". .. code-block:: php @@ -441,6 +447,7 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/07-hard-ref.png + :alt: Dump output showing the referenced arrays. .. code-block:: php @@ -451,6 +458,7 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/08-virtual-property.png + :alt: Dump output of the ArrayObject. .. code-block:: php @@ -464,6 +472,7 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/09-cut.png + :alt: Dump output where the children of the Container object are hidden. .. _var-dumper-advanced: diff --git a/components/workflow.rst b/components/workflow.rst index 77a648a2e0f..2e5e1eb0aa6 100644 --- a/components/workflow.rst +++ b/components/workflow.rst @@ -22,6 +22,7 @@ process is called a *place*. You do also define *transitions* that describe the action to get from one place to another. .. image:: /_images/components/workflow/states_transitions.png + :alt: An example state diagram for a workflow, showing transitions and places. A set of places and transitions creates a **definition**. A workflow needs a ``Definition`` and a way to write the states to the objects (i.e. an @@ -54,23 +55,6 @@ The ``Workflow`` can now help you to decide what *transitions* (actions) are all on a blog post depending on what *place* (state) it is in. This will keep your domain logic in one place and not spread all over your application. -When you define multiple workflows you should consider using a ``Registry``, -which is an object that stores and provides access to different workflows. -A registry will also help you to decide if a workflow supports the object you -are trying to use it with:: - - use Acme\Entity\BlogPost; - use Acme\Entity\Newsletter; - use Symfony\Component\Workflow\Registry; - use Symfony\Component\Workflow\SupportStrategy\InstanceOfSupportStrategy; - - $blogPostWorkflow = ...; - $newsletterWorkflow = ...; - - $registry = new Registry(); - $registry->addWorkflow($blogPostWorkflow, new InstanceOfSupportStrategy(BlogPost::class)); - $registry->addWorkflow($newsletterWorkflow, new InstanceOfSupportStrategy(Newsletter::class)); - Usage ----- @@ -93,13 +77,12 @@ you can retrieve a workflow from it and use it as follows:: Initialization -------------- -If the property of your object is ``null`` and you want to set it with the +If the marking property of your object is ``null`` and you want to set it with the ``initial_marking`` from the configuration, you can call the ``getMarking()`` method to initialize the object property:: // ... $blogPost = new BlogPost(); - $workflow = $registry->get($blogPost); // initiate workflow $workflow->getMarking($blogPost); diff --git a/components/yaml.rst b/components/yaml.rst index 0f4f76ef05f..5d007738d09 100644 --- a/components/yaml.rst +++ b/components/yaml.rst @@ -239,7 +239,7 @@ And parse them by using the ``PARSE_OBJECT`` flag:: The YAML component uses PHP's ``serialize()`` method to generate a string representation of the object. -.. caution:: +.. danger:: Object serialization is specific to this implementation, other PHP YAML parsers will likely not recognize the ``php/object`` tag and non-PHP @@ -436,7 +436,7 @@ Add the ``--format`` option to get the output in JSON format: .. code-block:: terminal - $ php lint.php path/to/file.yaml --format json + $ php lint.php path/to/file.yaml --format=json .. tip:: diff --git a/configuration.rst b/configuration.rst index f201fab29fb..56bc30fcf4c 100644 --- a/configuration.rst +++ b/configuration.rst @@ -338,7 +338,6 @@ configuration file using a special syntax: wrap the parameter name in two ``%`` ]); }; - .. note:: If some parameter value includes the ``%`` character, you need to escape it @@ -589,10 +588,15 @@ different scenarios: staging, quality assurance, client review, etc.) Configuration Based on Environment Variables -------------------------------------------- -Using `environment variables`_ (or "env vars" for short) is a common practice to -configure options that depend on where the application is run (e.g. the database -credentials are usually different in production versus your local machine). If -the values are sensitive, you can even :doc:`encrypt them as secrets `. +Using `environment variables`_ (or "env vars" for short) is a common practice to: + +* Configure options that depend on where the application is run (e.g. the database + credentials are usually different in production versus your local machine); +* Configure options that can change dynamically in a production environment (e.g. + to update the value of an expired API key without having to redeploy the entire + application). + +In other cases, it's recommended to keep using :ref:`configuration parameters `. Use the special syntax ``%env(ENV_VAR_NAME)%`` to reference environment variables. The values of these options are resolved at runtime (only once per request, to @@ -669,6 +673,56 @@ To define the value of an env var, you have several options: * :ref:`Encrypt the value as a secret `; * Set the value as a real environment variable in your shell or your web server. +If your application tries to use an env var that hasn't been defined, you'll see +an exception. You can prevent that by defining a default value for the env var. +To do so, define a parameter with the same name as the env var using this syntax: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + parameters: + # if the SECRET env var value is not defined anywhere, Symfony uses this value + env(SECRET): 'some_secret' + + # ... + + .. code-block:: xml + + + + + + + + some_secret + + + + + + .. code-block:: php + + // config/packages/framework.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container, FrameworkConfig $framework) { + // if the SECRET env var value is not defined anywhere, Symfony uses this value + $container->setParameter('env(SECRET)', 'some_secret'); + + // ... + }; + .. tip:: Some hosts - like Platform.sh - offer easy `utilities to manage env vars`_ @@ -682,7 +736,7 @@ To define the value of an env var, you have several options: always exists, because its value will be ``null`` when the related env var is not defined. -.. caution:: +.. danger:: Beware that dumping the contents of the ``$_SERVER`` and ``$_ENV`` variables or outputting the ``phpinfo()`` contents will display the values of the @@ -817,6 +871,25 @@ the env files ending in ``.local`` (``.env.local`` and ``.env..loca **should not be committed** because only you will use them. In fact, the ``.gitignore`` file that comes with Symfony prevents them from being committed. +Overriding Environment Variables Defined By The System +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to override an environment variable defined by the system, use the +``overrideExistingVars`` parameter defined by the +:method:`Symfony\\Component\\Dotenv\\Dotenv::loadEnv`, +:method:`Symfony\\Component\\Dotenv\\Dotenv::bootEnv`, and +:method:`Symfony\\Component\\Dotenv\\Dotenv::populate` methods:: + + use Symfony\Component\Dotenv\Dotenv; + + $dotenv = new Dotenv(); + $dotenv->loadEnv(__DIR__.'/.env', null, 'dev', ['test'], true); + + // ... + +This will override environment variables defined by the system but it **won't** +override environment variables defined in ``.env`` files. + .. _configuration-env-var-in-prod: Configuring Environment Variables in Production @@ -826,33 +899,43 @@ In production, the ``.env`` files are also parsed and loaded on each request. So the easiest way to define env vars is by creating a ``.env.local`` file on your production server(s) with your production values. -To improve performance, you can optionally run the ``dotenv:dump`` command (available -in :ref:`Symfony Flex ` 1.2 or later). The command is not registered -by default, so you must register first in your services: +To improve performance, you can optionally run the ``dump-env`` Composer command: -.. code-block:: yaml +.. code-block:: terminal - # config/services.yaml - services: - Symfony\Component\Dotenv\Command\DotenvDumpCommand: - - '%kernel.project_dir%/.env' - - '%kernel.environment%' + # parses ALL .env files and dumps their final values to .env.local.php + $ composer dump-env prod -In PHP >= 8, you can remove the two arguments when autoconfiguration is enabled -(which is the default): +.. sidebar:: Dumping Environment Variables without Composer -.. code-block:: yaml + If you don't have Composer installed in production, you can use the + ``dotenv:dump`` command instead (available in :ref:`Symfony Flex ` + 1.2 or later). The command is not registered by default, so you must register + first in your services: - # config/services.yaml - services: - Symfony\Component\Dotenv\Command\DotenvDumpCommand: ~ + .. code-block:: yaml -Then, run the command: + # config/services.yaml + services: + Symfony\Component\Dotenv\Command\DotenvDumpCommand: + - '%kernel.project_dir%/.env' + - '%kernel.environment%' -.. code-block:: terminal + In PHP >= 8, you can remove the two arguments when autoconfiguration is enabled + (which is the default): - # parses ALL .env files and dumps their final values to .env.local.php - $ php bin/console dotenv:dump prod + .. code-block:: yaml + + # config/services.yaml + services: + Symfony\Component\Dotenv\Command\DotenvDumpCommand: ~ + + Then, run the command: + + .. code-block:: terminal + + # parses ALL .env files and dumps their final values to .env.local.php + $ php bin/console dotenv:dump prod After running this command, Symfony will load the ``.env.local.php`` file to get the environment variables and will not spend time parsing the ``.env`` files. @@ -928,6 +1011,74 @@ environment variables, with their values, referenced in Symfony's container conf # run this command to show all the details for a specific env var: $ php bin/console debug:container --env-var=FOO +Creating Your Own Logic To Load Env Vars +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can implement your own logic to load environment variables if the default +Symfony behavior doesn't fit your needs. To do so, create a service whose class +implements :class:`Symfony\\Component\\DependencyInjection\\EnvVarLoaderInterface`. + +.. note:: + + If you're using the :ref:`default services.yaml configuration `, + the autoconfiguration feature will enable and tag thise service automatically. + Otherwise, you need to register and :doc:`tag your service ` + with the ``container.env_var_loader`` tag. + +Let's say you have a JSON file named ``env.json`` containing your environment +variables: + +.. code-block:: json + + { + "vars": { + "APP_ENV": "prod", + "APP_DEBUG": false + } + } + +You can define a class like the following ``JsonEnvVarLoader`` to populate the +environment variables from the file:: + + namespace App\DependencyInjection; + + use Symfony\Component\DependencyInjection\EnvVarLoaderInterface; + + final class JsonEnvVarLoader implements EnvVarLoaderInterface + { + private const ENV_VARS_FILE = 'env.json'; + + public function loadEnvVars(): array + { + $fileName = __DIR__.\DIRECTORY_SEPARATOR.self::ENV_VARS_FILE; + if (!is_file($fileName)) { + // throw an exception or just ignore this loader, depending on your needs + } + + $content = json_decode(file_get_contents($fileName), true); + + return $content['vars']; + } + } + +That's it! Now the application will look for a ``env.json`` file in the +current directory to populate environment variables (in addition to the +already existing ``.env`` files). + +.. tip:: + + If you want an env var to have a value on a certain environment but to fallback + on loaders on another environment, assign an empty value to the env var for + the environment you want to use loaders: + + .. code-block:: bash + + # .env (or .env.local) + APP_ENV=prod + + # .env.prod (or .env.prod.local) - this will fallback on the loaders you defined + APP_ENV= + .. _configuration-accessing-parameters: Accessing Configuration Parameters @@ -1152,6 +1303,12 @@ namespace ``Symfony\Config``:: Nested configs (e.g. ``\Symfony\Config\Framework\CacheConfig``) are regular PHP objects which aren't autowired when using them as an argument type. +.. note:: + + In order to get ConfigBuilders autocompletion in your IDE/editor, make sure + to not exclude the directory where these classes are generated (by default, + in ``var/cache/dev/Symfony/Config/``). + Keep Going! ----------- diff --git a/configuration/micro_kernel_trait.rst b/configuration/micro_kernel_trait.rst index 5940b918183..4d7494e72f8 100644 --- a/configuration/micro_kernel_trait.rst +++ b/configuration/micro_kernel_trait.rst @@ -70,6 +70,12 @@ Next, create an ``index.php`` file that defines the kernel class and runs it:: $response->send(); $kernel->terminate($request, $response); +.. note:: + + In addition to the ``index.php`` file, you'll need to create a directory called + ``config/`` in your project (even if it's empty because you define the configuration + options inside the ``configureContainer()`` method). + That's it! To test it, start the :doc:`Symfony Local Web Server `: @@ -105,7 +111,18 @@ Adding Interfaces to "Micro" Kernel When using the ``MicroKernelTrait``, you can also implement the ``CompilerPassInterface`` to automatically register the kernel itself as a compiler pass as explained in the dedicated -:ref:`compiler pass section `. +:ref:`compiler pass section `. If the +:class:`Symfony\\Component\\DependencyInjection\\Extension\\ExtensionInterface` +is implemented when using the ``MicroKernelTrait``, then the kernel will +be automatically registered as an extension. You can learn more about it in +the dedicated section about +:ref:`managing configuration with extensions `. + +.. versionadded:: 5.2 + + The automatic registration of the kernel as an extension when implementing the + :class:`Symfony\\Component\\DependencyInjection\\Extension\\ExtensionInterface` + was introduced in Symfony 5.2. It is also possible to implement the ``EventSubscriberInterface`` to handle events directly from the kernel, again it will be registered automatically:: diff --git a/configuration/multiple_kernels.rst b/configuration/multiple_kernels.rst index 9464fcf39f7..2ecee747e38 100644 --- a/configuration/multiple_kernels.rst +++ b/configuration/multiple_kernels.rst @@ -53,7 +53,7 @@ requirements, so it's up to you to decide which best suits your project. First, create a new ``apps`` directory at the root of your project, which will hold all the necessary applications. Each application will follow a simplified -directory structure like the one described in :ref:`Symfony Best Practice `: +directory structure like the one described in :doc:`Symfony Best Practice `: .. code-block:: text @@ -117,7 +117,8 @@ resources:: // src/Kernel.php namespace Shared; - // ... + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; class Kernel extends BaseKernel { @@ -257,7 +258,8 @@ the application ID to run under CLI context:: // bin/console use Shared\Kernel; - // ... + use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Input\InputOption; return function (InputInterface $input, array $context) { $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG'], $input->getParameterOption(['--id', '-i'], $context['APP_ID'])); @@ -319,7 +321,7 @@ Rendering Templates ------------------- Let's consider that you need to create another app called ``admin``. If you -follow the :ref:`Symfony Best Practices `, the shared Kernel +follow the :doc:`Symfony Best Practices `, the shared Kernel templates will be located in the ``templates/`` directory at the project's root. For admin-specific templates, you can create a new directory ``apps/admin/templates/`` which you will need to manually configure under the diff --git a/configuration/override_dir_structure.rst b/configuration/override_dir_structure.rst index 2a5df047611..41bf46d0e66 100644 --- a/configuration/override_dir_structure.rst +++ b/configuration/override_dir_structure.rst @@ -74,7 +74,6 @@ Web front-controller:: require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; // ... - .. _override-config-dir: Override the Configuration Directory @@ -254,7 +253,7 @@ your ``index.php`` front controller. If you renamed the directory, you're fine. But if you moved it in some way, you may need to modify these paths inside those files:: - require_once __DIR__.'/../path/to/vendor/autoload.php'; + require_once __DIR__.'/../path/to/vendor/autoload_runtime.php'; You also need to change the ``extra.public-dir`` option in the ``composer.json`` file: diff --git a/configuration/secrets.rst b/configuration/secrets.rst index 56270b75ca5..863f575287d 100644 --- a/configuration/secrets.rst +++ b/configuration/secrets.rst @@ -50,7 +50,7 @@ running: This will generate ``config/secrets/prod/prod.encrypt.public.php`` and ``config/secrets/prod/prod.decrypt.private.php``. -.. caution:: +.. danger:: The ``prod.decrypt.private.php`` file is highly sensitive. Your team of developers and even Continuous Integration services don't need that key. If the @@ -320,6 +320,5 @@ The secrets system is enabled by default and some of its behavior can be configu ; }; - .. _`libsodium`: https://pecl.php.net/package/libsodium .. _`paragonie/sodium_compat`: https://github.com/paragonie/sodium_compat diff --git a/console.rst b/console.rst index 28b560d1f9b..60d53d0c056 100644 --- a/console.rst +++ b/console.rst @@ -28,6 +28,10 @@ the ``list`` command to view all available commands in the application: cache:clear Clear the cache ... +.. note:: + + ``list`` is the default command, so running ``php bin/console`` is the same. + If you find the command you need, you can run it with the ``--help`` option to view the command's documentation: @@ -35,6 +39,13 @@ to view the command's documentation: $ php bin/console assets:install --help +.. note:: + + ``--help`` is one of the built-in global options from the Console component, + which are available for all commands, including those you can create. + To learn more about them, you can read + :ref:`this section `. + APP_ENV & APP_DEBUG ~~~~~~~~~~~~~~~~~~~ @@ -66,6 +77,7 @@ commands support name and option completion, and some can even complete values. .. image:: /_images/components/console/completion.gif + :alt: The terminal completes the command name "secrets:remove" and the argument "SOME_OTHER_SECRET". First, make sure you installed and setup the "bash completion" package for your OS (typically named ``bash-completion``). Then, install the Symfony @@ -91,6 +103,15 @@ options by pressing the Tab key. $ php vendor/bin/phpstan completion bash | sudo tee /etc/bash_completion.d/phpstan +.. tip:: + + If you are using the :doc:`Symfony local web server + `, it is recommended to use the built-in completion + script that will ensure the right PHP version and configuration are used when + running the Console Completion. Run ``symfony completion --help`` for the + installation instructions for your shell. The Symfony CLI will provide + completion for the ``console`` and ``composer`` commands. + Creating a Command ------------------ @@ -334,19 +355,23 @@ method, which returns an instance of $section1->writeln('Hello'); $section2->writeln('World!'); + sleep(1); // Output displays "Hello\nWorld!\n" // overwrite() replaces all the existing section contents with the given content $section1->overwrite('Goodbye'); + sleep(1); // Output now displays "Goodbye\nWorld!\n" // clear() deletes all the section contents... $section2->clear(); + sleep(1); // Output now displays "Goodbye\n" // ...but you can also delete a given number of lines // (this example deletes the last two lines of the section) $section1->clear(2); + sleep(1); // Output is now completely empty! return Command::SUCCESS; @@ -362,6 +387,11 @@ Output sections let you manipulate the Console output in advanced ways, such as are updated independently and :ref:`appending rows to tables ` that have already been rendered. +.. caution:: + + Terminals only allow overwriting the visible content, so you must take into + account the console height when trying to write/overwrite section contents. + Console Input ------------- @@ -492,8 +522,8 @@ console:: { public function testExecute() { - $kernel = self::bootKernel(); - $application = new Application($kernel); + self::bootKernel(); + $application = new Application(self::$kernel); $command = $application->find('app:create-user'); $commandTester = new CommandTester($command); @@ -550,7 +580,6 @@ call ``setAutoExit(false)`` on it to get the command result in ``CommandTester`` $tester = new ApplicationTester($application); - .. caution:: When testing ``InputOption::VALUE_NONE`` command options, you must pass an @@ -565,6 +594,21 @@ call ``setAutoExit(false)`` on it to get the command result in ``CommandTester`` :class:`Symfony\\Component\\Console\\Application` and extend the normal ``\PHPUnit\Framework\TestCase``. +When testing your commands, it could be useful to understand how your command +reacts on different settings like the width and the height of the terminal. +You have access to such information thanks to the +:class:`Symfony\\Component\\Console\\Terminal` class:: + + use Symfony\Component\Console\Terminal; + + $terminal = new Terminal(); + + // gets the number of lines available + $height = $terminal->getHeight(); + + // gets the number of columns available + $width = $terminal->getWidth(); + Logging Command Errors ---------------------- @@ -574,6 +618,12 @@ registers an :doc:`event subscriber ` to listen to the :ref:`ConsoleEvents::TERMINATE event ` and adds a log message whenever a command doesn't finish with the ``0`` `exit status`_. +Using Events And Handling Signals +--------------------------------- + +When a command is running, many events are dispatched, one of them allows to +react to signals, read more in :doc:`this section `. + Learn More ---------- @@ -593,6 +643,7 @@ tools capable of helping you with different tasks: * :doc:`/components/console/helpers/table`: displays tabular data as a table * :doc:`/components/console/helpers/debug_formatter`: provides functions to output debug information when running an external program +* :doc:`/components/console/helpers/processhelper`: allows to run processes using ``DebugFormatterHelper`` * :doc:`/components/console/helpers/cursor`: allows to manipulate the cursor in the terminal .. _`exit status`: https://en.wikipedia.org/wiki/Exit_status diff --git a/console/calling_commands.rst b/console/calling_commands.rst index 1a9cce4e6c3..35d388965ad 100644 --- a/console/calling_commands.rst +++ b/console/calling_commands.rst @@ -8,13 +8,13 @@ or if you want to create a "meta" command that runs a bunch of other commands changed on the production servers: clearing the cache, generating Doctrine proxies, dumping web assets, ...). -Use the :method:`Symfony\\Component\\Console\\Application::find` method to -find the command you want to run by passing the command name. Then, create a -new :class:`Symfony\\Component\\Console\\Input\\ArrayInput` with the -arguments and options you want to pass to the command. +Use the :method:`Symfony\\Component\\Console\\Application::doRun`. Then, create +a new :class:`Symfony\\Component\\Console\\Input\\ArrayInput` with the +arguments and options you want to pass to the command. The command name must be +the first argument. -Eventually, calling the ``run()`` method actually runs the command and returns -the returned code from the command (return value from command's ``execute()`` +Eventually, calling the ``doRun()`` method actually runs the command and returns +the returned code from the command (return value from command ``execute()`` method):: // ... @@ -29,15 +29,14 @@ method):: protected function execute(InputInterface $input, OutputInterface $output): int { - $command = $this->getApplication()->find('demo:greet'); - - $arguments = [ + $greetInput = new ArrayInput([ + // the command name is passed as first argument + 'command' => 'demo:greet', 'name' => 'Fabien', '--yell' => true, - ]; + ]); - $greetInput = new ArrayInput($arguments); - $returnCode = $command->run($greetInput, $output); + $returnCode = $this->getApplication()->doRun($greetInput, $output); // ... } @@ -47,7 +46,16 @@ method):: If you want to suppress the output of the executed command, pass a :class:`Symfony\\Component\\Console\\Output\\NullOutput` as the second - argument to ``$command->run()``. + argument to ``$application->doRun()``. + +.. note:: + + Using ``doRun()`` instead of ``run()`` prevents autoexiting and allows to + return the exit code instead. + + Also, using ``$this->getApplication()->doRun()`` instead of + ``$this->getApplication()->find('demo:greet')->run()`` will allow proper + events to be dispatched for that inner command as well. .. caution:: diff --git a/console/input.rst b/console/input.rst index 6a242185034..3abf3a37b9b 100644 --- a/console/input.rst +++ b/console/input.rst @@ -404,4 +404,26 @@ to help you unit test the completion logic:: } } +.. _console-global-options: + +Command Global Options +---------------------- + +The Console component adds some predefined options to all commands: + +* ``--verbose``: sets the verbosity level (e.g. ``1`` the default, ``2`` and + ``3``, or you can use respective shortcuts ``-v``, ``-vv`` and ``-vvv``) +* ``--quiet``: disables output and interaction +* ``--no-interaction``: disables interaction +* ``--version``: outputs the version number of the console application +* ``--help``: displays the command help +* ``--ansi|--no-ansi``: whether to force of disable coloring the output + +When using the ``FrameworkBundle``, two more options are predefined: + +* ``--env``: sets the Kernel configuration environment (defaults to ``APP_ENV``) +* ``--no-debug``: disables Kernel debug (defaults to ``APP_DEBUG``) + +So your custom commands can use them too out-of-the-box. + .. _`docopt standard`: http://docopt.org/ diff --git a/console/style.rst b/console/style.rst index 59ad22ba3fa..98ab5d66b38 100644 --- a/console/style.rst +++ b/console/style.rst @@ -96,6 +96,8 @@ Titling Methods // ... +.. _symfony-style-content: + Content Methods ~~~~~~~~~~~~~~~ @@ -219,6 +221,8 @@ Admonition Methods 'Aenean sit amet arcu vitae sem faucibus porta', ]); +.. _symfony-style-progressbar: + Progress Bar Methods ~~~~~~~~~~~~~~~~~~~~ @@ -267,6 +271,8 @@ Progress Bar Methods Creates an instance of :class:`Symfony\\Component\\Console\\Helper\\ProgressBar` styled according to the Symfony Style Guide. +.. _symfony-style-questions: + User Input Methods ~~~~~~~~~~~~~~~~~~ @@ -329,6 +335,8 @@ User Input Methods $io->choice('Select the queue to analyze', ['queue1', 'queue2', 'queue3'], 'queue1'); +.. _symfony-style-blocks: + Result Methods ~~~~~~~~~~~~~~ diff --git a/contributing/code/bc.rst b/contributing/code/bc.rst index 3caf969c432..a3664a0c32c 100644 --- a/contributing/code/bc.rst +++ b/contributing/code/bc.rst @@ -258,6 +258,14 @@ Make public or protected Yes Remove private property Yes **Constructors** Add constructor without mandatory arguments Yes :ref:`[1] ` +:ref:`Add argument without a default value ` No +Add argument with a default value Yes :ref:`[11] ` +Remove argument No :ref:`[3] ` +Add default value to an argument Yes +Remove default value of an argument No +Add type hint to an argument No +Remove type hint of an argument Yes +Change argument type No Remove constructor No Reduce visibility of a public constructor No Reduce visibility of a protected constructor No :ref:`[7] ` @@ -473,6 +481,10 @@ a return type is only possible with a child type. constructors of Attribute classes. Using PHP named arguments might break your code when upgrading to newer Symfony versions. +.. _note-11: + +**[11]** Only optional argument(s) of a constructor at last position may be added. + Making Code Changes in a Backward Compatible Way ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -496,42 +508,42 @@ If that's the case, here is how to do it properly in a minor version: #. Add the argument as a comment in the signature:: // the new argument can be optional - public function say(string $text, /* bool $stripWithespace = true */): void + public function say(string $text, /* bool $stripWhitespace = true */): void { } // or required - public function say(string $text, /* bool $stripWithespace */): void + public function say(string $text, /* bool $stripWhitespace */): void { } #. Document the new argument in a PHPDoc:: /** - * @param bool $stripWithespace + * @param bool $stripWhitespace */ #. Use ``func_num_args`` and ``func_get_arg`` to retrieve the argument in the method:: - $stripWithespace = 2 <= \func_num_args() ? func_get_arg(1) : false; + $stripWhitespace = 2 <= \func_num_args() ? func_get_arg(1) : false; Note that the default value is ``false`` to keep the current behavior. #. If the argument has a default value that will change the current behavior, warn the user:: - trigger_deprecation('symfony/COMPONENT', 'X.Y', 'Not passing the "bool $stripWithespace" argument explicitly is deprecated, its default value will change to X in Z.0.'); + trigger_deprecation('symfony/COMPONENT', 'X.Y', 'Not passing the "bool $stripWhitespace" argument explicitly is deprecated, its default value will change to X in Z.0.'); #. If the argument has no default value, warn the user that is going to be required in the next major version:: if (\func_num_args() < 2) { - trigger_deprecation('symfony/COMPONENT', 'X.Y', 'The "%s()" method will have a new "bool $stripWithespace" argument in version Z.0, not defining it is deprecated.', __METHOD__); + trigger_deprecation('symfony/COMPONENT', 'X.Y', 'The "%s()" method will have a new "bool $stripWhitespace" argument in version Z.0, not defining it is deprecated.', __METHOD__); - $stripWithespace = false; + $stripWhitespace = false; } else { - $stripWithespace = func_get_arg(1); + $stripWhitespace = func_get_arg(1); } #. In the next major version (``X.0``), uncomment the argument, remove the diff --git a/contributing/code/conventions.rst b/contributing/code/conventions.rst index cd1d87b4282..455bc8de0ed 100644 --- a/contributing/code/conventions.rst +++ b/contributing/code/conventions.rst @@ -181,8 +181,6 @@ after the use declarations, like in this example from `ServiceRouterLoader`_:: */ class ServiceRouterLoader extends ObjectRouteLoader -.. _`ServiceRouterLoader`: https://github.com/symfony/symfony/blob/4.4/src/Symfony/Component/Routing/Loader/DependencyInjection/ServiceRouterLoader.php - The deprecation must be added to the ``CHANGELOG.md`` file of the impacted component: .. code-block:: markdown @@ -239,3 +237,5 @@ Commands and their options should be named and described using the English imperative mood (i.e. 'run' instead of 'runs', 'list' instead of 'lists'). Using the imperative mood is concise and consistent with similar command-line interfaces (such as Unix man pages). + +.. _`ServiceRouterLoader`: https://github.com/symfony/symfony/blob/4.4/src/Symfony/Component/Routing/Loader/DependencyInjection/ServiceRouterLoader.php diff --git a/contributing/code/core_team.rst b/contributing/code/core_team.rst index 6cef3400384..efc60894c7c 100644 --- a/contributing/code/core_team.rst +++ b/contributing/code/core_team.rst @@ -36,8 +36,6 @@ 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.) -* **Recipes Team**: manages the recipes in the main and contrib recipe repositories. - * **Documentation Team**: manages the whole `symfony-docs repository`_. Active Core Members @@ -52,19 +50,16 @@ Active Core Members * **Nicolas Grekas** (`nicolas-grekas`_); * **Christophe Coevoet** (`stof`_); * **Christian Flothmann** (`xabbuh`_); - * **Tobias Schultze** (`Tobion`_); * **Kévin Dunglas** (`dunglas`_); * **Javier Eguiluz** (`javiereguiluz`_); * **Grégoire Pineau** (`lyrixx`_); * **Ryan Weaver** (`weaverryan`_); * **Robin Chalas** (`chalasr`_); - * **Maxime Steinhausser** (`ogizanagi`_); * **Yonel Ceruto** (`yceruto`_); * **Tobias Nyholm** (`Nyholm`_); * **Wouter De Jong** (`wouterj`_); * **Alexander M. Turek** (`derrabus`_); * **Jérémy Derussé** (`jderusse`_); - * **Titouan Galopin** (`tgalopin`_); * **Oskar Stark** (`OskarStark`_); * **Thomas Calvet** (`fancyweb`_); * **Mathieu Santostefano** (`welcomattic`_); @@ -74,14 +69,8 @@ Active Core Members * **Security Team** (``@symfony/security`` on GitHub): * **Fabien Potencier** (`fabpot`_); - * **Michael Cullum** (`michaelcullum`_); * **Jérémy Derussé** (`jderusse`_). -* **Recipes Team**: - - * **Fabien Potencier** (`fabpot`_); - * **Tobias Nyholm** (`Nyholm`_). - * **Documentation Team** (``@symfony/team-symfony-docs`` on GitHub): * **Fabien Potencier** (`fabpot`_); @@ -104,7 +93,11 @@ Symfony contributions: * **Lukas Kahwe Smith** (`lsmith77`_); * **Jules Pietri** (`HeahDude`_); * **Jakub Zalas** (`jakzal`_); -* **Samuel Rozé** (`sroze`_). +* **Samuel Rozé** (`sroze`_); +* **Tobias Schultze** (`Tobion`_); +* **Maxime Steinhausser** (`ogizanagi`_); +* **Titouan Galopin** (`tgalopin`_); +* **Michael Cullum** (`michaelcullum`_). Core Membership Application ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/contributing/code/license.rst b/contributing/code/license.rst index 8f0ff3f6501..0a4eaafce0d 100644 --- a/contributing/code/license.rst +++ b/contributing/code/license.rst @@ -5,7 +5,7 @@ Symfony Code License Symfony code is released under `the MIT license`_: -Copyright (c) 2004-2021 Fabien Potencier +Copyright (c) 2004-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/contributing/code/maintenance.rst b/contributing/code/maintenance.rst index 04740ce8c6e..27e4fd73ea0 100644 --- a/contributing/code/maintenance.rst +++ b/contributing/code/maintenance.rst @@ -67,6 +67,9 @@ issue): * **Adding new deprecations**: After a version reaches stability, new deprecations cannot be added anymore. +* **Adding or updating annotations**: Adding or updating annotations (PHPDoc + annotations for instance) is not allowed; fixing them might be accepted. + Anything not explicitly listed above should be done on the next minor or major version instead. For instance, the following changes are never accepted in a patch version: diff --git a/contributing/code/pull_requests.rst b/contributing/code/pull_requests.rst index 5255613c000..e9e8470bb96 100644 --- a/contributing/code/pull_requests.rst +++ b/contributing/code/pull_requests.rst @@ -132,7 +132,7 @@ work: branch (you can find them on the `Symfony releases page`_). E.g. if you found a bug introduced in ``v5.1.10``, you need to work on ``5.4``. -* ``6.3``, if you are adding a new feature. +* ``7.1``, if you are adding a new feature. The only exception is when a new :doc:`major Symfony version ` (5.0, 6.0, etc.) comes out every two years. Because of the @@ -172,8 +172,8 @@ Then create a new branch off the ``5.4`` branch to work on the bug fix: .. tip:: - Use a descriptive name for your branch (``ticket_XXX`` where ``XXX`` is the - ticket number is a good convention for bug fixes). + Use a descriptive name for your branch (``fix_XXX`` where ``XXX`` is the + issue number is a good convention for bug fixes). The above checkout commands automatically switch the code to the newly created branch (check the branch you are working on with ``git branch``). @@ -338,7 +338,7 @@ Symfony as quickly as possible. Some answers to the questions trigger some more requirements: * If you answer yes to "Bug fix?", check if the bug is already listed in the - Symfony issues and reference it/them in "Fixed tickets"; + Symfony issues and reference it/them in "Issues"; * If you answer yes to "New feature?", you must submit a pull request to the documentation and reference it under the "Doc PR" section; @@ -521,7 +521,7 @@ before merging. .. _ProGit: https://git-scm.com/book .. _GitHub: https://github.com/join -.. _`GitHub's documentation`: https://help.github.com/github/using-git/ignoring-files +.. _`GitHub's documentation`: https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files .. _Symfony repository: https://github.com/symfony/symfony .. _Symfony releases page: https://symfony.com/releases#maintained-symfony-branches .. _`documentation repository`: https://github.com/symfony/symfony-docs diff --git a/contributing/code/reproducer.rst b/contributing/code/reproducer.rst index 6efae2a8ee8..3392ca87035 100644 --- a/contributing/code/reproducer.rst +++ b/contributing/code/reproducer.rst @@ -2,8 +2,8 @@ Creating a Bug Reproducer ========================= The main Symfony code repository receives thousands of issues reports per year. -Some of those issues are easy to understand and the Symfony Core developers can -fix them without any other information. However, other issues are much harder to +Some of those issues are easy to understand and can +be fixed without any other information. However, other issues are much harder to understand because developers can't reproduce them in their computers. That's when we'll ask you to create a "bug reproducer", which is the minimum amount of code needed to make the bug appear when executed. diff --git a/contributing/code/stack_trace.rst b/contributing/code/stack_trace.rst index cd672e05a2a..6fd6987d4e3 100644 --- a/contributing/code/stack_trace.rst +++ b/contributing/code/stack_trace.rst @@ -91,8 +91,8 @@ 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 example below (it - is the one marked as an exception [1/2]). + 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. @@ -102,8 +102,8 @@ from your development environment through a web browser: are getting, but are not what the term "stack trace" refers to. .. image:: /_images/contributing/code/stack-trace.gif - :align: center - :class: with-browser + :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 diff --git a/contributing/code/standards.rst b/contributing/code/standards.rst index 2668269dfcc..b516f835179 100644 --- a/contributing/code/standards.rst +++ b/contributing/code/standards.rst @@ -214,8 +214,8 @@ Naming Conventions * Use `camelCase`_ for PHP variables, function and method names, arguments (e.g. ``$acceptableContentTypes``, ``hasSession()``); -* Use `snake_case`_ for configuration parameters and Twig template variables - (e.g. ``framework.csrf_protection``, ``http_status_code``); +Use `snake_case`_ for configuration parameters, route names and Twig template + variables (e.g. ``framework.csrf_protection``, ``http_status_code``); * Use SCREAMING_SNAKE_CASE for constants (e.g. ``InputArgument::IS_ARRAY``); diff --git a/contributing/code_of_conduct/care_team.rst b/contributing/code_of_conduct/care_team.rst index f7f565a266f..e061c0a0afe 100644 --- a/contributing/code_of_conduct/care_team.rst +++ b/contributing/code_of_conduct/care_team.rst @@ -19,30 +19,37 @@ 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 (in alphabetic order). 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 **. +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 `_ + * *SymfonySlack*: `@Timo Bakx `_ * **Zan Baldwin** * *E-mail*: hello [at] zanbaldwin.com * *Twitter*: `@ZanBaldwin `_ * *SymfonyConnect*: `zanbaldwin `_ - * *SymfonySlack*: `@Zan `_ + * *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 `_ + * *SymfonySlack*: `@Tobias Nyholm `_ About the CARE Team ------------------- @@ -51,12 +58,3 @@ The :doc:`Symfony project leader ` appoints the CA 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. - -CARE Team Transparency Reports ------------------------------- - -The CARE team publishes a transparency report at the end of each year: - -* `Symfony Code of Conduct Transparency Report 2018`_. - -.. _`Symfony Code of Conduct Transparency Report 2018`: https://symfony.com/blog/symfony-code-of-conduct-transparency-report-2018 diff --git a/contributing/community/review-comments.rst b/contributing/community/review-comments.rst index 0a048d8fa6e..5b9bc932205 100644 --- a/contributing/community/review-comments.rst +++ b/contributing/community/review-comments.rst @@ -149,7 +149,6 @@ 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 ---------------------- diff --git a/contributing/community/reviews.rst b/contributing/community/reviews.rst index ba08e4ffd36..94c37643988 100644 --- a/contributing/community/reviews.rst +++ b/contributing/community/reviews.rst @@ -167,7 +167,7 @@ Pick a pull request from the `PRs in need of review`_ and follow these steps: 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:: text + .. code-block:: terminal $ cd vendor/symfony/symfony $ git fetch origin pull//head:pr @@ -175,7 +175,7 @@ Pick a pull request from the `PRs in need of review`_ and follow these steps: For example: - .. code-block:: text + .. code-block:: terminal $ git fetch origin pull/15723/head:pr15723 $ git checkout pr15723 diff --git a/contributing/documentation/format.rst b/contributing/documentation/format.rst index 33798d48da3..1ff8b8e56c1 100644 --- a/contributing/documentation/format.rst +++ b/contributing/documentation/format.rst @@ -110,18 +110,25 @@ The current list of supported formats are the following: =================== ============================================================================== Markup Format Use It to Display =================== ============================================================================== -``html`` HTML -``xml`` XML -``php`` PHP -``yaml`` YAML -``twig`` Pure Twig markup -``html+twig`` Twig markup blended with HTML +``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-symfony`` PHP code example when using the Symfony framework ``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 @@ -190,6 +197,29 @@ If you want to modify that title, use this alternative syntax: :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 + + # /service_container/autowiring.rst + + # define the target + .. _autowiring-calls: + + Autowiring other Methods (e.g. Setters and Public Typed Properties) + ------------------------------------------------------------------- + + // section content ... + +Then, use the ``:ref::`` directive to link to that section from another file: + +.. code-block:: rst + + # /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``): diff --git a/contributing/documentation/overview.rst b/contributing/documentation/overview.rst index 2ea1054eb7b..183910e6ac6 100644 --- a/contributing/documentation/overview.rst +++ b/contributing/documentation/overview.rst @@ -21,23 +21,24 @@ 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. -**Step 1.** Click on the **edit this page** button on the upper right corner +**Step 1.** Click on the **edit this page** button on the top of the page and you'll be redirected to GitHub: .. image:: /_images/contributing/docs-github-edit-page.png - :align: center - :class: with-browser + :alt: The "Edit this page" button is located directly below the first heading. + :class: with-browser -**Step 2.** Edit the contents, describe your changes and click on the -**Propose file change** button. +**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. -**Step 3.** GitHub will now create a branch and a commit for your changes -(forking the repository first if this is your first contribution) and it will +**Step 3.** GitHub will now create a branch and a commit for your changes and it will also display a preview of your changes: .. image:: /_images/contributing/docs-github-create-pr.png - :align: center - :class: with-browser + :alt: The "Comparing changes" page on GitHub. + :class: with-browser If everything is correct, click on the **Create pull request** button. @@ -103,7 +104,7 @@ Fetch all the commits of the upstream branches by executing this command: $ git fetch upstream -The purpose of this step is to allow you work simultaneously on the official +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 @@ -152,7 +153,7 @@ exact changes that you want to propose, select the appropriate branches where changes should be applied: .. image:: /_images/contributing/docs-pull-request-change-base.png - :align: center + :alt: The base branch select option on the GitHub page. In this example, the **base fork** should be ``symfony/symfony-docs`` and the **base** branch should be the ``5.4``, which is the branch that you selected @@ -184,6 +185,9 @@ changes and push the new changes: $ git push +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 `. + **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`_ diff --git a/contributing/documentation/standards.rst b/contributing/documentation/standards.rst index 2bdd816af49..0184fef36fc 100644 --- a/contributing/documentation/standards.rst +++ b/contributing/documentation/standards.rst @@ -146,6 +146,35 @@ Files and Directories ├─ 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 -------------------------- @@ -201,4 +230,6 @@ In addition, documentation follows these rules: .. _`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/controller.rst b/controller.rst index c3a11e99a6a..01bf572d9a2 100644 --- a/controller.rst +++ b/controller.rst @@ -146,7 +146,7 @@ and ``redirect()`` methods:: return $this->redirect('http://symfony.com/doc'); } -.. caution:: +.. 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 @@ -178,7 +178,8 @@ These are used for rendering templates, sending emails, querying the database an 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. Symfony will automatically pass you the service you need:: +(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; @@ -462,6 +463,14 @@ 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/controller/argument_value_resolver.rst b/controller/argument_value_resolver.rst index eb100c258f0..1cddcede0bf 100644 --- a/controller/argument_value_resolver.rst +++ b/controller/argument_value_resolver.rst @@ -53,8 +53,8 @@ In addition, some components and official bundles provide other value resolvers: PSR-7 Objects Resolver: Injects a Symfony HttpFoundation ``Request`` object created from a PSR-7 object - of type :class:`Psr\\Http\\Message\\ServerRequestInterface`, - :class:`Psr\\Http\\Message\\RequestInterface` or :class:`Psr\\Http\\Message\\MessageInterface`. + of type ``Psr\Http\Message\ServerRequestInterface``, + ``Psr\Http\Message\RequestInterface`` or ``Psr\Http\Message\MessageInterface``. It requires installing :doc:`the PSR-7 Bridge ` component. Adding a Custom Value Resolver @@ -250,7 +250,7 @@ To ensure your resolvers are added in the right position you can run the followi 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 --show-arguments .. tip:: diff --git a/controller/error_pages.rst b/controller/error_pages.rst index 56f8e60a408..0341c30e941 100644 --- a/controller/error_pages.rst +++ b/controller/error_pages.rst @@ -10,18 +10,16 @@ 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 - :align: center - :class: with-browser + :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 - :align: center - :class: with-browser + :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: @@ -218,7 +216,7 @@ contents, create a new Normalizer that supports the ``FlattenException`` input:: class MyCustomProblemNormalizer implements NormalizerInterface { - public function normalize($exception, string $format = null, array $context = []) + public function normalize($exception, ?string $format = null, array $context = []) { return [ 'content' => 'This is my custom problem normalizer.', @@ -229,7 +227,7 @@ contents, create a new Normalizer that supports the ``FlattenException`` input:: ]; } - public function supportsNormalization($data, string $format = null) + public function supportsNormalization($data, ?string $format = null) { return $data instanceof FlattenException; } @@ -321,7 +319,7 @@ error pages. .. note:: - If your listener calls ``setThrowable()`` on the + 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. diff --git a/controller/forwarding.rst b/controller/forwarding.rst index a0e0648517a..8d8be859da5 100644 --- a/controller/forwarding.rst +++ b/controller/forwarding.rst @@ -11,7 +11,7 @@ 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($name): Response + public function index(string $name): Response { $response = $this->forward('App\Controller\OtherController::fancy', [ 'name' => $name, diff --git a/controller/service.rst b/controller/service.rst index 50ee34a1aac..d7a263e7206 100644 --- a/controller/service.rst +++ b/controller/service.rst @@ -25,6 +25,39 @@ in method parameters: 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:: diff --git a/controller/upload_file.rst b/controller/upload_file.rst index 886c772d98a..b122b76c71a 100644 --- a/controller/upload_file.rst +++ b/controller/upload_file.rst @@ -60,7 +60,7 @@ so Symfony doesn't try to get/set its value from the related entity:: class ProductType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder // ... @@ -134,7 +134,7 @@ Finally, you need to update the code of the controller that handles the form:: /** * @Route("/product/new", name="app_product_new") */ - public function new(Request $request, SluggerInterface $slugger): Response + public function new(Request $request, SluggerInterface $slugger, string $brochuresDirectory): Response { $product = new Product(); $form = $this->createForm(ProductType::class, $product); @@ -154,10 +154,7 @@ Finally, you need to update the code of the controller that handles the form:: // Move the file to the directory where brochures are stored try { - $brochureFile->move( - $this->getParameter('brochures_directory'), - $newFilename - ); + $brochureFile->move($brochuresDirectory, $newFilename); } catch (FileException $e) { // ... handle exception if something happens during file upload } @@ -178,16 +175,17 @@ Finally, you need to update the code of the controller that handles the form:: } } -Now, create the ``brochures_directory`` parameter that was used in the -controller to specify the directory in which the brochures should be stored: +Now, bind the ``$brochuresDirectory`` controller argument to its actual value +using the service configuration: .. code-block:: yaml # config/services.yaml - - # ... - parameters: - brochures_directory: '%kernel.project_dir%/public/uploads/brochures' + services: + _defaults: + # ... + bind: + string $brochuresDirectory: '%kernel.project_dir%/public/uploads/brochures' There are some important things to consider in the code of the above controller: @@ -223,7 +221,7 @@ You can use the following code to link to the PDF brochure of a product: // ... $product->setBrochureFilename( - new File($this->getParameter('brochures_directory').'/'.$product->getBrochureFilename()) + new File($brochuresDirectory.DIRECTORY_SEPARATOR.$product->getBrochureFilename()) ); Creating an Uploader Service diff --git a/create_framework/http_foundation.rst b/create_framework/http_foundation.rst index dd838e9a5e2..4406dde64a0 100644 --- a/create_framework/http_foundation.rst +++ b/create_framework/http_foundation.rst @@ -255,7 +255,7 @@ 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::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 diff --git a/create_framework/introduction.rst b/create_framework/introduction.rst index d3574de4c94..7a1e6b2ad50 100644 --- a/create_framework/introduction.rst +++ b/create_framework/introduction.rst @@ -29,7 +29,7 @@ a few good reasons to start creating your own framework: * To refactor an old/existing application that needs a good dose of recent web development best practices; -* To prove the world that you can actually create a framework on your own (... +* 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, diff --git a/create_framework/templating.rst b/create_framework/templating.rst index 6fca67d84a1..f7ff66fa9f8 100644 --- a/create_framework/templating.rst +++ b/create_framework/templating.rst @@ -142,13 +142,14 @@ 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($year = null) { if (null === $year) { - $year = date('Y'); + $year = (int)date('Y'); } return 0 === $year % 400 || (0 === $year % 4 && 0 !== $year % 100); diff --git a/create_framework/unit_testing.rst b/create_framework/unit_testing.rst index 916711de0ce..e39c96b9035 100644 --- a/create_framework/unit_testing.rst +++ b/create_framework/unit_testing.rst @@ -12,7 +12,7 @@ using `PHPUnit`_. At first, install PHPUnit as a development dependency: .. code-block:: terminal - $ composer require --dev phpunit/phpunit + $ composer require --dev phpunit/phpunit:^9.6 Then, create a PHPUnit configuration file in ``example.com/phpunit.xml.dist``: @@ -21,7 +21,7 @@ Then, create a PHPUnit configuration file in ``example.com/phpunit.xml.dist``: `, + 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. + Starting in v1.44.0 - `MakerBundle`_: only supports entities using PHP attributes. .. note:: @@ -185,14 +195,17 @@ 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: -.. image:: /_images/doctrine/mapping_single_entity.png - :align: center +.. 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. -To see a full list, check out `Doctrine's Mapping Types documentation`_. +Check out the `list of Doctrine mapping types`_ in the Doctrine documentation. If you want to use XML instead of annotations, add ``type: xml`` and ``dir: '%kernel.project_dir%/config/doctrine'`` to the entity mappings in your ``config/packages/doctrine.yaml`` file. @@ -219,6 +232,11 @@ already installed: $ 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 @@ -536,7 +554,7 @@ and injected by the dependency injection container:: class ProductController extends AbstractController { #[Route('/product/{id}', name: 'product_show')] - public function show(int $id, ProductRepository $productRepository): Response + public function show(ProductRepository $productRepository, int $id): Response { $product = $productRepository ->find($id); @@ -586,8 +604,8 @@ the :ref:`doctrine-queries` section. will display the number of queries and the time it took to execute them: .. image:: /_images/doctrine/doctrine_web_debug_toolbar.png - :align: center - :class: with-browser + :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 @@ -595,9 +613,13 @@ the :ref:`doctrine-queries` section. 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 `. + Automatically Fetching Objects (ParamConverter) ----------------------------------------------- +.. _doctrine-entity-value-resolver: + In many cases, you can use the `SensioFrameworkExtraBundle`_ to do the query for you automatically! First, install the bundle in case you don't have it: @@ -883,7 +905,7 @@ Learn more .. _`Doctrine`: https://www.doctrine-project.org/ .. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt -.. _`Doctrine's Mapping Types documentation`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/basic-mapping.html +.. _`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 @@ -899,3 +921,4 @@ Learn more .. _`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 index 0aea5cf2d58..5cd1ff1e07f 100644 --- a/doctrine/associations.rst +++ b/doctrine/associations.rst @@ -79,6 +79,13 @@ This will generate your new entity class:: // ... 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 ---------------------------------- @@ -409,8 +416,11 @@ When you go to ``/product``, a single row is added to both the ``category`` and to whatever the ``id`` is of the new category. Doctrine manages the persistence of this relationship for you: -.. image:: /_images/doctrine/mapping_relations.png - :align: center +.. 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 @@ -456,8 +466,11 @@ 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/doctrine/mapping_relations_proxy.png - :align: center +.. 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 @@ -638,6 +651,15 @@ also generated a ``removeProduct()`` method:: 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``: @@ -664,7 +686,6 @@ that behavior, use the `orphanRemoval`_ option inside ``Category``: #[ORM\OneToMany(targetEntity: Product::class, mappedBy: "category", orphanRemoval: true)] private $products; - Thanks to this, if the ``Product`` is removed from the ``Category``, it will be removed from the database entirely. @@ -686,3 +707,4 @@ Doctrine's `Association Mapping Documentation`_. .. _`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/dbal.rst b/doctrine/dbal.rst index 544428a9691..a0e0286d53e 100644 --- a/doctrine/dbal.rst +++ b/doctrine/dbal.rst @@ -32,7 +32,7 @@ Then configure the ``DATABASE_URL`` environment variable in ``.env``: # .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=5.7" + 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 diff --git a/event_dispatcher.rst b/event_dispatcher.rst index a1e26412a85..17449012eb3 100644 --- a/event_dispatcher.rst +++ b/event_dispatcher.rst @@ -41,6 +41,9 @@ The most common way to listen to an event is to register an **event listener**:: // 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 @@ -162,7 +165,10 @@ having to add any configuration in external files:: } } -You can add multiple ``#[AsEventListener()]`` attributes to configure different methods:: +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; @@ -198,7 +204,7 @@ can also be applied to methods directly:: final class MyMultiListener { - #[AsEventListener()] + #[AsEventListener] public function onCustomEvent(CustomEvent $event): void { // ... @@ -811,3 +817,11 @@ could listen to the ``mailer.post_send`` event and change the method's return va 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/create_custom_field_type.rst b/form/create_custom_field_type.rst index cfd8a872a0d..fe9e074f58c 100644 --- a/form/create_custom_field_type.rst +++ b/form/create_custom_field_type.rst @@ -94,7 +94,9 @@ following set of fields as the "postal address": .. raw:: html - + As explained above, form types are PHP classes that implement :class:`Symfony\\Component\\Form\\FormTypeInterface`, although it's more @@ -360,9 +362,8 @@ fragments used to render the types: {# ... here you will add the Twig code ... #} -Then, update the :ref:`form_themes option ` to -add this new template at the beginning of the list (the first one overrides the -rest of files): +Then, update the :ref:`form_themes option ` to +add this new template at the end of the list (each theme overrides all the previous ones): .. configuration-block:: @@ -371,8 +372,8 @@ rest of files): # config/packages/twig.yaml twig: form_themes: - - 'form/custom_types.html.twig' - '...' + - 'form/custom_types.html.twig' .. code-block:: xml @@ -387,8 +388,8 @@ rest of files): https://symfony.com/schema/dic/twig/twig-1.0.xsd"> - form/custom_types.html.twig ... + form/custom_types.html.twig @@ -399,8 +400,8 @@ rest of files): return static function (TwigConfig $twig) { $twig->formThemes([ - 'form/custom_types.html.twig', '...', + 'form/custom_types.html.twig', ]); }; @@ -429,12 +430,23 @@ second part of the Twig block name (e.g. ``_row``) defines which form type part is being rendered (row, widget, help, errors, etc.) The article about form themes explains the -:ref:`form fragment naming rules ` in detail. The -following diagram shows some of the Twig block names defined in this example: +:ref:`form fragment naming rules ` in detail. These +are some examples of Twig block names for the postal address type: .. raw:: html - + + +``postal_address_row`` + The full form type block. +``postal_address_addressLine1_help`` + The help message block below the first address line. +``postal_address_state_widget`` + The text input widget for the State field. +``postal_address_zipCode_label`` + The label block of the ZIP Code field. .. caution:: @@ -452,7 +464,6 @@ Symfony passes a series of variables to the template used to render the form type. You can also pass your own variables, which can be based on the options defined by the form or be completely independent:: - // src/Form/Type/PostalAddressType.php namespace App\Form\Type; diff --git a/form/data_transformers.rst b/form/data_transformers.rst index 005413ef992..56a08d71132 100644 --- a/form/data_transformers.rst +++ b/form/data_transformers.rst @@ -440,8 +440,11 @@ In the above example, the transformer was used as a "model" transformer. In fact, there are two different types of transformers and three different types of underlying data. -.. image:: /_images/form/data-transformer-types.png - :align: center +.. raw:: html + + In any form, the three different types of data are: @@ -481,7 +484,7 @@ To use the view transformer, call ``addViewTransformer()``. data. So your model transformer cannot reduce the number of items within the Collection (i.e. filtering out some items), as in that case the collection ends up with some empty children. - + A possible workaround for that limitation could be not using the underlying object directly, but a DTO (Data Transfer Object) instead, that implements the transformation of such incompatible data structures. diff --git a/form/direct_submit.rst b/form/direct_submit.rst index 3e239bfc138..dfd24acec82 100644 --- a/form/direct_submit.rst +++ b/form/direct_submit.rst @@ -17,7 +17,7 @@ control over when exactly your form is submitted and what data is passed to it:: $form = $this->createForm(TaskType::class, $task); if ($request->isMethod('POST')) { - $form->submit($request->request->get($form->getName())); + $form->submit($request->request->all($form->getName())); if ($form->isSubmitted() && $form->isValid()) { // perform some action... diff --git a/form/dynamic_form_modification.rst b/form/dynamic_form_modification.rst index 8ad446915c4..8244c41b74a 100644 --- a/form/dynamic_form_modification.rst +++ b/form/dynamic_form_modification.rst @@ -9,7 +9,7 @@ how to customize your form based on three common use-cases: 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:`How to dynamically Generate Forms Based on user Data ` +2) :ref:`How to Dynamically Generate Forms Based on 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 @@ -188,7 +188,7 @@ Great! Now use that in your form class:: .. _form-events-user-data: -How to dynamically Generate Forms Based on 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 @@ -459,7 +459,7 @@ The type would now look like:: ]) ; - $formModifier = function (FormInterface $form, Sport $sport = null) { + $formModifier = function (FormInterface $form, ?Sport $sport = null) { $positions = null === $sport ? [] : $sport->getAvailablePositions(); $form->add('position', EntityType::class, [ diff --git a/form/events.rst b/form/events.rst index 37c3918d67d..44cb6cc0074 100644 --- a/form/events.rst +++ b/form/events.rst @@ -29,16 +29,28 @@ register an event listener to the ``FormEvents::PRE_SUBMIT`` event as follows:: 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() ` @@ -55,13 +67,14 @@ 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 -=============== ======== -Model data ``null`` -Normalized data ``null`` -View data ``null`` -=============== ======== +==================== ====================================== +Data Type Value +==================== ====================================== +Event data Model data injected into ``setData()`` +Form model data ``null`` +Form normalized data ``null`` +Form view data ``null`` +==================== ====================================== .. seealso:: @@ -86,13 +99,14 @@ The ``FormEvents::POST_SET_DATA`` event is dispatched at the end of the method. This event can be used to modify a form depending on the populated data (adding or removing fields dynamically). -=============== ==================================================== -Data Type Value -=============== ==================================================== -Model data Model data injected into ``setData()`` -Normalized data Model data transformed using a model transformer -View data Normalized data transformed using a view transformer -=============== ==================================================== +==================== ==================================================== +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:: @@ -111,7 +125,9 @@ View data Normalized data transformed using a view transformer .. raw:: html - + Three events are dispatched when :method:`Form::handleRequest() ` @@ -130,13 +146,14 @@ 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 -=============== ======================================== -Model data Same as in ``FormEvents::POST_SET_DATA`` -Normalized data Same as in ``FormEvents::POST_SET_DATA`` -View data Same as in ``FormEvents::POST_SET_DATA`` -=============== ======================================== +==================== ======================================== +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:: @@ -161,13 +178,14 @@ 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 -=============== =================================================================================== -Model data Same as in ``FormEvents::POST_SET_DATA`` -Normalized data Data from the request reverse-transformed from the request using a view transformer -View data Same as in ``FormEvents::POST_SET_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:: @@ -193,13 +211,14 @@ model and view data have been denormalized. It can be used to fetch data after denormalization. -=============== ============================================================= -Data Type Value -=============== ============================================================= -Model data Normalized data reverse-transformed using a model transformer -Normalized data Same as in ``FormEvents::SUBMIT`` -View data Normalized data transformed using a view transformer -=============== ============================================================= +==================== =================================================================================== +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:: diff --git a/form/form_collections.rst b/form/form_collections.rst index a2726ed1ed6..b3caff2f436 100644 --- a/form/form_collections.rst +++ b/form/form_collections.rst @@ -529,7 +529,6 @@ Now, you need to put some code into the ``removeTag()`` method of ``Task``:: } } - The ``allow_delete`` option means that if an item of a collection isn't sent on submission, the related data is removed from the collection on the server. In order for this to work in an HTML form, you must remove diff --git a/form/form_customization.rst b/form/form_customization.rst index 26ec9e38c7f..005e0eac461 100644 --- a/form/form_customization.rst +++ b/form/form_customization.rst @@ -51,10 +51,12 @@ customized using other Twig functions, as illustrated in the following diagram: .. raw:: html - + The :ref:`form_label() `, -:ref:`form_widget() `, +:ref:`form_widget() ` (the HTML input), :ref:`form_help() ` and :ref:`form_errors() ` Twig functions give you total control over how each form field is rendered, so you can fully customize them: diff --git a/form/form_themes.rst b/form/form_themes.rst index 2f3a1a403ea..5f462ce4bbb 100644 --- a/form/form_themes.rst +++ b/form/form_themes.rst @@ -247,7 +247,9 @@ In both cases, the ``field-part`` can be any of these valid form field parts: .. raw:: html - + Fragment Naming for All Fields of the Same Type ............................................... diff --git a/form/tailwindcss.rst b/form/tailwindcss.rst new file mode 100644 index 00000000000..0a92bcd1ebc --- /dev/null +++ b/form/tailwindcss.rst @@ -0,0 +1,95 @@ +Tailwind CSS Form Theme +======================= + +Symfony provides a minimal form theme for `Tailwind CSS`_. Tailwind is a *utility first* +CSS framework and provides *unlimited ways* to customize your forms. Tailwind has +an official `form plugin`_ that provides a basic form reset that standardizes their look +on all browsers. This form theme requires this plugin and adds a few basic tailwind +classes so out of the box, your forms will look decent. Customization is almost always +going to be required so this theme makes that easy. + +.. image:: /_images/form/tailwindcss-form.png + :alt: An HTML form showing a range of form types styled using TailwindCSS. + +To use, first be sure you have installed and integrated `Tailwind CSS`_ and the +`form plugin`_. Follow their respective documentation to install both packages. + +If you prefer to use the Tailwind theme on a form by form basis, include the +``form_theme`` tag in the templates where those forms are used: + +.. code-block:: html+twig + + {# ... #} + {# this tag only applies to the forms defined in this template #} + {% form_theme form 'tailwind_2_layout.html.twig' %} + + {% block body %} +

User Sign Up:

+ {{ form(form) }} + {% endblock %} + +Customization +------------- + +Customizing CSS classes is especially important for this theme. + +Twig Form Functions +~~~~~~~~~~~~~~~~~~~ + +You can customize classes of individual fields by setting some class options. + +.. code-block:: twig + + {{ form_row(form.title, { + row_class: 'my row classes', + label_class: 'my label classes', + error_item_class: 'my error item classes', + widget_class: 'my widget classes', + widget_disabled_class: 'my disabled widget classes', + widget_errors_class: 'my widget with error classes', + }) }} + +When customizing the classes this way the defaults provided by the theme +are *overridden* opposed to merged as is the case with other themes. This +enables you to take full control of the classes without worrying about +*undoing* the generic defaults the theme provides. + +Project Specific Form Layout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you have a generic Tailwind style for all your forms, you can create +a custom form theme using the Tailwind CSS theme as a base. + +.. code-block:: twig + + {% use 'tailwind_2_layout.html.twig' %} + + {%- block form_row -%} + {%- set row_class = row_class|default('my row classes') -%} + {{- parent() -}} + {%- endblock form_row -%} + + {%- block widget_attributes -%} + {%- set widget_class = widget_class|default('my widget classes') -%} + {%- set widget_disabled_class = widget_disabled_class|default('my disabled widget classes') -%} + {%- set widget_errors_class = widget_errors_class|default('my widget with error classes') -%} + {{- parent() -}} + {%- endblock widget_attributes -%} + + {%- block form_label -%} + {%- set label_class = label_class|default('my label classes') -%} + {{- parent() -}} + {%- endblock form_label -%} + + {%- block form_help -%} + {%- set help_class = help_class|default('my label classes') -%} + {{- parent() -}} + {%- endblock form_help -%} + + {%- block form_errors -%} + {%- set error_item_class = error_item_class|default('my error item classes') -%} + {{- parent() -}} + {%- endblock form_errors -%} + +.. _`Tailwind CSS`: https://tailwindcss.com +.. _`form plugin`: https://github.com/tailwindlabs/tailwindcss-forms diff --git a/form/without_class.rst b/form/without_class.rst index d0a44ed6205..b2ebdcc5482 100644 --- a/form/without_class.rst +++ b/form/without_class.rst @@ -80,7 +80,10 @@ But if the form is not mapped to an object and you instead want to retrieve an array of your submitted data, how can you add constraints to the data of your form? -The answer is to set up the constraints yourself, and attach them to the individual +Constraints At Field Level +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +One possibility is to set up the constraints yourself, and attach them to the individual fields. The overall approach is covered a bit more in :doc:`this validation article `, but here's a short example:: @@ -123,3 +126,55 @@ but here's a short example:: When a form is only partially submitted (for example, in an HTTP PATCH request), only the constraints from the submitted form fields will be evaluated. + +Constraints At Class Level +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Another possibility is to add the constraints at the class level. +This can be done by setting the ``constraints`` option in the +``configureOptions()`` method:: + + use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\Form\FormBuilderInterface; + use Symfony\Component\OptionsResolver\OptionsResolver; + use Symfony\Component\Validator\Constraints\Length; + use Symfony\Component\Validator\Constraints\NotBlank; + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('firstName', TextType::class) + ->add('lastName', TextType::class); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $constraints = [ + 'firstName' => new Length(['min' => 3]), + 'lastName' => [ + new NotBlank(), + new Length(['min' => 3]), + ], + ]; + + $resolver->setDefaults([ + 'data_class' => null, + 'constraints' => $constraints, + ]); + } + +This means you can also do this when using the ``createFormBuilder()`` method +in your controller:: + + $form = $this->createFormBuilder($defaultData, [ + 'constraints' => [ + 'firstName' => new Length(['min' => 3]), + 'lastName' => [ + new NotBlank(), + new Length(['min' => 3]), + ], + ], + ]) + ->add('firstName', TextType::class) + ->add('lastName', TextType::class) + ->getForm(); diff --git a/forms.rst b/forms.rst index b3dd9e207a0..8b8a0534201 100644 --- a/forms.rst +++ b/forms.rst @@ -95,6 +95,22 @@ much easier to implement. There are tens of :doc:`form types provided by Symfony ` and you can also :doc:`create your own form types `. +.. tip:: + + You can use the ``debug:form`` to list all the available types, type + extensions and type guessers in your application: + + .. code-block:: terminal + + $ php bin/console debug:form + + # pass the form type FQCN to only show the options for that type, its parents and extensions. + # For built-in types, you can pass the short classname instead of the FQCN + $ php bin/console debug:form BirthdayType + + # pass also an option name to only display the full definition of that option + $ php bin/console debug:form BirthdayType label_attr + Building Forms -------------- @@ -824,7 +840,7 @@ to the ``form()`` or the ``form_start()`` helper functions: that stores this method. The form will be submitted in a normal ``POST`` request, but :doc:`Symfony's routing ` is capable of detecting the ``_method`` parameter and will interpret it as a ``PUT``, ``PATCH`` or - ``DELETE`` request. The :ref:`configuration-framework-http_method_override` + ``DELETE`` request. The :ref:`http_method_override ` option must be enabled for this to work. Changing the Form Name @@ -1029,6 +1045,7 @@ Form Themes and Customization: /form/bootstrap4 /form/bootstrap5 + /form/tailwindcss /form/form_customization /form/form_themes diff --git a/frontend/create_ux_bundle.rst b/frontend/create_ux_bundle.rst index 12002d10356..87d6b123e42 100644 --- a/frontend/create_ux_bundle.rst +++ b/frontend/create_ux_bundle.rst @@ -47,8 +47,8 @@ to the ``peerDependencies``: "fetch": "eager", "enabled": true, "autoimport": { - "dist/bootstrap4-theme.css": false, - "dist/bootstrap5-theme.css": true + "@acme/feature/dist/bootstrap4-theme.css": false, + "@acme/feature/dist/bootstrap5-theme.css": true } } } @@ -87,11 +87,11 @@ In this case, the file located at ``[assets directory]/dist/controller.js`` will } } - 2. Run either ``npm install`` or ``yarn install`` to install the new dependencies. + 2. Run ``npm install`` to install the new dependencies. 3. Write your Stimulus controller with TypeScript in ``src/controller.ts``. - 4. Run ``npm run build`` or ``yarn run build`` to transpile your TypeScript controller into JavaScript. + 4. Run ``npm run build`` to transpile your TypeScript controller into JavaScript. To use your controller in a template (e.g. one defined in your bundle) you can use it like this: diff --git a/frontend/custom_version_strategy.rst b/frontend/custom_version_strategy.rst index 04a2d45f245..3ae6b20bc44 100644 --- a/frontend/custom_version_strategy.rst +++ b/frontend/custom_version_strategy.rst @@ -68,7 +68,7 @@ version string:: * @param string $manifestPath * @param string|null $format */ - public function __construct(string $manifestPath, string $format = null) + public function __construct(string $manifestPath, ?string $format = null) { $this->manifestPath = $manifestPath; $this->format = $format ?: '%s?%s'; @@ -152,7 +152,6 @@ After creating the strategy PHP class, register it as a Symfony service. ); }; - Finally, enable the new asset versioning for all the application assets or just for some :ref:`asset package ` thanks to the :ref:`version_strategy ` option: diff --git a/frontend/encore/advanced-config.rst b/frontend/encore/advanced-config.rst index cfe50ee1658..c6ad915acdb 100644 --- a/frontend/encore/advanced-config.rst +++ b/frontend/encore/advanced-config.rst @@ -105,10 +105,6 @@ prefer to build configs separately, pass the ``--config-name`` option: .. code-block:: terminal - # if you use the Yarn package manager - $ yarn encore dev --config-name firstConfig - - # if you use the npm package manager $ npm run dev -- --config-name firstConfig Next, define the output directories of each build: @@ -148,6 +144,60 @@ functions to specify which build to use: {{ encore_entry_script_tags('mobile', null, 'secondConfig') }} {{ encore_entry_link_tags('mobile', null, 'secondConfig') }} +Avoid Missing CSS When Rendering Multiple Templates +--------------------------------------------------- + +When you render two or more templates in the same request, such as two emails, +you should call the ``reset()`` method on the ``EntrypointLookupInterface`` interface. +To do this, inject the ``EntrypointLookupInterface`` interface:: + + public function __construct(EntrypointLookupInterface $entryPointLookup) {} + + public function send() { + $this->twig->render($emailOne); + $this->entryPointLookup->reset(); + $this->render($emailTwo); + } + +If you are using multiple Webpack configurations (e.g. one for the admin and one +for emails) you will need to inject the right ``EntrypointLookupInterface`` service. +Use the following command to find the right service: + +.. code-block:: terminal + + $ php bin/console console debug:container entrypoint_lookup + + # You will see a result similar to this: + Select one of the following services to display its information: + [0] webpack_encore.entrypoint_lookup_collection + [1] webpack_encore.entrypoint_lookup.cache_warmer + [2] webpack_encore.entrypoint_lookup[_default] + [3] webpack_encore.entrypoint_lookup[admin] + [4] webpack_encore.entrypoint_lookup[email] + +In this example, the configuration related to the ``email`` configuration is +the one called ``webpack_encore.entrypoint_lookup[email]``. + +To inject this service into your class, use the ``bind`` option: + +.. code-block:: yaml + + # config/services.yaml + services: + _defaults + bind: + Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface $entryPointLookupEmail: '@webpack_encore.entrypoint_lookup[email]' + +Now you can inject your service into your class:: + + public function __construct(EntrypointLookupInterface $entryPointLookupEmail) {} + + public function send() { + $this->twig->render($emailOne); + $this->entryPointLookupEmail->reset(); + $this->render($emailTwo); + } + Generating a Webpack Configuration Object without using the Command-Line Interface ---------------------------------------------------------------------------------- diff --git a/frontend/encore/babel.rst b/frontend/encore/babel.rst index 95ba6086913..133559b1a0d 100644 --- a/frontend/encore/babel.rst +++ b/frontend/encore/babel.rst @@ -49,6 +49,23 @@ cache directory: # On Unix run this command. On Windows, clear this directory manually $ rm -rf node_modules/.cache/babel-loader/ +If you want to customize the ``preset-env`` configuration, use the ``configureBabelPresetEnv()`` +method to add any of the `@babel/preset-env configuration options`_: + +.. code-block:: javascript + + // webpack.config.js + // ... + + Encore + // ... + + .configureBabelPresetEnv((config) => { + config.useBuiltIns = 'usage'; + config.corejs = 3; + }) + ; + Creating a ``.babelrc`` File ---------------------------- @@ -63,3 +80,4 @@ As soon as a ``.babelrc`` file is present, it will take priority over the Babel configuration added by Encore. .. _`Babel`: https://babeljs.io/ +.. _`@babel/preset-env configuration options`: https://babeljs.io/docs/babel-preset-env diff --git a/frontend/encore/bootstrap.rst b/frontend/encore/bootstrap.rst index f5b3959eafd..f027b22ddc4 100644 --- a/frontend/encore/bootstrap.rst +++ b/frontend/encore/bootstrap.rst @@ -7,10 +7,6 @@ First, to be able to customize things further, we'll install ``bootstrap``: .. code-block:: terminal - # if you use the Yarn package manager - $ yarn add bootstrap --dev - - # if you use the npm package manager $ npm install bootstrap --save-dev Importing Bootstrap Styles @@ -46,11 +42,6 @@ used in your application: .. code-block:: terminal - # if you use the Yarn package manager - # (jQuery is only required in versions prior to Bootstrap 5) - $ yarn add jquery @popperjs/core --dev - - # if you use the npm package manager # (jQuery is only required in versions prior to Bootstrap 5) $ npm install jquery @popperjs/core --save-dev diff --git a/frontend/encore/dev-server.rst b/frontend/encore/dev-server.rst index 4feecb3deec..01501178caf 100644 --- a/frontend/encore/dev-server.rst +++ b/frontend/encore/dev-server.rst @@ -1,15 +1,11 @@ Using webpack-dev-server and HMR ================================ -While developing, instead of using ``yarn encore dev --watch``, you can use the +While developing, instead of using ``npx encore dev --watch``, you can use the `webpack-dev-server`_: .. code-block:: terminal - # if you use the Yarn package manager - $ yarn encore dev-server - - # if you use the npm package manager $ npm run dev-server This builds and serves the front-end assets from a new server. This server runs at @@ -17,10 +13,10 @@ This builds and serves the front-end assets from a new server. This server runs This server does not actually write the files to disk; instead it serves them from memory, allowing for hot module reloading. -As a consequence, the ``link`` and ``script`` tags need to point to the new server. If you're using the -``encore_entry_script_tags()`` and ``encore_entry_link_tags()`` Twig shortcuts (or are -:ref:`processing your assets through entrypoints.json ` in some other way), -you're done: the paths in your templates will automatically point to the dev server. +As a consequence, the ``link`` and ``script`` tags need to point to the new server. +If you're using the ``encore_entry_script_tags()`` and ``encore_entry_link_tags()`` +Twig shortcuts (or are :ref:`processing your assets through entrypoints.json ` +in some other way), you're done: the paths in your templates will automatically point to the dev server. dev-server Options ------------------ @@ -30,10 +26,6 @@ You can set these options via command line options: .. code-block:: terminal - # if you use the Yarn package manager - $ yarn encore dev-server --port 9000 - - # if you use the npm package manager $ npm run dev-server -- --port 9000 You can also set these options using the ``Encore.configureDevServerOptions()`` @@ -85,16 +77,12 @@ server SSL certificate: .. note:: - If you are using Node.js 17 or newer, you have to run the ``dev-server`` command with the - ``--openssl-legacy-provider`` option: - - .. code-block:: terminal + If you are using Node.js 17 or newer and ``dev-server`` fails to start with TLS error, + the certificate file might be generated by an old version of **symfony-cli**. Upgrade + **symfony-cli** to the latest version, delete the old ``~/.symfony5/certs/default.p12`` file, + and start symfony server again. - # if you use the Yarn package manager - $ NODE_OPTIONS=--openssl-legacy-provider yarn encore dev-server - - # if you use the npm package manager - $ NODE_OPTIONS=--openssl-legacy-provider npm run dev-server + This generates a new ``default.p12`` file suitable for use with recent Node.js versions. CORS Issues ----------- @@ -134,7 +122,6 @@ Live Reloading when changing PHP / Twig Files To utilize the HMR superpower along with live reload for your PHP code and templates, set the following options: - .. code-block:: javascript // webpack.config.js diff --git a/frontend/encore/faq.rst b/frontend/encore/faq.rst index 6c1392ac5a0..709df68d2b3 100644 --- a/frontend/encore/faq.rst +++ b/frontend/encore/faq.rst @@ -53,7 +53,7 @@ and the built files. Your ``.gitignore`` file should include: # whatever path you're passing to Encore.setOutputPath() /public/build -You *should* commit all of your source asset files, ``package.json`` and ``yarn.lock`` or ``package-lock.json``. +You *should* commit all of your source asset files, ``package.json`` and ``package-lock.json``. My App Lives under a Subdirectory --------------------------------- @@ -105,8 +105,8 @@ file script tag is rendered automatically. This dependency was not found: some-module in ./path/to/file.js --------------------------------------------------------------- -Usually, after you install a package via yarn or npm, you can require / import -it to use it. For example, after running ``yarn add respond.js`` or ``npm install respond.js``, +Usually, after you install a package via npm, you can require / import +it to use it. For example, after running ``npm install respond.js``, you try to require that module: .. code-block:: javascript @@ -137,8 +137,6 @@ For performance, Encore does not process libraries inside ``node_modules/`` thro Babel. But, you can change that via the ``configureBabel()`` method. See :doc:`/frontend/encore/babel` for details. -.. _`rsync`: https://rsync.samba.org/ - How Do I Integrate my Encore Configuration with my IDE? ------------------------------------------------------- @@ -153,7 +151,7 @@ productive (for example by resolving aliases). However, you may face this error: calling Encore directly. It fails because the Encore Runtime Environment is only configured when you are -running it (e.g. when executing ``yarn encore dev``). Fix this issue calling to +running it (e.g. when executing ``npx encore dev``). Fix this issue calling to ``Encore.isRuntimeEnvironmentConfigured()`` and ``Encore.configureRuntimeEnvironment()`` methods: @@ -168,8 +166,6 @@ running it (e.g. when executing ``yarn encore dev``). Fix this issue calling to // ... the rest of the Encore configuration -.. _`Webpack integration in PhpStorm`: https://www.jetbrains.com/help/phpstorm/using-webpack.html - My Tests are Failing Because of ``entrypoints.json`` File --------------------------------------------------------- @@ -194,3 +190,6 @@ functions to trigger exceptions when there's no ``entrypoints.json`` file): webpack_encore: strict_mode: false # ... + +.. _`rsync`: https://rsync.samba.org/ +.. _`Webpack integration in PhpStorm`: https://www.jetbrains.com/help/phpstorm/using-webpack.html diff --git a/frontend/encore/installation.rst b/frontend/encore/installation.rst index 118e15e7b0e..1753b770b36 100644 --- a/frontend/encore/installation.rst +++ b/frontend/encore/installation.rst @@ -1,12 +1,8 @@ Installing Encore ================= -First, make sure you `install Node.js`_. Optionally you can also install the -`Yarn package manager`_. In the next sections you will always see the commands -for both ``npm`` and ``yarn``, but you only need to run one of them. - -The following instructions depend on whether you are installing Encore in a -Symfony application or not. +First, make sure you `install Node.js`_. Then, follow the instructions below, +which depend on whether you are installing Encore in a Symfony application or not. Installing Encore in Symfony Applications ----------------------------------------- @@ -17,11 +13,6 @@ project: .. code-block:: terminal $ composer require symfony/webpack-encore-bundle - - # if you use the Yarn package manager - $ yarn install - - # if you use the npm package manager $ npm install If you are using :ref:`Symfony Flex `, this will install and enable @@ -36,24 +27,19 @@ and files by yourself following the instructions shown in the next section. Installing Encore in non Symfony Applications --------------------------------------------- -Install Encore into your project via Yarn: +Install Encore into your project via npm: .. code-block:: terminal - # if you use the Yarn package manager - $ yarn add @symfony/webpack-encore --dev - - # if you use the npm package manager $ npm install @symfony/webpack-encore --save-dev This command creates (or modifies) a ``package.json`` file and downloads -dependencies into a ``node_modules/`` directory. Yarn also creates/updates a -``yarn.lock`` (called ``package-lock.json`` if you use npm). +dependencies into a ``node_modules/`` directory. .. tip:: - You *should* commit ``package.json`` and ``yarn.lock`` (or ``package-lock.json`` - if using npm) to version control, but ignore ``node_modules/``. + You *should* commit ``package.json`` and ``package-lock.json`` + to version control, but ignore ``node_modules/``. Creating the webpack.config.js File ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -223,5 +209,4 @@ on which features of Encore you have enabled. :doc:`split chunks `. .. _`install Node.js`: https://nodejs.org/en/download/ -.. _`Yarn package manager`: https://yarnpkg.com/getting-started/install .. _`WebpackEncoreBundle`: https://github.com/symfony/webpack-encore-bundle diff --git a/frontend/encore/postcss.rst b/frontend/encore/postcss.rst index 40b2dc43923..9f42f2606d0 100644 --- a/frontend/encore/postcss.rst +++ b/frontend/encore/postcss.rst @@ -23,10 +23,6 @@ Next, download any plugins you want, like ``autoprefixer``: .. code-block:: terminal - # if you use the Yarn package manager - $ yarn add autoprefixer --dev - - # if you use the npm package manager $ npm install autoprefixer --save-dev Next, create a ``postcss.config.js`` file at the root of your project: @@ -36,7 +32,7 @@ Next, create a ``postcss.config.js`` file at the root of your project: module.exports = { plugins: { // include whatever plugins you want - // but make sure you install these via yarn or npm! + // but make sure you install these via npm! // add browserslist config to package.json (see below) autoprefixer: {} diff --git a/frontend/encore/reactjs.rst b/frontend/encore/reactjs.rst index facd7cdcbb6..c12783781c3 100644 --- a/frontend/encore/reactjs.rst +++ b/frontend/encore/reactjs.rst @@ -9,15 +9,11 @@ Enabling React.js .. tip:: Check out live demos of Symfony UX React component at `https://ux.symfony.com/react`_! - -Using React? First add some dependencies with Yarn: -.. code-block:: terminal +Using React? First add some dependencies with npm: - # if you use the Yarn package manager - $ yarn add react react-dom prop-types +.. code-block:: terminal - # if you use the npm package manager $ npm install react react-dom prop-types --save Enable react in your ``webpack.config.js``: @@ -32,7 +28,6 @@ Enable react in your ``webpack.config.js``: + .enableReactPreset() ; - 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! diff --git a/frontend/encore/simple-example.rst b/frontend/encore/simple-example.rst index 2d46a392293..ce15976e464 100644 --- a/frontend/encore/simple-example.rst +++ b/frontend/encore/simple-example.rst @@ -56,33 +56,32 @@ together and - thanks to the first ``app`` argument - output final ``app.js`` an .. _encore-build-assets: -To build the assets, run the following if you use the Yarn package manager: +To build the assets, run the following if you use the npm package manager: .. code-block:: terminal # compile assets and automatically re-compile when files change - $ yarn watch - # or $ npm run watch # or, run a dev-server that can sometimes update your code without refreshing the page - $ yarn dev-server - # or $ npm run dev-server # compile assets once - $ yarn dev - # or $ npm run dev # on deploy, create a production build - $ yarn build - # or $ npm run build All of these commands - e.g. ``dev`` or ``watch`` - are shortcuts that are defined in your ``package.json`` file. +.. tip:: + + 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. + .. caution:: Whenever you make changes in your ``webpack.config.js`` file, you must @@ -179,10 +178,6 @@ We'll use jQuery to print this message on the page. Install it via: .. code-block:: terminal - # if you use the Yarn package manager - $ yarn add jquery --dev - - # if you use the npm package manager $ npm install jquery --save-dev Great! Use ``import`` to import ``jquery`` and ``greet.js``: @@ -365,10 +360,6 @@ and restart Encore: .. code-block:: terminal - # if you use the Yarn package manager - $ yarn watch - - # if you use the npm package manager $ npm run watch Webpack will now output a new ``checkout.js`` file and a new ``account.js`` file @@ -430,18 +421,12 @@ Encore. When you do, you'll see an error! .. code-block:: terminal > Error: Install sass-loader & sass to use enableSassLoader() - > yarn add sass-loader@^13.0.0 sass --dev Encore supports many features. But, instead of forcing all of them on you, when you need a feature, Encore will tell you what you need to install. Run: .. code-block:: terminal - # if you use the Yarn package manager - $ yarn add sass-loader@^13.0.0 sass --dev - $ yarn encore dev --watch - - # if you use the npm package manager $ npm install sass-loader@^13.0.0 sass --save-dev $ npm run watch diff --git a/frontend/encore/split-chunks.rst b/frontend/encore/split-chunks.rst index 7739b0a49c6..f9d2353a75e 100644 --- a/frontend/encore/split-chunks.rst +++ b/frontend/encore/split-chunks.rst @@ -22,7 +22,6 @@ To enable this, call ``splitEntryChunks()``: + .splitEntryChunks() - Now, each output file (e.g. ``homepage.js``) *may* be split into multiple file (e.g. ``homepage.js`` & ``vendors-node_modules_jquery_dist_jquery_js.js`` - the filename of the second will be less obvious when you build for production). This diff --git a/frontend/encore/vuejs.rst b/frontend/encore/vuejs.rst index ca39808483e..1c403721188 100644 --- a/frontend/encore/vuejs.rst +++ b/frontend/encore/vuejs.rst @@ -73,10 +73,6 @@ your Vue.js app update *without* a browser refresh! To activate it, use the .. code-block:: terminal - # if you use the Yarn package manager - $ yarn encore dev-server - - # if you use the npm package manager $ npm run dev-server That's it! Change one of your ``.vue`` files and watch your browser update. But diff --git a/http_cache/cache_invalidation.rst b/http_cache/cache_invalidation.rst index 76c13ab975b..8e0b022a5a1 100644 --- a/http_cache/cache_invalidation.rst +++ b/http_cache/cache_invalidation.rst @@ -136,7 +136,7 @@ Then, register the class as a service that :doc:`decorates client = $client->withOptions( + (new HttpOptions()) + ->setBaseUri('https://...') + ->setHeaders(['header-name' => 'header-value']) + ->toArray() + ); + Some options are described in this guide: * `Authentication`_ @@ -477,6 +487,11 @@ each request (which overrides any global authentication): // ... ]); +.. note:: + + Basic Authentication can also be set by including the credentials in the URL, + such as: ``http://the-username:the-password@example.com`` + .. note:: The NTLM authentication mechanism requires using the cURL transport. @@ -666,6 +681,7 @@ when the streams are large):: $client->request('POST', 'https://...', [ // ... 'body' => $formData->bodyToString(), + 'headers' => $formData->getPreparedHeaders()->toArray(), ]); If you need to add a custom HTTP header to the upload, you can do:: @@ -683,17 +699,21 @@ cookies automatically. You can either :ref:`send cookies with the BrowserKit component `, which integrates seamlessly with the HttpClient component, or manually setting -the ``Cookie`` HTTP header as follows:: +`the Cookie HTTP request header`_ as follows:: use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpFoundation\Cookie; $client = HttpClient::create([ 'headers' => [ - 'Cookie' => new Cookie('flavor', 'chocolate', strtotime('+1 day')), + // set one cookie as a name=value pair + 'Cookie' => 'flavor=chocolate', + + // you can set multiple cookies at once separating them with a ; + 'Cookie' => 'flavor=chocolate; size=medium', - // you can also pass the cookie contents as a string - 'Cookie' => 'flavor=chocolate; expires=Sat, 11 Feb 2023 12:18:13 GMT; Max-Age=86400; path=/' + // if needed, encode the cookie value to ensure that it contains valid characters + 'Cookie' => sprintf("%s=%s", 'foo', rawurlencode('...')), ], ]); @@ -854,15 +874,28 @@ To leverage all these design benefits, the cURL extension is needed. Enabling cURL Support ~~~~~~~~~~~~~~~~~~~~~ -This component supports both the native PHP streams and cURL to make the HTTP -requests. Although both are interchangeable and provide the same features, -including concurrent requests, HTTP/2 is only supported when using cURL. +This component can make HTTP requests using native PHP streams and the +``amphp/http-client`` and cURL libraries. Although they are interchangeable and +provide the same features, including concurrent requests, HTTP/2 is only supported +when using cURL or ``amphp/http-client``. + +.. note:: + + To use the :class:`Symfony\\Component\\HttpClient\\AmpHttpClient`, the + `amphp/http-client`_ package must be installed. + +.. versionadded:: 5.1 + + Integration with ``amphp/http-client`` was introduced in Symfony 5.1. The :method:`Symfony\\Component\\HttpClient\\HttpClient::create` method -selects the cURL transport if the `cURL PHP extension`_ is enabled and falls -back to PHP streams otherwise. If you prefer to select the transport -explicitly, use the following classes to create the client:: +selects the cURL transport if the `cURL PHP extension`_ is enabled. It falls +back to ``AmpHttpClient`` if cURL couldn't be found or is too old. Finally, if +``AmpHttpClient`` is not available, it falls back to PHP streams. +If you prefer to select the transport explicitly, use the following classes +to create the client:: + use Symfony\Component\HttpClient\AmpHttpClient; use Symfony\Component\HttpClient\CurlHttpClient; use Symfony\Component\HttpClient\NativeHttpClient; @@ -872,9 +905,12 @@ explicitly, use the following classes to create the client:: // uses the cURL PHP extension $client = new CurlHttpClient(); + // uses the client from the `amphp/http-client` package + $client = new AmpHttpClient(); + When using this component in a full-stack Symfony application, this behavior is not configurable and cURL will be used automatically if the cURL PHP extension -is installed and enabled. Otherwise, the native PHP streams will be used. +is installed and enabled, and will fall back as explained above. Configuring CurlHttpClient Options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -913,8 +949,8 @@ HTTP Compression The HTTP header ``Accept-Encoding: gzip`` is added automatically if: -* When using cURL client: cURL was compiled with ZLib support (see ``php --ri curl``) -* When using the native HTTP client: `Zlib PHP extension`_ is installed +* using cURL client: cURL was compiled with ZLib support (see ``php --ri curl``) +* using the native HTTP client: `Zlib PHP extension`_ is installed If the server does respond with a gzipped response, it's decoded transparently. To disable HTTP compression, send an ``Accept-Encoding: identity`` HTTP header. @@ -928,7 +964,7 @@ HTTP/2 Support When requesting an ``https`` URL, HTTP/2 is enabled by default if one of the following tools is installed: -* The `libcurl`_ package version 7.36 or higher; +* The `libcurl`_ package version 7.36 or higher, used with PHP >= 7.2.17 / 7.3.4; * The `amphp/http-client`_ Packagist package version 4.2 or higher. .. versionadded:: 5.1 @@ -984,9 +1020,9 @@ To force HTTP/2 for ``http`` URLs, you need to enable it explicitly via the $client = HttpClient::create(['http_version' => '2.0']); -Support for HTTP/2 PUSH works out of the box when libcurl >= 7.61 is used with -PHP >= 7.2.17 / 7.3.4: pushed responses are put into a temporary cache and are -used when a subsequent request is triggered for the corresponding URLs. +Support for HTTP/2 PUSH works out of the box when using a compatible client: +pushed responses are put into a temporary cache and are used when a +subsequent request is triggered for the corresponding URLs. Processing Responses -------------------- @@ -1027,6 +1063,10 @@ following methods:: // returns detailed logs about the requests and responses of the HTTP transaction $httpLogs = $response->getInfo('debug'); + // the special "pause_handler" info item is a callable that allows to delay the request + // for a given number of seconds; this allows you to delay retries, throttle streams, etc. + $response->getInfo('pause_handler')(2); + .. note:: ``$response->toStream()`` is part of :class:`Symfony\\Component\\HttpClient\\Response\\StreamableInterface`. @@ -1037,6 +1077,10 @@ following methods:: about the response. Some of them might not be known yet (e.g. ``http_code``) when you'll call it. +.. versionadded:: 5.2 + + The ``pause_handler`` info item was introduced in Symfony 5.2. + .. _http-client-streaming-responses: Streaming Responses @@ -1334,7 +1378,7 @@ installed in your application:: // this won't hit the network if the resource is already in the cache $response = $client->request('GET', 'https://example.com/cacheable-resource'); -:class:`Symfony\\Component\\HttpClient\\CachingHttpClient`` accepts a third argument +:class:`Symfony\\Component\\HttpClient\\CachingHttpClient` accepts a third argument to set the options of the :class:`Symfony\\Component\\HttpKernel\\HttpCache\\HttpCache`. Consuming Server-Sent Events @@ -1617,7 +1661,7 @@ If you want to extend the behavior of a base HTTP client, you can use { private $decoratedClient; - public function __construct(HttpClientInterface $decoratedClient = null) + public function __construct(?HttpClientInterface $decoratedClient = null) { $this->decoratedClient = $decoratedClient ?? HttpClient::create(); } @@ -1633,7 +1677,7 @@ If you want to extend the behavior of a base HTTP client, you can use return $response; } - public function stream($responses, float $timeout = null): ResponseStreamInterface + public function stream($responses, ?float $timeout = null): ResponseStreamInterface { return $this->decoratedClient->stream($responses, $timeout); } @@ -1785,7 +1829,7 @@ assertions on the request before returning the mocked response:: }, ]; - $client = new MockHttpClient($expectedRequest); + $client = new MockHttpClient($expectedRequests); // ... @@ -2055,3 +2099,4 @@ test it in a real application:: .. _`EventSource`: https://www.w3.org/TR/eventsource/#eventsource .. _`idempotent method`: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods .. _`SSRF`: https://portswigger.net/web-security/ssrf +.. _`the Cookie HTTP request header`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie diff --git a/introduction/from_flat_php_to_symfony.rst b/introduction/from_flat_php_to_symfony.rst index 5affeaa5f99..7386f629546 100644 --- a/introduction/from_flat_php_to_symfony.rst +++ b/introduction/from_flat_php_to_symfony.rst @@ -657,7 +657,9 @@ It's a beautiful thing. .. raw:: html - + Where Symfony Delivers ---------------------- diff --git a/introduction/http_fundamentals.rst b/introduction/http_fundamentals.rst index 802429b5253..fceb6a4a13d 100644 --- a/introduction/http_fundamentals.rst +++ b/introduction/http_fundamentals.rst @@ -17,8 +17,11 @@ HTTP (Hypertext Transfer Protocol) is a text language that allows two machines to communicate with each other. For example, when checking for the latest `xkcd`_ comic, the following (approximate) conversation takes place: -.. image:: /_images/http/xkcd-full.png - :align: center +.. raw:: html + + HTTP is the term used to describe this text-based language. The goal of your server is *always* to understand text requests and return text responses. @@ -38,8 +41,11 @@ 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 +.. raw:: html + + In HTTP-speak, this HTTP request would actually look something like this: @@ -100,8 +106,11 @@ client needs (via the URI) and what the client wants to do with that resource prepares the resource and returns it in an HTTP response. Consider the response from the xkcd web server: -.. image:: /_images/http/xkcd-full.png - :align: center +.. raw:: html + + Translated into HTTP, the response sent back to the browser will look something like this: @@ -346,7 +355,9 @@ to do: .. raw:: html - + Incoming requests are interpreted by the :doc:`Routing component ` and passed to PHP functions that return ``Response`` objects. diff --git a/logging.rst b/logging.rst index c546701f78d..978bd57da28 100644 --- a/logging.rst +++ b/logging.rst @@ -154,6 +154,7 @@ to write logs using the :phpfunction:`syslog` function: .. code-block:: php // config/packages/prod/monolog.php + use Psr\Log\LogLevel; use Symfony\Config\MonologConfig; return static function (MonologConfig $monolog) { @@ -162,13 +163,13 @@ to write logs using the :phpfunction:`syslog` function: ->type('stream') // log to var/logs/(environment).log ->path('%kernel.logs_dir%/%kernel.environment%.log') - // log *all* messages (debug is lowest level) - ->level('debug'); + // log *all* messages (LogLevel::DEBUG is lowest level) + ->level(LogLevel::DEBUG); $monolog->handler('syslog_handler') ->type('syslog') // log error-level messages and higher - ->level('error'); + ->level(LogLevel::ERROR); }; This defines a *stack* of handlers and each handler is called in the order that it's @@ -253,13 +254,14 @@ one of the messages reaches an ``action_level``. Take this example: .. code-block:: php // config/packages/prod/monolog.php + use Psr\Log\LogLevel; use Symfony\Config\MonologConfig; return static function (MonologConfig $monolog) { $monolog->handler('filter_for_errors') ->type('fingers_crossed') // if *one* log is error or higher, pass *all* to file_log - ->actionLevel('error') + ->actionLevel(LogLevel::ERROR) ->handler('file_log') ; @@ -267,17 +269,17 @@ one of the messages reaches an ``action_level``. Take this example: $monolog->handler('file_log') ->type('stream') ->path('%kernel.logs_dir%/%kernel.environment%.log') - ->level('debug') + ->level(LogLevel::DEBUG) ; // still passed *all* logs, and still only logs error or higher $monolog->handler('syslog_handler') ->type('syslog') - ->level('error') + ->level(LogLevel::ERROR) ; }; -Now, if even one log entry has an ``error`` level or higher, then *all* log entries +Now, if even one log entry has an ``LogLevel::ERROR`` level or higher, then *all* log entries for that request are saved to a file via the ``file_log`` handler. That means that your log file will contain *all* the details about the problematic request - making debugging much easier! @@ -348,13 +350,14 @@ option of your handler to ``rotating_file``: .. code-block:: php // config/packages/prod/monolog.php + use Psr\Log\LogLevel; use Symfony\Config\MonologConfig; return static function (MonologConfig $monolog) { $monolog->handler('main') ->type('rotating_file') ->path('%kernel.logs_dir%/%kernel.environment%.log') - ->level('debug') + ->level(LogLevel::DEBUG) // max number of log files to keep // defaults to zero, which means infinite files ->maxFiles(10); diff --git a/logging/channels_handlers.rst b/logging/channels_handlers.rst index b53c136b5bc..387e25a59f4 100644 --- a/logging/channels_handlers.rst +++ b/logging/channels_handlers.rst @@ -23,27 +23,27 @@ Switching a Channel to a different Handler Now, suppose you want to log the ``security`` channel to a different file. To do this, create a new handler and configure it to log only messages from the ``security`` channel. The following example does that only in the -``prod`` :ref:`configuration environment ` but you -can do it in any (or all) environments: +``prod`` :ref:`configuration environment `: .. configuration-block:: .. code-block:: yaml - # config/packages/prod/monolog.yaml - monolog: - handlers: - security: - # log all messages (since debug is the lowest level) - level: debug - type: stream - path: '%kernel.logs_dir%/security.log' - channels: [security] - - # an example of *not* logging security channel messages for this handler - main: - # ... - # channels: ['!security'] + # config/packages/monolog.yaml + when@prod: + monolog: + handlers: + security: + # log all messages (since debug is the lowest level) + level: debug + type: stream + path: '%kernel.logs_dir%/security.log' + channels: [security] + + # an example of *not* logging security channel messages for this handler + main: + # ... + # channels: ['!security'] .. code-block:: xml @@ -56,12 +56,15 @@ can do it in any (or all) environments: http://symfony.com/schema/dic/monolog https://symfony.com/schema/dic/monolog/monolog-1.0.xsd"> - - - - security - - + + + + + security + + + + @@ -75,18 +78,21 @@ can do it in any (or all) environments: .. code-block:: php // config/packages/prod/monolog.php + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Config\MonologConfig; - return static function (MonologConfig $monolog) { - $monolog->handler('security') - ->type('stream') - ->path('%kernel.logs_dir%/security.log') - ->channels()->elements(['security']); + return static function (MonologConfig $monolog, ContainerConfigurator $container) { + if ('prod' === $container->env()) { + $monolog->handler('security') + ->type('stream') + ->path(param('kernel.logs_dir') . \DIRECTORY_SEPARATOR . 'security.log') + ->channels()->elements(['security']); - $monolog->handler('main') - // ... + $monolog->handler('main') + // ... - ->channels()->elements(['!security']); + ->channels()->elements(['!security']); + } }; .. caution:: @@ -131,13 +137,13 @@ You can also configure additional channels without the need to tag your services .. code-block:: yaml - # config/packages/prod/monolog.yaml + # config/packages/monolog.yaml monolog: channels: ['foo', 'bar', 'foo_bar'] .. code-block:: xml - + logger = $fooBarLogger; } +Configure Logger Channels with Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Starting from `Monolog`_ 3.5 you can also configure the logger channel +by using the ``#[WithMonologChannel]`` attribute directly on your service +class:: + + // src/Service/MyFixtureService.php + namespace App\Service; + + use Monolog\Attribute\WithMonologChannel; + use Psr\Log\LoggerInterface; + use Symfony\Bridge\Monolog\Logger; + + #[WithMonologChannel('fixtures')] + class MyFixtureService + { + public function __construct(LoggerInterface $logger) + { + // ... + } + } + +This way you can avoid declaring your service manually to use a specific +channel. + +.. versionadded:: 3.5 + + The ``#[WithMonologChannel]`` attribute was introduced in Monolog 3.5.0. + .. _`MonologBundle`: https://github.com/symfony/monolog-bundle +.. _`Monolog`: https://github.com/Seldaek/monolog diff --git a/logging/handlers.rst b/logging/handlers.rst index 8821113405e..37ad7ca0269 100644 --- a/logging/handlers.rst +++ b/logging/handlers.rst @@ -8,14 +8,6 @@ This handler deals directly with the HTTP interface of Elasticsearch. This means it will slow down your application if Elasticsearch takes time to answer. Even if all HTTP calls are done asynchronously. -In a development environment, it's fine to keep the default configuration: for -each log, an HTTP request will be made to push the log to Elasticsearch. - -In a production environment, it's highly recommended to wrap this handler in a -handler with buffering capabilities (like the ``FingersCrossedHandler`` or -``BufferHandler``) in order to call Elasticsearch only once with a bulk push. For -even better performance and fault tolerance, a proper `ELK stack`_ is recommended. - To use it, declare it as a service: .. configuration-block:: @@ -87,7 +79,10 @@ To use it, declare it as a service: The ``$elasticsearchVersion`` argument was introduced in Symfony 5.4. -Then reference it in the Monolog configuration: +Then reference it in the Monolog configuration. + +In a development environment, it's fine to keep the default configuration: for +each log, an HTTP request will be made to push the log to Elasticsearch: .. configuration-block:: @@ -134,4 +129,69 @@ Then reference it in the Monolog configuration: ; }; +In a production environment, it's highly recommended to wrap this handler in a +handler with buffering capabilities (like the `FingersCrossedHandler`_ or +`BufferHandler`_) in order to call Elasticsearch only once with a bulk push. For +even better performance and fault tolerance, a proper `ELK stack`_ is recommended. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/prod/monolog.yaml + monolog: + handlers: + main: + type: fingers_crossed + handler: es + + es: + type: service + id: Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // config/packages/prod/monolog.php + use Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler; + use Symfony\Config\MonologConfig; + + return static function (MonologConfig $monolog): void { + $monolog->handler('main') + ->type('fingers_crossed') + ->handler('es') + ; + $monolog->handler('es') + ->type('service') + ->id(ElasticsearchLogstashHandler::class) + ; + }; + +.. _`BufferHandler`: https://github.com/Seldaek/monolog/blob/main/src/Monolog/Handler/BufferHandler.php .. _`ELK stack`: https://www.elastic.co/what-is/elk-stack +.. _`FingersCrossedHandler`: https://github.com/Seldaek/monolog/blob/main/src/Monolog/Handler/FingersCrossedHandler.php diff --git a/logging/monolog_console.rst b/logging/monolog_console.rst index 133185937c7..6df9111eca8 100644 --- a/logging/monolog_console.rst +++ b/logging/monolog_console.rst @@ -46,6 +46,7 @@ The example above could then be rewritten as:: public function __construct(LoggerInterface $logger) { + parent::__construct(); $this->logger = $logger; } diff --git a/logging/monolog_email.rst b/logging/monolog_email.rst index 3302707bea3..350a5646a31 100644 --- a/logging/monolog_email.rst +++ b/logging/monolog_email.rst @@ -292,7 +292,7 @@ get logged on the server as well as the emails being sent: ->handler('grouped') ; - $monolog->handler('group') + $monolog->handler('grouped') ->type('group') ->members(['streamed', 'deduplicated']) ; @@ -322,7 +322,7 @@ get logged on the server as well as the emails being sent: ; }; -This uses the ``group`` handler to send the messages to the two +This uses the ``grouped`` handler to send the messages to the two group members, the ``deduplicated`` and the ``stream`` handlers. The messages will now be both written to the log file and emailed. diff --git a/logging/processors.rst b/logging/processors.rst index cbc039d992f..90320d0baeb 100644 --- a/logging/processors.rst +++ b/logging/processors.rst @@ -31,13 +31,13 @@ using a processor:: $this->requestStack = $requestStack; } - // this method is called for each log record; optimize it to not hurt performance + // method is called for each log record; optimize it to not hurt performance public function __invoke(array $record) { try { $session = $this->requestStack->getSession(); } catch (SessionNotFoundException $e) { - return; + return $record; } if (!$session->isStarted()) { return $record; @@ -270,8 +270,8 @@ the ``monolog.processor`` tag: Registering Processors per Channel ---------------------------------- -You can register a processor per channel using the ``channel`` option of -the ``monolog.processor`` tag: +By default, processors are applied to all channels. Add the ``channel`` option +to the ``monolog.processor`` tag to only apply a processor for the given channel: .. configuration-block:: @@ -281,7 +281,7 @@ the ``monolog.processor`` tag: services: App\Logger\SessionRequestProcessor: tags: - - { name: monolog.processor, channel: main } + - { name: monolog.processor, channel: 'app' } .. code-block:: xml @@ -297,7 +297,7 @@ the ``monolog.processor`` tag: - + @@ -309,7 +309,7 @@ the ``monolog.processor`` tag: // ... $container ->register(SessionRequestProcessor::class) - ->addTag('monolog.processor', ['channel' => 'main']); + ->addTag('monolog.processor', ['channel' => 'app']); .. _`Monolog`: https://github.com/Seldaek/monolog .. _`built-in Monolog processors`: https://github.com/Seldaek/monolog/tree/main/src/Monolog/Processor diff --git a/mailer.rst b/mailer.rst index 6fe753ae44c..17a8d97955b 100644 --- a/mailer.rst +++ b/mailer.rst @@ -55,20 +55,16 @@ over SMTP by configuring the DSN in your ``.env`` file (the ``user``, // config/packages/mailer.php use function Symfony\Component\DependencyInjection\Loader\Configurator\env; - use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; - - return static function (ContainerConfigurator $container): void { - $container->extension('framework', [ - 'mailer' => [ - 'dsn' => env('MAILER_DSN'), - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->mailer()->dsn(env('MAILER_DSN')); }; .. caution:: If the username, password or host contain any character considered special in a - URI (such as ``+``, ``@``, ``$``, ``#``, ``/``, ``:``, ``*``, ``!``), you must + 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. @@ -112,7 +108,7 @@ via a third-party provider: Service Install with ===================== ============================================== `Amazon SES`_ ``composer require symfony/amazon-mailer`` -`Mailchimp Mandrill`_ ``composer require symfony/mailchimp-mailer`` +`Mandrill`_ ``composer require symfony/mailchimp-mailer`` `Mailgun`_ ``composer require symfony/mailgun-mailer`` `Mailjet`_ ``composer require symfony/mailjet-mailer`` `OhMySMTP`_ ``composer require symfony/oh-my-smtp-mailer`` @@ -174,19 +170,19 @@ transport, but you can force to use one: This table shows the full list of available DSN formats for each third party provider: -===================== ==================================================== =========================================== ======================================== -Provider SMTP HTTP API -===================== ==================================================== =========================================== ======================================== -`Amazon SES`_ ses+smtp://USERNAME:PASSWORD@default ses+https://ACCESS_KEY:SECRET_KEY@default ses+api://ACCESS_KEY:SECRET_KEY@default -`Google Gmail`_ gmail+smtp://USERNAME:APP-PASSWORD@default n/a n/a -`Mailchimp Mandrill`_ mandrill+smtp://USERNAME:PASSWORD@default mandrill+https://KEY@default mandrill+api://KEY@default -`Mailgun`_ mailgun+smtp://USERNAME:PASSWORD@default mailgun+https://KEY:DOMAIN@default mailgun+api://KEY:DOMAIN@default -`Mailjet`_ mailjet+smtp://ACCESS_KEY:SECRET_KEY@default n/a mailjet+api://ACCESS_KEY:SECRET_KEY@default -`Postmark`_ postmark+smtp://ID@default n/a postmark+api://KEY@default -`Sendgrid`_ sendgrid+smtp://KEY@default n/a sendgrid+api://KEY@default -`Sendinblue`_ sendinblue+smtp://USERNAME:PASSWORD@default n/a sendinblue+api://KEY@default -`OhMySMTP`_ ohmysmtp+smtp://API_TOKEN@default n/a ohmysmtp+api://API_TOKEN@default -===================== ==================================================== =========================================== ======================================== +===================== ======================================================== =============================================== ============================================ +Provider SMTP HTTP API +===================== ======================================================== =============================================== ============================================ +`Amazon SES`_ ``ses+smtp://USERNAME:PASSWORD@default`` ``ses+https://ACCESS_KEY:SECRET_KEY@default`` ``ses+api://ACCESS_KEY:SECRET_KEY@default`` +`Google Gmail`_ ``gmail+smtp://USERNAME:APP-PASSWORD@default`` n/a n/a +`Mandrill`_ ``mandrill+smtp://USERNAME:PASSWORD@default`` ``mandrill+https://KEY@default`` ``mandrill+api://KEY@default`` +`Mailgun`_ ``mailgun+smtp://USERNAME:PASSWORD@default`` ``mailgun+https://KEY:DOMAIN@default`` ``mailgun+api://KEY:DOMAIN@default`` +`Mailjet`_ ``mailjet+smtp://ACCESS_KEY:SECRET_KEY@default`` n/a ``mailjet+api://ACCESS_KEY:SECRET_KEY@default`` +`Postmark`_ ``postmark+smtp://ID@default`` n/a ``postmark+api://KEY@default`` +`Sendgrid`_ ``sendgrid+smtp://KEY@default`` n/a ``sendgrid+api://KEY@default`` +`Sendinblue`_ ``sendinblue+smtp://USERNAME:PASSWORD@default`` n/a ``sendinblue+api://KEY@default`` +`OhMySMTP`_ ``ohmysmtp+smtp://API_TOKEN@default`` n/a ``ohmysmtp+api://API_TOKEN@default`` +===================== ======================================================== =============================================== ============================================ .. caution:: @@ -205,6 +201,12 @@ Provider SMTP HTTP The ``ping_threshold`` option for ``ses-smtp`` was introduced in Symfony 5.4. +.. caution:: + + If you send custom headers when using the `Amazon SES`_ transport (to receive + them later via a webhook), make sure to use the ``ses+https`` provider because + it's the only one that supports them. + .. note:: When using SMTP, the default timeout for sending a message before throwing an @@ -245,7 +247,7 @@ Provider SMTP HTTP .. note:: The specific transports, e.g. ``mailgun+smtp`` are designed to work without any manual configuration. - Changing the port by appending it to your DSN is not supported for any of these ``+smtp` transports. + Changing the port by appending it to your DSN is not supported for any of these ``+smtp`` transports. If you need to change the port, use the ``smtp`` transport instead, like so: .. code-block:: env @@ -321,7 +323,6 @@ Other Options This option was introduced in Symfony 5.2. - ``local_domain`` The domain name to use in ``HELO`` command:: @@ -360,6 +361,35 @@ Other Options This option was introduced in Symfony 5.2. +Custom Transport Factories +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to support your own custom DSN (``acme://...``), you can create a +custom transport factory. To do so, create a class that implements +:class:`Symfony\\Component\\Mailer\\Transport\\TransportFactoryInterface` or, if +you prefer, extend the :class:`Symfony\\Component\\Mailer\\Transport\\AbstractTransportFactory` +class to save some boilerplate code:: + + // src/Mailer/AcmeTransportFactory.php + final class AcmeTransportFactory extends AbstractTransportFactory + { + public function create(Dsn $dsn): TransportInterface + { + // parse the given DSN, extract data/credentials from it + // and then, create and return the transport + } + + protected function getSupportedSchemes(): array + { + // this supports DSN starting with `acme://` + return ['acme']; + } + } + +After creating the custom transport class, register it as a service in your +application and :doc:`tag it ` with the +``mailer.transport_factory`` tag. + Creating & Sending Messages --------------------------- @@ -398,9 +428,12 @@ and create an :class:`Symfony\\Component\\Mime\\Email` object:: } } -That's it! The message will be sent via the transport you configured. If the -transport is configured to :ref:`send emails asynchronously `, -the message won't be actually sent until :doc:`a worker consumes it `. +That's it! The message will be sent immediately via the transport you configured. +If you prefer to send emails asynchronously to improve performance, read the +:ref:`Sending Messages Async ` section. Also, if +your application has the :doc:`Messenger component ` installed, all +emails will be sent asynchronously by default +(but :ref:`you can change that `). Email Addresses ~~~~~~~~~~~~~~~ @@ -751,7 +784,7 @@ Then, create the template:

{{ email.to[0].address }}

- Click here to activate your account + Activate your account (this link is valid until {{ expiration_date|date('F jS') }})

@@ -972,7 +1005,7 @@ the entire email contents from Markdown to HTML: You signed up to our site using the following email: `{{ email.to[0].address }}` - [Click here to activate your account]({{ url('...') }}) + [Activate your account]({{ url('...') }}) {% endapply %} .. _mailer-inky: @@ -1317,7 +1350,6 @@ you have a transport called ``async``, you can route the message there: ->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`). @@ -1400,8 +1432,8 @@ If your transport does not support tags and metadata, they will be added as cust The following transports currently support tags and metadata: -* Mailchimp * Mailgun +* Mandrill * Postmark * Sendgrid * Sendinblue @@ -1594,6 +1626,13 @@ the :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\MailerAssertionsTrait`:: } } +.. 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. + .. _`Amazon SES`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Amazon/README.md .. _`App Password`: https://support.google.com/accounts/answer/185833 .. _`default_socket_timeout`: https://www.php.net/manual/en/filesystem.configuration.php#ini.default-socket-timeout @@ -1604,7 +1643,7 @@ the :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\MailerAssertionsTrait`:: .. _`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) -.. _`Mailchimp Mandrill`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailchimp/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/ diff --git a/mercure.rst b/mercure.rst index a627cdee92d..ec0a3dfd042 100644 --- a/mercure.rst +++ b/mercure.rst @@ -48,13 +48,28 @@ Run this command to install the Mercure support: $ 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. -Thanks to :ref:`the Docker integration of Symfony `, -:ref:`Flex ` proposes to install a Mercure hub. +.. 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 `, @@ -64,19 +79,7 @@ you must start it with the ``--no-tls`` option. $ symfony server:start --no-tls -d -Running a Mercure Hub -~~~~~~~~~~~~~~~~~~~~~ - -.. image:: /_images/mercure/schema.png - -If you use the Docker integration, a hub is already up and running, -and you can go straight to the next section. - -Otherwise, and in production, you have to install a 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. +If you use the Docker integration, a hub is already up and running. Configuration ------------- @@ -303,12 +306,17 @@ as patterns: } +.. tip:: + + Test if a URI Template matches a URL using `the online debugger`_ + .. tip:: Google Chrome DevTools natively integrate a `practical UI`_ displaying in live the received events: .. image:: /_images/mercure/chrome.png + :alt: The Chrome DevTools showing the EventStream tab containing information about each SSE event. To use it: @@ -317,10 +325,6 @@ as patterns: * click on the request to the Mercure hub * click on the "EventStream" sub-tab. -.. tip:: - - Test if a URI Template match a URL using `the online debugger`_ - Discovery --------- @@ -328,7 +332,11 @@ 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. -.. image:: /_images/mercure/discovery.png +.. raw:: html + + You can create ``Link`` headers with the ``Discovery`` helper class (under the hood, it uses the :doc:`WebLink Component `):: @@ -492,14 +500,12 @@ And here is the controller:: } } - .. 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 --------------------------------------------------- @@ -666,8 +672,9 @@ sent: .. code-block:: yaml # config/services_test.yaml - mercure.hub.default: - class: App\Tests\Functional\Stub\HubStub + services: + mercure.hub.default: + class: App\Tests\Functional\Stub\HubStub As MercureBundle support multiple hubs, you may have to replace the other service definitions accordingly. @@ -693,6 +700,8 @@ enable it:: $ 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 Async dispatching ----------------- diff --git a/messenger.rst b/messenger.rst index 6835e8dff11..b546082b100 100644 --- a/messenger.rst +++ b/messenger.rst @@ -124,7 +124,8 @@ is capable of sending messages (e.g. to a queueing system) and then .. note:: If you want to use a transport that's not supported, check out the - `Enqueue's transport`_, which supports things like Kafka and Google Pub/Sub. + `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. @@ -391,6 +392,8 @@ Then, in your handler, you can query for a fresh object:: This guarantees the entity contains fresh data. +.. _messenger-handling-messages-synchronously: + Handling Messages Synchronously ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -479,6 +482,13 @@ 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". +.. 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 @@ -522,7 +532,7 @@ On production, there are a few important things to think about: **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 ` + 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 @@ -554,7 +564,7 @@ different messages to them. For example: # name: high #queues: # messages_high: ~ - # or redis try "group" + # for redis try "group" async_priority_low: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' options: @@ -707,7 +717,7 @@ 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): +(see the :ref:`Redis section ` below): .. code-block:: ini @@ -723,6 +733,10 @@ Next, tell Supervisor to read your config and start your workers: $ 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 @@ -1445,38 +1459,16 @@ a table named ``messenger_messages``. The ability to automatically generate a migration for the ``messenger_messages`` table was introduced in Symfony 5.1 and DoctrineBundle 2.1. -Or, to create the table yourself, set the ``auto_setup`` option to ``false`` and -:ref:`generate a migration `. - -.. tip:: - - To avoid tools like Doctrine Migrations from trying to remove this table because - it's not part of your normal schema, you can set the ``schema_filter`` option: - - .. configuration-block:: - - .. code-block:: yaml +If you want to change the default table name, pass a custom table name in the +DSN by using the ``table_name`` option: - # config/packages/doctrine.yaml - doctrine: - dbal: - schema_filter: '~^(?!messenger_messages)~' - - .. code-block:: xml - - - +.. code-block:: env - .. code-block:: php + # .env + MESSENGER_TRANSPORT_DSN=doctrine://default?table_name=your_custom_table_name - # config/packages/doctrine.php - $container->loadFromExtension('doctrine', [ - 'dbal' => [ - 'schema_filter' => '~^(?!messenger_messages)~', - // ... - ], - // ... - ]); +Or, to create the table yourself, set the ``auto_setup`` option to ``false`` and +:ref:`generate a migration `. .. caution:: @@ -1602,6 +1594,8 @@ The Redis transport DSN may looks like this: 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 .. versionadded:: 5.1 @@ -1610,7 +1604,6 @@ The Redis transport DSN may looks like this: A number of options can be configured via the DSN or via the ``options`` key under the transport in ``messenger.yaml``: - =================== ===================================== ================================= Option Description Default =================== ===================================== ================================= @@ -1632,7 +1625,6 @@ stream_max_entries The maximum number of entries which ``0`` (which means " the stream will be trimmed to. Set it to a large enough number to avoid losing pending messages -tls Enable TLS support for the connection false redeliver_timeout Timeout before retrying a pending ``3600`` message which is owned by an abandoned consumer (if a worker died @@ -1671,6 +1663,14 @@ claim_interval Interval on which pending/abandoned ``60000`` (1 Minute) The ``delete_after_reject`` and ``lazy`` options were introduced in Symfony 5.2. +.. versionadded:: 5.3 + + The ``rediss://`` DSN scheme support for TLS protocol was introduced in Symfony 5.3. + +.. deprecated:: 5.3 + + The ``tls`` option was deprecated in Symfony 5.3, use ``rediss://`` DSN scheme for TLS support instead. + .. deprecated:: 5.4 Not setting a explicit value for the ``delete_after_ack`` option is @@ -1745,7 +1745,7 @@ during a request:: $this->assertSame(200, $client->getResponse()->getStatusCode()); - /* @var InMemoryTransport $transport */ + /** @var InMemoryTransport $transport */ $transport = $this->getContainer()->get('messenger.transport.async_priority_normal'); $this->assertCount(1, $transport->getSent()); } @@ -2220,7 +2220,7 @@ provided in order to ease the declaration of these special handlers:: { use BatchHandlerTrait; - public function __invoke(MyMessage $message, Acknowledger $ack = null) + public function __invoke(MyMessage $message, ?Acknowledger $ack = null) { return $this->handle($message, $ack); } @@ -2283,8 +2283,8 @@ to your message:: public function index(MessageBusInterface $bus) { + // wait 5 seconds before processing $bus->dispatch(new SmsNotification('...'), [ - // wait 5 seconds before processing new DelayStamp(5000), ]); @@ -2335,7 +2335,10 @@ 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: +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:: @@ -2349,13 +2352,14 @@ middleware and *only* include your own: # disable the default middleware default_middleware: false - # and/or add your own middleware: - # service ids that implement Symfony\Component\Messenger\Middleware\MiddlewareInterface + # 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 @@ -2371,11 +2375,17 @@ middleware and *only* include your own: - + + + + + messenger.bus.default + - - - + + + + @@ -2389,16 +2399,16 @@ middleware and *only* include your own: $messenger = $framework->messenger(); $bus = $messenger->bus('messenger.bus.default') - ->defaultMiddleware(false); + ->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'); }; -.. note:: - - If a middleware service is abstract, a different instance of the service will - be created per bus. - .. _middleware-doctrine: Middleware for Doctrine @@ -2433,6 +2443,9 @@ may want to use: # in any handler will cause a rollback - doctrine_transaction + # logs an error when a Doctrine transaction was opened but not closed + - doctrine_open_transaction_logger + # or pass a different entity manager to any #- doctrine_transaction: ['custom'] @@ -2454,6 +2467,7 @@ may want to use: + + + + + + + + + + + + + + .. 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); + }; + Multiple Buses, Command & Event Buses ------------------------------------- @@ -2585,7 +2695,7 @@ Learn more .. _`streams`: https://redis.io/topics/streams-intro .. _`Supervisor docs`: http://supervisord.org/ .. _`PCNTL`: https://www.php.net/manual/book.pcntl.php -.. _`systemd docs`: https://www.freedesktop.org/wiki/Software/systemd/ +.. _`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 diff --git a/messenger/custom-transport.rst b/messenger/custom-transport.rst index e496fcf6263..0ae6441564f 100644 --- a/messenger/custom-transport.rst +++ b/messenger/custom-transport.rst @@ -50,7 +50,7 @@ Here is a simplified example of a database transport:: /** * @param FakeDatabase $db is used for demo purposes. It is not a real class. */ - public function __construct(FakeDatabase $db, SerializerInterface $serializer = null) + public function __construct(FakeDatabase $db, ?SerializerInterface $serializer = null) { $this->db = $db; $this->serializer = $serializer ?? new PhpSerializer(); @@ -132,6 +132,11 @@ and :class:`Symfony\\Component\\Messenger\\Bridge\\Doctrine\\Transport\\Doctrine 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 diff --git a/notifier.rst b/notifier.rst index 659f8f245ff..6372662234d 100644 --- a/notifier.rst +++ b/notifier.rst @@ -50,7 +50,7 @@ SMS Channel .. caution:: If any of the DSN values contains any character considered special in a - URI (such as ``+``, ``@``, ``$``, ``#``, ``/``, ``:``, ``*``, ``!``), you must + 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. @@ -179,9 +179,7 @@ send SMS messages:: class SecurityController { - /** - * @Route("/login/success") - */ + #[Route('/login/success')] public function loginSuccess(TexterInterface $texter) { $sms = new SmsMessage( @@ -213,7 +211,7 @@ Chat Channel .. caution:: If any of the DSN values contains any character considered special in a - URI (such as ``+``, ``@``, ``$``, ``#``, ``/``, ``:``, ``*``, ``!``), you must + 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. @@ -417,7 +415,7 @@ Push Channel .. caution:: If any of the DSN values contains any character considered special in a - URI (such as ``+``, ``@``, ``$``, ``#``, ``/``, ``:``, ``*``, ``!``), you must + 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. @@ -780,7 +778,7 @@ and its ``asChatMessage()`` method:: $this->price = $price; } - public function asChatMessage(RecipientInterface $recipient, string $transport = null): ?ChatMessage + 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) { @@ -830,11 +828,11 @@ Using Events The ``MessageEvent``, ``FailedMessageEvent`` and ``SentMessageEvent`` were introduced in Symfony 5.4. -The :class:`Symfony\\Component\\Notifier\\Transport`` class of the Notifier component +The :class:`Symfony\\Component\\Notifier\\Transport` class of the Notifier component allows you to optionally hook into the lifecycle via events. -The ``MessageEvent::class`` Event -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +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 @@ -894,7 +892,7 @@ is dispatched. Listeners receive a $dispatcher->addListener(SentMessageEvent::class, function (SentMessageEvent $event) { // gets the message instance - $message = $event->getOriginalMessage(); + $message = $event->getMessage(); // log something $this->logger(sprintf('The message has been successfully sent and has id: %s', $message->getMessageId())); diff --git a/page_creation.rst b/page_creation.rst index a7d6e84c199..24735ffbc85 100644 --- a/page_creation.rst +++ b/page_creation.rst @@ -89,7 +89,7 @@ Annotation Routes Instead of defining your route in YAML, Symfony also allows you to use *annotation* or *attribute* routes. Attributes are built-in in PHP starting from PHP 8. In earlier -PHP versions you can use annotations. To do this, install the annotations package: +PHP versions you can use annotations, which require installing this package: .. code-block:: terminal @@ -104,13 +104,13 @@ You can now add your route directly *above* the controller: // src/Controller/LuckyController.php // ... - + use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Annotation\Route; class LuckyController { - + /** - + * @Route("/lucky/number") - + */ + /** + * @Route("/lucky/number") + */ public function number(): Response { // this looks exactly the same @@ -122,11 +122,11 @@ You can now add your route directly *above* the controller: // src/Controller/LuckyController.php // ... - + use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Annotation\Route; class LuckyController { - + #[Route('/lucky/number')] + #[Route('/lucky/number')] public function number(): Response { // this looks exactly the same diff --git a/performance.rst b/performance.rst index f855c7f4bd8..cf41c814eb6 100644 --- a/performance.rst +++ b/performance.rst @@ -91,7 +91,7 @@ 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`_. +used byte code cache is APC. .. _performance-use-preloading: @@ -224,7 +224,7 @@ 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 free features that you can use to find bottlenecks in your projects. +but provides a `full-featured demo`_. Profiling with Symfony Stopwatch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -347,11 +347,11 @@ Learn more .. _`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 -.. _`APC`: https://www.php.net/manual/en/book.apc.php .. _`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 index ed89cfe7a08..e894cb644d1 100644 --- a/profiler.rst +++ b/profiler.rst @@ -4,7 +4,7 @@ Profiler The profiler is a powerful **development tool** that gives detailed information about the execution of any request. -.. caution:: +.. danger:: **Never** enable the profiler in production environments as it will lead to major security vulnerabilities in your project. @@ -25,8 +25,8 @@ 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 - :align: center - :class: with-browser + :alt: The Symfony Web profiler page. + :class: with-browser .. note:: @@ -49,6 +49,12 @@ 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 @@ -110,6 +116,8 @@ need to create a custom data collector. Instead, use the built-in utilities to Consider using a professional profiler such as `Blackfire`_ to measure and analyze the execution of your application in detail. +.. _enabling_the_profiler_conditionally_tag: + Enabling the Profiler Conditionally ----------------------------------- @@ -193,19 +201,18 @@ production. To do that, create an :doc:`event subscriber ` and listen to the :ref:`kernel.response ` event:: - use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelInterface; // ... - + class MySubscriber implements EventSubscriberInterface { public function __construct(private KernelInterface $kernel) { } - + // ... public function onKernelResponse(ResponseEvent $event) @@ -256,7 +263,7 @@ request:: class RequestCollector extends AbstractDataCollector { - public function collect(Request $request, Response $response, \Throwable $exception = null) + public function collect(Request $request, Response $response, ?\Throwable $exception = null) { $this->data = [ 'method' => $request->getMethod(), diff --git a/quick_tour/the_big_picture.rst b/quick_tour/the_big_picture.rst index 7905a6fcbe0..3c3c4e41bf6 100644 --- a/quick_tour/the_big_picture.rst +++ b/quick_tour/the_big_picture.rst @@ -52,8 +52,8 @@ it as follows: Try your new app by going to ``http://localhost:8000`` in a browser! .. image:: /_images/quick_tour/no_routes_page.png - :align: center - :class: with-browser + :alt: The default Symfony welcome page. + :class: with-browser Fundamentals: Route, Controller, Response ----------------------------------------- diff --git a/rate_limiter.rst b/rate_limiter.rst index cbf960f5f9a..0d349e2f132 100644 --- a/rate_limiter.rst +++ b/rate_limiter.rst @@ -15,7 +15,7 @@ Symfony uses these rate limiters in built-in features like :ref:`login throttlin 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. -.. caution:: +.. 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`_. @@ -45,7 +45,9 @@ 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, @@ -66,7 +68,9 @@ 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. @@ -89,18 +93,20 @@ 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. +#. 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 @@ -344,7 +350,7 @@ the :class:`Symfony\\Component\\RateLimiter\\Reservation` object returned by the $limit = $limiter->consume(); $headers = [ 'X-RateLimit-Remaining' => $limit->getRemainingTokens(), - 'X-RateLimit-Retry-After' => $limit->getRetryAfter()->getTimestamp(), + 'X-RateLimit-Retry-After' => $limit->getRetryAfter()->getTimestamp() - time(), 'X-RateLimit-Limit' => $limit->getLimit(), ]; @@ -526,5 +532,5 @@ you can use a specific :ref:`named lock ` via the .. _`Apache mod_ratelimit`: https://httpd.apache.org/docs/current/mod/mod_ratelimit.html .. _`NGINX rate limiting`: https://www.nginx.com/blog/rate-limiting-nginx/ .. _`token bucket algorithm`: https://en.wikipedia.org/wiki/Token_bucket -.. _`PHP date relative formats`: https://www.php.net/datetime.formats.relative +.. _`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/reference/configuration/doctrine.rst b/reference/configuration/doctrine.rst index c49122575a0..5ee35e63288 100644 --- a/reference/configuration/doctrine.rst +++ b/reference/configuration/doctrine.rst @@ -57,7 +57,7 @@ The following block shows all possible configuration keys: charset: utf8mb4 logging: '%kernel.debug%' platform_service: App\DBAL\MyDatabasePlatformService - server_version: '5.7' + server_version: '8.0.37' mapping_types: enum: string types: @@ -91,7 +91,7 @@ The following block shows all possible configuration keys: charset="utf8mb4" logging="%kernel.debug%" platform-service="App\DBAL\MyDatabasePlatformService" - server-version="5.7"> + server-version="8.0.37"> bar string @@ -134,13 +134,13 @@ If you want to configure multiple connections in YAML, put them under the user: root password: null host: localhost - server_version: '5.6' + server_version: '8.0.37' customer: dbname: customer user: root password: null host: localhost - server_version: '5.7' + 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 @@ -158,7 +158,7 @@ you can access it using the ``getConnection()`` method and the name of the conne public function someMethod(ManagerRegistry $doctrine) { $connection = $doctrine->getConnection('customer'); - $result = $connection->fetchAll('SELECT name FROM customer'); + $result = $connection->fetchAllAssociative('SELECT name FROM customer'); // ... } @@ -269,9 +269,13 @@ you can control. The following configuration options exist for a mapping: ........ One of ``annotation`` (for PHP annotations; it's the default value), -``attribute`` (for PHP attributes), ``xml``, ``yml``, ``php`` or +``attribute`` (for PHP attributes), ``xml``, ``php`` or ``staticphp``. This specifies which type of metadata type your mapping uses. +.. 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`` diff --git a/reference/configuration/framework.rst b/reference/configuration/framework.rst index 52f9e78a823..4432563f597 100644 --- a/reference/configuration/framework.rst +++ b/reference/configuration/framework.rst @@ -219,8 +219,8 @@ following values: ``phpstorm``, ``sublime``, ``textmate``, ``macvim``, ``emacs`` .. note:: - The ``phpstorm`` option is supported natively by PhpStorm on MacOS, - Windows requires `PhpStormProtocol`_ and Linux requires `phpstorm-url-handler`_. + 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`` @@ -311,7 +311,10 @@ Another alternative is to set the ``xdebug.file_link_format`` option in your // example for PhpStorm xdebug.file_link_format="phpstorm://open?file=%f&line=%l" - // example for Sublime + // 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:: @@ -425,9 +428,11 @@ performance a bit: $framework->enabledLocales(['en', 'es']); }; -If some user makes requests with a locale not included in this option, the -application won't display any error because Symfony will display contents using -the fallback locale. +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 ................................ @@ -882,41 +887,6 @@ If you use for example as the type and name of an argument, autowiring will inject the ``my_api.client`` service into your autowired classes. -.. _reference-http-client-retry-failed: - -By enabling the optional ``retry_failed`` configuration, the HTTP client service -will automatically retry failed HTTP requests. - -.. 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 - -.. versionadded:: 5.2 - - The ``retry_failed`` option was introduced in Symfony 5.2. - auth_basic .......... @@ -1019,6 +989,8 @@ ciphers A list of the names of the ciphers allowed for the SSL/TLS connections. They can be separated by colons, commas or spaces (e.g. ``'RC4-SHA:TLS13-AES-128-GCM-SHA256'``). +.. _reference-http-client-retry-delay: + delay ..... @@ -1050,6 +1022,8 @@ headers 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 .......... @@ -1069,6 +1043,8 @@ http_version 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 ...... @@ -1100,6 +1076,8 @@ local_pk 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 ......... @@ -1138,6 +1116,8 @@ max_redirects The maximum number of redirects to follow. Use ``0`` to not follow any redirection. +.. _reference-http-client-retry-max-retries: + max_retries ........... @@ -1150,6 +1130,8 @@ max_retries 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 .......... @@ -1221,6 +1203,54 @@ 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`` + +.. versionadded:: 5.2 + + The ``retry_failed`` option was introduced in Symfony 5.2. + +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 .............. @@ -1599,7 +1629,8 @@ To see a list of all available storages, run: .. versionadded:: 5.3 - The ``storage_factory_id`` option was introduced in Symfony 5.3. + The ``storage_factory_id`` option was introduced in Symfony 5.3 as a replacement + of the ``storage_id`` option. .. _config-framework-session-handler-id: @@ -1608,48 +1639,119 @@ handler_id **type**: ``string`` **default**: ``session.handler.native_file`` -The service id used for session storage. The default value ``'session.handler.native_file'`` +The service id or DSN used for session storage. The default value ``'session.handler.native_file'`` will let Symfony manage the sessions itself using files to store the session metadata. Set it to ``null`` to use the native PHP session mechanism. -You can also :ref:`store sessions in a database `. +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`` .. _name: name .... -**type**: ``string`` **default**: ``null`` +**type**: ``string`` -This specifies the name of the session cookie. By default, it will use the -cookie name which is defined in the ``php.ini`` with the ``session.name`` -directive. +This specifies the name of the session cookie. + +If not set, ``php.ini``'s `session.name`_ directive will be relied on. cookie_lifetime ............... -**type**: ``integer`` **default**: ``null`` +**type**: ``integer`` -This determines the lifetime of the session - in seconds. The default value -- ``null`` - means that the ``session.cookie_lifetime`` value from ``php.ini`` -will be used. Setting this value to ``0`` means the cookie is valid for +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`` **default**: ``/`` +**type**: ``string`` + +This determines the path to set in the session cookie. -This determines the path to set in the session cookie. By default, it will -use ``/``. +If not set, ``php.ini``'s `session.cookie_path`_ directive will be relied on. cache_limiter ............. -**type**: ``string`` or ``int`` **default**: ``''`` +**type**: ``string`` **default**: ``0`` If set to ``0``, Symfony won't set any particular header related to the cache -and it will rely on the cache control method configured in the -`session.cache-limiter`_ PHP.ini option. +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 `: @@ -1686,19 +1788,22 @@ Unlike the other session options, ``cache_limiter`` is set as a regular '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`` **default**: ``''`` +**type**: ``string`` + +This determines the domain to set in the session cookie. -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. +If not set, ``php.ini``'s `session.cookie_domain`_ directive will be relied on. cookie_samesite ............... -**type**: ``string`` or ``null`` **default**: ``lax`` +**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 @@ -1712,8 +1817,7 @@ those cookies when making that HTTP request. The possible values for this option are: -* ``null``, use it to disable this protection. Same behavior as in older Symfony - versions. +* ``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'`` @@ -1727,18 +1831,20 @@ The possible values for this option are: .. note:: - This option is available starting from PHP 7.3, but Symfony has a polyfill - so you can use it with any older PHP version as well. + Same-site cookies are a PHP 7.3 feature, but Symfony has a polyfill + so you can set this option with any older PHP version as well. cookie_secure ............. -**type**: ``boolean`` or ``'auto'`` **default**: ``auto`` +**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. + cookie_httponly ............... @@ -1747,15 +1853,17 @@ cookie_httponly 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 XSS attacks. +identity theft through :ref:`XSS attacks `. gc_divisor .......... -**type**: ``integer`` **default**: ``100`` +**type**: ``integer`` See `gc_probability`_. +If not set, ``php.ini``'s `session.gc_divisor`_ directive will be relied on. + gc_probability .............. @@ -1769,45 +1877,46 @@ chance that the GC process will start on each request. gc_maxlifetime .............. -**type**: ``integer`` **default**: ``1440`` +**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. + sid_length .......... -**type**: ``integer`` **default**: ``32`` +**type**: ``integer`` This determines the length of session ID string, which can be an integer between -``22`` and ``256`` (both inclusive), being ``32`` the recommended value. Longer +``22`` and ``256`` (both inclusive), ``32`` being the recommended value. Longer session IDs are harder to guess. -This option is related to the `session.sid_length PHP option`_. +If not set, ``php.ini``'s `session.sid_length`_ directive will be relied on. sid_bits_per_character ...................... -**type**: ``integer`` **default**: ``4`` +**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. -This option is related to the `session.sid_bits_per_character PHP option`_. +If not set, ``php.ini``'s `session.sid_bits_per_character`_ directive will be relied on. save_path ......... -**type**: ``string`` **default**: ``%kernel.cache_dir%/sessions`` +**type**: ``string`` or ``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. -You can also set this value to the ``save_path`` of your ``php.ini`` by -setting the value to ``null``: +If ``null``, ``php.ini``'s `session.save_path`_ directive will be relied on: .. configuration-block:: @@ -1902,11 +2011,12 @@ Whether to enable the session support in the framework. use_cookies ........... -**type**: ``boolean`` **default**: ``null`` +**type**: ``boolean`` This specifies if the session ID is stored on the client side using cookies or -not. By default, it will use the value defined in the ``php.ini`` with the -``session.use_cookies`` directive. +not. + +If not set, ``php.ini``'s `session.use_cookies`_ directive will be relied on. ssi ~~~ @@ -2426,7 +2536,7 @@ translator cache_dir ......... -**type**: ``string`` | ``null`` **default**: ``%kernel.cache_dir%/translations/`` +**type**: ``string`` | ``null`` **default**: ``%kernel.cache_dir%/translations`` Defines the directory where the translation cache is stored. Use ``null`` to disable this cache. @@ -2780,7 +2890,6 @@ annotation changes). For performance reasons, it is recommended to disable debug mode in production, which will happen automatically if you use the default value. - secrets ~~~~~~~ @@ -2915,21 +3024,21 @@ This option also accepts a map of PHP errors to log levels: 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 + !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 @@ -3159,7 +3268,7 @@ settings from the base pool as defaults. .. note:: - Your service MUST implement the ``Psr\Cache\CacheItemPoolInterface`` interface. + Your service needs to implement the ``Psr\Cache\CacheItemPoolInterface`` interface. public """""" @@ -3691,7 +3800,6 @@ use the configuration of the first exception that matches ``instanceof``: .. _`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 .. _`Doctrine Cache`: https://www.doctrine-project.org/projects/doctrine-cache/en/current/index.html -.. _`PhpStormProtocol`: https://github.com/aik099/PhpStormProtocol .. _`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 @@ -3699,14 +3807,24 @@ use the configuration of the first exception that matches ``instanceof``: .. _`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 -.. _`session.sid_length PHP option`: https://www.php.net/manual/session.configuration.php#ini.session.sid-length -.. _`session.sid_bits_per_character PHP option`: https://www.php.net/manual/session.configuration.php#ini.session.sid-bits-per-character .. _`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.cache-limiter`: https://www.php.net/manual/en/session.configuration.php#ini.session.cache-limiter +.. _`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_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 diff --git a/reference/configuration/kernel.rst b/reference/configuration/kernel.rst index 0e31e423dd9..18554d2d467 100644 --- a/reference/configuration/kernel.rst +++ b/reference/configuration/kernel.rst @@ -47,7 +47,7 @@ charset:: Project Directory ~~~~~~~~~~~~~~~~~ -**type**: ``string`` **default**: the directory of the project ``composer.json`` +**type**: ``string`` **default**: the directory of the project's ``composer.json`` This returns the absolute path of the root directory of your Symfony project, which is used by applications to perform operations with file paths relative to @@ -75,6 +75,8 @@ method to return the right project directory:: public function getProjectDir(): string { + // 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__); } } diff --git a/reference/configuration/security.rst b/reference/configuration/security.rst index 22884fdbbe1..a859c2fd239 100644 --- a/reference/configuration/security.rst +++ b/reference/configuration/security.rst @@ -119,25 +119,21 @@ user logs out:: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ + + // ... + + return static function (SecurityConfig $securityConfig): void { // ... - 'firewalls' => [ - 'main' => [ - 'logout' => [ - 'delete_cookies' => [ - 'cookie1-name' => null, - 'cookie2-name' => [ - 'path' => '/', - ], - 'cookie3-name' => [ - 'path' => null, - 'domain' => 'example.com', - ], - ], - ], - ], - ], - ]); + + $securityConfig->firewall('main') + ->logout() + ->deleteCookie('cookie1-name') + ->deleteCookie('cookie2-name') + ->path('/') + ->deleteCookie('cookie3-name') + ->path(null) + ->domain('example.com'); + }; erase_credentials ~~~~~~~~~~~~~~~~~ @@ -307,7 +303,6 @@ the ``debug:firewall`` command: The ``debug:firewall`` command was introduced in Symfony 5.3. - .. _reference-security-firewall-form-login: ``form_login`` Authentication @@ -355,10 +350,10 @@ form_only **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``). -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 `. +(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 `. .. versionadded:: 5.4 diff --git a/reference/configuration/twig.rst b/reference/configuration/twig.rst index 1153846f35d..f0e8bd31454 100644 --- a/reference/configuration/twig.rst +++ b/reference/configuration/twig.rst @@ -41,12 +41,12 @@ autoescape If set to ``false``, automatic escaping is disabled (you can still escape each content individually in the templates). -.. caution:: +.. danger:: Setting this option to ``false`` is dangerous and it will make your - application vulnerable to `XSS attacks`_ because most third-party bundles - assume that auto-escaping is enabled and they don't escape contents - themselves. + application vulnerable to :ref:`XSS attacks ` because most + third-party bundles assume that auto-escaping is enabled and they don't + escape contents themselves. If set to a string, the template contents are escaped using the strategy with that name. Allowed values are ``html``, ``js``, ``css``, ``url``, ``html_attr`` @@ -345,4 +345,3 @@ 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 -.. _`XSS attacks`: https://en.wikipedia.org/wiki/Cross-site_scripting diff --git a/reference/constraints/Callback.rst b/reference/constraints/Callback.rst index e3f68c2b788..2dbfa7657a5 100644 --- a/reference/constraints/Callback.rst +++ b/reference/constraints/Callback.rst @@ -304,11 +304,13 @@ callback method: * A closure. Concrete callbacks receive an :class:`Symfony\\Component\\Validator\\Context\\ExecutionContextInterface` -instance as only argument. +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 -and the :class:`Symfony\\Component\\Validator\\Context\\ExecutionContextInterface` -instance 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 diff --git a/reference/constraints/Choice.rst b/reference/constraints/Choice.rst index ebf0efaaff7..de7b3052a2a 100644 --- a/reference/constraints/Choice.rst +++ b/reference/constraints/Choice.rst @@ -250,7 +250,7 @@ you can pass the class name and the method as an array. // src/Entity/Author.php namespace App\Entity; - use App\Entity\Genre + use App\Entity\Genre; use Symfony\Component\Validator\Constraints as Assert; class Author diff --git a/reference/constraints/Cidr.rst b/reference/constraints/Cidr.rst index bb51a4826be..b90f07ea1c8 100644 --- a/reference/constraints/Cidr.rst +++ b/reference/constraints/Cidr.rst @@ -112,7 +112,7 @@ It's a constraint for the lowest value a valid netmask may have. ``netmaskMax`` ~~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``32`` for IPv4 or ``128`` for IPv6 +**type**: ``integer`` **default**: ``32`` for IPv4 or ``128`` for IPv6 It's a constraint for the biggest value a valid netmask may have. diff --git a/reference/constraints/Country.rst b/reference/constraints/Country.rst index 60ed57b98d2..fbffb0e4d17 100644 --- a/reference/constraints/Country.rst +++ b/reference/constraints/Country.rst @@ -120,4 +120,3 @@ Parameter Description .. _`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/Email.rst b/reference/constraints/Email.rst index da3c263d99e..cf151610324 100644 --- a/reference/constraints/Email.rst +++ b/reference/constraints/Email.rst @@ -142,7 +142,6 @@ This option defines the pattern used to validate the email address. Valid values :class:`Symfony\\Component\\Validator\\Constraints\\Email` (e.g. ``Email::VALIDATION_MODE_STRICT``). - The default value used by this option is set in the :ref:`framework.validation.email_validation_mode ` configuration option. diff --git a/reference/constraints/EqualTo.rst b/reference/constraints/EqualTo.rst index 06578c27c19..444df708eb2 100644 --- a/reference/constraints/EqualTo.rst +++ b/reference/constraints/EqualTo.rst @@ -10,7 +10,6 @@ To force that a value is *not* equal, see :doc:`/reference/constraints/NotEqualT equal. Use :doc:`/reference/constraints/IdenticalTo` to compare with ``===``. - ========== =================================================================== Applies to :ref:`property or method ` Class :class:`Symfony\\Component\\Validator\\Constraints\\EqualTo` diff --git a/reference/constraints/File.rst b/reference/constraints/File.rst index ad36a42abb8..71fd2ac5932 100644 --- a/reference/constraints/File.rst +++ b/reference/constraints/File.rst @@ -40,7 +40,7 @@ type. The ``Author`` class might look as follows:: { protected $bioFile; - public function setBioFile(File $file = null) + public function setBioFile(?File $file = null) { $this->bioFile = $file; } diff --git a/reference/constraints/GreaterThan.rst b/reference/constraints/GreaterThan.rst index 22331edd957..02659140122 100644 --- a/reference/constraints/GreaterThan.rst +++ b/reference/constraints/GreaterThan.rst @@ -12,6 +12,8 @@ Class :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThan` Validator :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThanValidator` ========== =================================================================== +.. include:: /reference/constraints/_php7-string-and-number.rst.inc + Basic Usage ----------- diff --git a/reference/constraints/GreaterThanOrEqual.rst b/reference/constraints/GreaterThanOrEqual.rst index 79578c5a5f7..38621c1cb1c 100644 --- a/reference/constraints/GreaterThanOrEqual.rst +++ b/reference/constraints/GreaterThanOrEqual.rst @@ -11,6 +11,8 @@ Class :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThanOrEqu Validator :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThanOrEqualValidator` ========== =================================================================== +.. include:: /reference/constraints/_php7-string-and-number.rst.inc + Basic Usage ----------- diff --git a/reference/constraints/Image.rst b/reference/constraints/Image.rst index 917335f49cb..c2c838db847 100644 --- a/reference/constraints/Image.rst +++ b/reference/constraints/Image.rst @@ -35,7 +35,7 @@ would be a ``file`` type. The ``Author`` class might look as follows:: { protected $headshot; - public function setHeadshot(File $file = null) + public function setHeadshot(?File $file = null) { $this->headshot = $file; } diff --git a/reference/constraints/Length.rst b/reference/constraints/Length.rst index 3a197426975..44977ca0ea6 100644 --- a/reference/constraints/Length.rst +++ b/reference/constraints/Length.rst @@ -55,7 +55,6 @@ and ``50``, you might add the following: protected $firstName; } - .. code-block:: yaml # config/validator/validation.yaml diff --git a/reference/constraints/LessThan.rst b/reference/constraints/LessThan.rst index 22ebce5c094..d35050826f3 100644 --- a/reference/constraints/LessThan.rst +++ b/reference/constraints/LessThan.rst @@ -12,6 +12,8 @@ Class :class:`Symfony\\Component\\Validator\\Constraints\\LessThan` Validator :class:`Symfony\\Component\\Validator\\Constraints\\LessThanValidator` ========== =================================================================== +.. include:: /reference/constraints/_php7-string-and-number.rst.inc + Basic Usage ----------- diff --git a/reference/constraints/LessThanOrEqual.rst b/reference/constraints/LessThanOrEqual.rst index 05e2dedbfe5..dd32de0fe0f 100644 --- a/reference/constraints/LessThanOrEqual.rst +++ b/reference/constraints/LessThanOrEqual.rst @@ -11,6 +11,8 @@ Class :class:`Symfony\\Component\\Validator\\Constraints\\LessThanOrEqual` Validator :class:`Symfony\\Component\\Validator\\Constraints\\LessThanOrEqualValidator` ========== =================================================================== +.. include:: /reference/constraints/_php7-string-and-number.rst.inc + Basic Usage ----------- diff --git a/reference/constraints/Negative.rst b/reference/constraints/Negative.rst index c77d0586cbf..078a4cf9b90 100644 --- a/reference/constraints/Negative.rst +++ b/reference/constraints/Negative.rst @@ -8,9 +8,11 @@ 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\\LesserThanValidator` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\LessThanValidator` ========== =================================================================== +.. include:: /reference/constraints/_php7-string-and-number.rst.inc + Basic Usage ----------- diff --git a/reference/constraints/NegativeOrZero.rst b/reference/constraints/NegativeOrZero.rst index 0aead7184e3..593bd824c05 100644 --- a/reference/constraints/NegativeOrZero.rst +++ b/reference/constraints/NegativeOrZero.rst @@ -7,9 +7,11 @@ 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\\LesserThanOrEqualValidator` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\LessThanOrEqualValidator` ========== =================================================================== +.. include:: /reference/constraints/_php7-string-and-number.rst.inc + Basic Usage ----------- diff --git a/reference/constraints/Positive.rst b/reference/constraints/Positive.rst index b918c21695a..ca0d030cb64 100644 --- a/reference/constraints/Positive.rst +++ b/reference/constraints/Positive.rst @@ -11,6 +11,8 @@ Class :class:`Symfony\\Component\\Validator\\Constraints\\Positive` Validator :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThanValidator` ========== =================================================================== +.. include:: /reference/constraints/_php7-string-and-number.rst.inc + Basic Usage ----------- @@ -78,7 +80,6 @@ positive number (greater than zero): use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Mapping\ClassMetadata; - class Employee { public static function loadValidatorMetadata(ClassMetadata $metadata) diff --git a/reference/constraints/PositiveOrZero.rst b/reference/constraints/PositiveOrZero.rst index 2d39d0d6d19..808eafd071a 100644 --- a/reference/constraints/PositiveOrZero.rst +++ b/reference/constraints/PositiveOrZero.rst @@ -10,6 +10,8 @@ Class :class:`Symfony\\Component\\Validator\\Constraints\\PositiveOrZero` Validator :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThanOrEqualValidator` ========== =================================================================== +.. include:: /reference/constraints/_php7-string-and-number.rst.inc + Basic Usage ----------- diff --git a/reference/constraints/Regex.rst b/reference/constraints/Regex.rst index 6c7f34a5422..d57ded2bb77 100644 --- a/reference/constraints/Regex.rst +++ b/reference/constraints/Regex.rst @@ -193,7 +193,7 @@ Options ``htmlPattern`` ~~~~~~~~~~~~~~~ -**type**: ``string|boolean`` **default**: ``null`` +**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 @@ -289,7 +289,7 @@ need to specify the HTML5 compatible pattern in the ``htmlPattern`` option: } } -Setting ``htmlPattern`` to false will disable client side validation. +Setting ``htmlPattern`` to the empty string will disable client side validation. ``match`` ~~~~~~~~~ diff --git a/reference/constraints/Ulid.rst b/reference/constraints/Ulid.rst index 102e6486e41..be7b8355cd6 100644 --- a/reference/constraints/Ulid.rst +++ b/reference/constraints/Ulid.rst @@ -112,5 +112,4 @@ Parameter Description .. include:: /reference/constraints/_payload-option.rst.inc - .. _`Universally Unique Lexicographically Sortable Identifier (ULID)`: https://github.com/ulid/spec diff --git a/reference/constraints/UniqueEntity.rst b/reference/constraints/UniqueEntity.rst index f0bf8c6879a..5ae4b29e8ed 100644 --- a/reference/constraints/UniqueEntity.rst +++ b/reference/constraints/UniqueEntity.rst @@ -211,8 +211,8 @@ Consider this example: * @ORM\Entity * @UniqueEntity( * fields={"host", "port"}, - * errorPath="port", - * message="This port is already in use on that host." + * message="This port is already in use on that host.", + * errorPath="port" * ) */ class Service @@ -240,8 +240,8 @@ Consider this example: #[ORM\Entity] #[UniqueEntity( fields: ['host', 'port'], - errorPath: 'port', message: 'This port is already in use on that host.', + errorPath: 'port', )] class Service { @@ -259,8 +259,8 @@ Consider this example: constraints: - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: fields: [host, port] - errorPath: port message: 'This port is already in use on that host.' + errorPath: port .. code-block:: xml @@ -276,8 +276,8 @@ Consider this example: host port - + @@ -300,8 +300,8 @@ Consider this example: { $metadata->addConstraint(new UniqueEntity([ 'fields' => ['host', 'port'], - 'errorPath' => 'port', 'message' => 'This port is already in use on that host.', + 'errorPath' => 'port', ])); } } @@ -320,7 +320,7 @@ 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. .. include:: /reference/constraints/_groups-option.rst.inc diff --git a/reference/constraints/Valid.rst b/reference/constraints/Valid.rst index ba4e789091c..c308a8eac93 100644 --- a/reference/constraints/Valid.rst +++ b/reference/constraints/Valid.rst @@ -297,6 +297,13 @@ Options .. 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`` diff --git a/reference/constraints/_payload-option.rst.inc b/reference/constraints/_payload-option.rst.inc index 5121ba1ae51..a76c9a4a29d 100644 --- a/reference/constraints/_payload-option.rst.inc +++ b/reference/constraints/_payload-option.rst.inc @@ -1,3 +1,5 @@ +.. _reference-constraints-payload: + ``payload`` ~~~~~~~~~~~ diff --git a/reference/constraints/_php7-string-and-number.rst.inc b/reference/constraints/_php7-string-and-number.rst.inc new file mode 100644 index 00000000000..3d19f2eb0d3 --- /dev/null +++ b/reference/constraints/_php7-string-and-number.rst.inc @@ -0,0 +1,6 @@ +.. caution:: + + When using PHP 7.x, if the value is a string (e.g. ``1234asd``), the validator + will not trigger an error. In this case, you must also use the + :doc:`Type constraint ` with + ``numeric``, ``integer``, etc. to reject strings. diff --git a/reference/constraints/map.rst.inc b/reference/constraints/map.rst.inc index 1d56346b096..6c4d7f10936 100644 --- a/reference/constraints/map.rst.inc +++ b/reference/constraints/map.rst.inc @@ -4,53 +4,60 @@ 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:`NotNull ` +* :doc:`IsFalse ` * :doc:`IsNull ` * :doc:`IsTrue ` -* :doc:`IsFalse ` +* :doc:`NotBlank ` +* :doc:`NotNull ` * :doc:`Type ` String Constraints ~~~~~~~~~~~~~~~~~~ +.. class:: ui-list-three-columns + +* :doc:`Cidr ` +* :doc:`CssColor ` * :doc:`Email ` * :doc:`ExpressionLanguageSyntax ` -* :doc:`Length ` -* :doc:`Url ` -* :doc:`Regex ` * :doc:`Hostname ` * :doc:`Ip ` -* :doc:`Cidr ` * :doc:`Json ` -* :doc:`Uuid ` +* :doc:`Length ` +* :doc:`NotCompromisedPassword ` +* :doc:`Regex ` * :doc:`Ulid ` +* :doc:`Url ` * :doc:`UserPassword ` -* :doc:`NotCompromisedPassword ` -* :doc:`CssColor ` +* :doc:`Uuid ` Comparison Constraints ~~~~~~~~~~~~~~~~~~~~~~ +.. class:: ui-list-three-columns + +* :doc:`DivisibleBy ` * :doc:`EqualTo ` -* :doc:`NotEqualTo ` +* :doc:`GreaterThan ` +* :doc:`GreaterThanOrEqual ` * :doc:`IdenticalTo ` -* :doc:`NotIdenticalTo ` * :doc:`LessThan ` * :doc:`LessThanOrEqual ` -* :doc:`GreaterThan ` -* :doc:`GreaterThanOrEqual ` +* :doc:`NotEqualTo ` +* :doc:`NotIdenticalTo ` * :doc:`Range ` -* :doc:`DivisibleBy ` * :doc:`Unique ` Number Constraints ~~~~~~~~~~~~~~~~~~ -* :doc:`Positive ` -* :doc:`PositiveOrZero ` + * :doc:`Negative ` * :doc:`NegativeOrZero ` +* :doc:`Positive ` +* :doc:`PositiveOrZero ` Date Constraints ~~~~~~~~~~~~~~~~ @@ -64,9 +71,9 @@ Choice Constraints ~~~~~~~~~~~~~~~~~~ * :doc:`Choice ` +* :doc:`Country ` * :doc:`Language ` * :doc:`Locale ` -* :doc:`Country ` File Constraints ~~~~~~~~~~~~~~~~ @@ -77,29 +84,38 @@ File Constraints Financial and other Number Constraints ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. class:: ui-list-two-columns + * :doc:`Bic ` * :doc:`CardScheme ` * :doc:`Currency ` -* :doc:`Luhn ` * :doc:`Iban ` * :doc:`Isbn ` -* :doc:`Issn ` * :doc:`Isin ` +* :doc:`Issn ` +* :doc:`Luhn ` + +Doctrine Constraints +~~~~~~~~~~~~~~~~~~~~ + +* :doc:`DisableAutoMapping ` +* :doc:`EnableAutoMapping ` +* :doc:`UniqueEntity ` Other Constraints ~~~~~~~~~~~~~~~~~ +.. class:: ui-list-three-columns + +* :doc:`All ` * :doc:`AtLeastOneOf ` -* :doc:`Sequentially ` -* :doc:`Compound ` * :doc:`Callback ` -* :doc:`Expression ` -* :doc:`All ` -* :doc:`Valid ` * :doc:`Cascade ` -* :doc:`Traverse ` * :doc:`Collection ` +* :doc:`Compound ` * :doc:`Count ` -* :doc:`UniqueEntity ` -* :doc:`EnableAutoMapping ` -* :doc:`DisableAutoMapping ` +* :doc:`Expression ` +* :doc:`GroupSequence ` +* :doc:`Sequentially ` +* :doc:`Traverse ` +* :doc:`Valid ` diff --git a/reference/dic_tags.rst b/reference/dic_tags.rst index 5b035d6f89b..16480b3fb3c 100644 --- a/reference/dic_tags.rst +++ b/reference/dic_tags.rst @@ -1228,6 +1228,49 @@ This is the name that's used to determine which dumper should be used. $container->register(JsonFileDumper::class) ->addTag('translation.dumper', ['alias' => 'json']); +.. _reference-dic-tags-translation-provider-factory: + +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: twig.extension @@ -1296,8 +1339,7 @@ twig.loader **Purpose**: Register a custom service that loads Twig templates -By default, Symfony uses only one `Twig Loader`_ - -:class:`Symfony\\Bundle\\TwigBundle\\Loader\\FilesystemLoader`. If you need +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``. @@ -1414,6 +1456,7 @@ Then, tag it with the ``validator.initializer`` tag (it has no options). For an example, see the ``DoctrineInitializer`` class inside the Doctrine Bridge. +.. _`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/formats/expression_language.rst b/reference/formats/expression_language.rst index cf51ac7c7ff..82c30d2ec49 100644 --- a/reference/formats/expression_language.rst +++ b/reference/formats/expression_language.rst @@ -1,9 +1,9 @@ The Expression Syntax ===================== -The ExpressionLanguage component uses a specific syntax which is based on the -expression syntax of Twig. In this document, you can find all supported -syntaxes. +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 ------------------ @@ -20,8 +20,8 @@ The component supports: .. caution:: - A backslash (``\``) must be escaped by 4 backslashes (``\\\\``) in a string - and 8 backslashes (``\\\\\\\\``) in a regex:: + 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 diff --git a/reference/formats/message_format.rst b/reference/formats/message_format.rst index 99c02f0f5b2..2a694ed45d2 100644 --- a/reference/formats/message_format.rst +++ b/reference/formats/message_format.rst @@ -3,7 +3,8 @@ 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 Translator component supports the `ICU MessageFormat`_ syntax. +handle this, the :doc:`Translator component ` supports the +`ICU MessageFormat`_ syntax. .. tip:: @@ -63,7 +64,6 @@ The basic usage of the MessageFormat allows you to use placeholders (called 'say_hello' => "Hello {name}!", ]; - .. caution:: In the previous translation format, placeholders were often wrapped in ``%`` diff --git a/reference/formats/yaml.rst b/reference/formats/yaml.rst index 8127bf43729..cd55ab6dd9b 100644 --- a/reference/formats/yaml.rst +++ b/reference/formats/yaml.rst @@ -1,8 +1,8 @@ The YAML Format --------------- -The Symfony Yaml Component implements a selected subset of features defined in -the `YAML 1.2 version specification`_. +The Symfony :doc:`Yaml Component ` implements a selected subset +of features defined in the `YAML 1.2 version specification`_. Scalars ~~~~~~~ @@ -34,12 +34,10 @@ must be doubled to escape it: '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 ``\``: - -* ``:``, ``{``, ``}``, ``[``, ``]``, ``,``, ``&``, ``*``, ``#``, ``?``, ``|``, - ``-``, ``<``, ``>``, ``=``, ``!``, ``%``, ``@``, ````` +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 @@ -52,11 +50,11 @@ when you need to embed a ``\n`` or a Unicode character in a string. 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`` +``\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: diff --git a/reference/forms/types/checkbox.rst b/reference/forms/types/checkbox.rst index 2e699464eee..472d6f84024 100644 --- a/reference/forms/types/checkbox.rst +++ b/reference/forms/types/checkbox.rst @@ -81,6 +81,8 @@ These options inherit from the :doc:`FormType `: .. 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 diff --git a/reference/forms/types/choice.rst b/reference/forms/types/choice.rst index beb3f27d08f..d349fc76103 100644 --- a/reference/forms/types/choice.rst +++ b/reference/forms/types/choice.rst @@ -41,7 +41,7 @@ end users and the array values are the internal values used in the form field:: This will create a ``select`` drop-down like this: .. image:: /_images/reference/form/choice-example1.png - :align: center + :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. @@ -137,7 +137,7 @@ by passing a multi-dimensional ``choices`` array:: ]); .. image:: /_images/reference/form/choice-example4.png - :align: center + :alt: A choice list with the options "Yes" and "No" grouped under "Main Statuses" and the options "Backordered" and "Discontinued" under "Out of Stock Statuses". To get fancier, use the `group_by`_ option instead. @@ -263,6 +263,8 @@ These options inherit from the :doc:`FormType `: .. 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 diff --git a/reference/forms/types/collection.rst b/reference/forms/types/collection.rst index c4aa8be8a7f..801dcfd0b5b 100644 --- a/reference/forms/types/collection.rst +++ b/reference/forms/types/collection.rst @@ -160,7 +160,7 @@ the value is removed from the collection. For example:: $builder->add('users', CollectionType::class, [ // ... - 'delete_empty' => function (User $user = null) { + 'delete_empty' => function (?User $user = null) { return null === $user || empty($user->getFirstName()); }, ]); @@ -310,6 +310,8 @@ error_bubbling .. 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 diff --git a/reference/forms/types/color.rst b/reference/forms/types/color.rst index 62811d0386d..82e36417552 100644 --- a/reference/forms/types/color.rst +++ b/reference/forms/types/color.rst @@ -77,6 +77,8 @@ The default value is ``''`` (the empty string). .. 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 diff --git a/reference/forms/types/country.rst b/reference/forms/types/country.rst index 3cd748c74c8..e5c14c547d7 100644 --- a/reference/forms/types/country.rst +++ b/reference/forms/types/country.rst @@ -110,6 +110,8 @@ The actual default value of this option depends on other field options: .. 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 diff --git a/reference/forms/types/currency.rst b/reference/forms/types/currency.rst index 7417ac636c2..c68ce61215c 100644 --- a/reference/forms/types/currency.rst +++ b/reference/forms/types/currency.rst @@ -91,6 +91,8 @@ The actual default value of this option depends on other field options: .. 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 diff --git a/reference/forms/types/email.rst b/reference/forms/types/email.rst index 90752042409..9045bba8cc4 100644 --- a/reference/forms/types/email.rst +++ b/reference/forms/types/email.rst @@ -54,6 +54,8 @@ The default value is ``''`` (the empty string). .. 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 diff --git a/reference/forms/types/entity.rst b/reference/forms/types/entity.rst index 884ab26a0d0..4d8c7f2b5ee 100644 --- a/reference/forms/types/entity.rst +++ b/reference/forms/types/entity.rst @@ -49,7 +49,8 @@ Using a Custom Query for the Entities If you want to create a custom query to use when fetching the entities (e.g. you only want to return some entities, or need to order them), use -the `query_builder`_ option:: +the `query_builder`_ option (which must be a ``QueryBuilder`` object, a closure +returning a ``QueryBuilder`` object or ``null`` to load all entities):: use App\Entity\User; use Doctrine\ORM\EntityRepository; @@ -173,7 +174,7 @@ instead of the ``default`` entity manager. **type**: ``Doctrine\ORM\QueryBuilder`` or a ``callable`` **default**: ``null`` Allows you to create a custom query for your choices. See -:ref:`ref-form-entity-query-builder` for an example. +:ref:`how to use it ` for an example. The value of this option can either be a ``QueryBuilder`` object, a callable or ``null`` (which will load all entities). When using a callable, you will be @@ -210,7 +211,7 @@ submitted. Instead of allowing the `class`_ and `query_builder`_ options to fetch the entities to include for you, you can pass the ``choices`` option directly. -See :ref:`reference-forms-entity-choices`. +See :ref:`how to use choices `. ``data_class`` ~~~~~~~~~~~~~~ @@ -320,6 +321,8 @@ The actual default value of this option depends on other field options: .. 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 diff --git a/reference/forms/types/enum.rst b/reference/forms/types/enum.rst index 7a01f41f27b..43ca7833a38 100644 --- a/reference/forms/types/enum.rst +++ b/reference/forms/types/enum.rst @@ -172,6 +172,8 @@ These options inherit from the :doc:`FormType `: .. 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 diff --git a/reference/forms/types/file.rst b/reference/forms/types/file.rst index 29601e860f8..e306b1b120d 100644 --- a/reference/forms/types/file.rst +++ b/reference/forms/types/file.rst @@ -129,6 +129,8 @@ These options inherit from the :doc:`FormType `: .. 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 diff --git a/reference/forms/types/integer.rst b/reference/forms/types/integer.rst index b88211d9ae5..8ea50e3158d 100644 --- a/reference/forms/types/integer.rst +++ b/reference/forms/types/integer.rst @@ -108,6 +108,8 @@ The default value is ``''`` (the empty string). .. 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 diff --git a/reference/forms/types/language.rst b/reference/forms/types/language.rst index 2ede5f38e9f..0fd0a63ecd2 100644 --- a/reference/forms/types/language.rst +++ b/reference/forms/types/language.rst @@ -131,6 +131,8 @@ The actual default value of this option depends on other field options: .. 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 diff --git a/reference/forms/types/locale.rst b/reference/forms/types/locale.rst index eb8075093ed..c869155bdc2 100644 --- a/reference/forms/types/locale.rst +++ b/reference/forms/types/locale.rst @@ -104,6 +104,8 @@ The actual default value of this option depends on other field options: .. 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 diff --git a/reference/forms/types/money.rst b/reference/forms/types/money.rst index 99631f3e1e4..23955947e40 100644 --- a/reference/forms/types/money.rst +++ b/reference/forms/types/money.rst @@ -130,6 +130,8 @@ The default value is ``''`` (the empty string). .. 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 diff --git a/reference/forms/types/number.rst b/reference/forms/types/number.rst index 81b71bfe91b..ea0aa5de5a2 100644 --- a/reference/forms/types/number.rst +++ b/reference/forms/types/number.rst @@ -95,6 +95,8 @@ The default value is ``''`` (the empty string). .. 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 diff --git a/reference/forms/types/options/button_label.rst.inc b/reference/forms/types/options/button_label.rst.inc index 623e8bf6200..c63d48b032c 100644 --- a/reference/forms/types/options/button_label.rst.inc +++ b/reference/forms/types/options/button_label.rst.inc @@ -1,7 +1,7 @@ ``label`` ~~~~~~~~~ -**type**: ``string`` **default**: The label is "guessed" from the field name +**type**: ``string`` or ``TranslatableMessage`` **default**: The label is "guessed" from the field name Sets the label that will be displayed on the button. The label can also be directly set inside the template: diff --git a/reference/forms/types/options/choice_label.rst.inc b/reference/forms/types/options/choice_label.rst.inc index 0acb25f67b9..d2b263422ee 100644 --- a/reference/forms/types/options/choice_label.rst.inc +++ b/reference/forms/types/options/choice_label.rst.inc @@ -39,7 +39,7 @@ This method is called for *each* choice, passing you the ``$choice`` and This will give you: .. image:: /_images/reference/form/choice-example2.png - :align: center + :alt: A choice list with the options "Definitely!", "NO" and "MAYBE". If your choice values are objects, then ``choice_label`` can also be a :ref:`property path `. Imagine you have some diff --git a/reference/forms/types/options/choice_loader.rst.inc b/reference/forms/types/options/choice_loader.rst.inc index 67062c56ada..a906007c324 100644 --- a/reference/forms/types/options/choice_loader.rst.inc +++ b/reference/forms/types/options/choice_loader.rst.inc @@ -26,6 +26,21 @@ This will cause the call of ``StaticClass::getConstants()`` to not happen if the request is redirected and if there is no pre set or submitted data. Otherwise the choice options would need to be resolved thus triggering the callback. +If the built-in ``CallbackChoiceLoader`` doesn't fit your needs, you can create +your own loader by implementing the +:class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\ChoiceLoaderInterface` +or by extending the +:class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\AbstractChoiceLoader`. +This abstract class saves you some boilerplate by implementing some methods of +the interface so you'll only have to implement the +:method:`Symfony\\Component\\Form\\ChoiceList\\Loader\\AbstractChoiceLoader::loadChoices` +method to have a fully functional choice loader. + +.. versionadded:: 5.1 + + The :class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\AbstractChoiceLoader` + class was introduced in Symfony 5.1. + When you're defining a custom choice type that may be reused in many fields (like entries of a collection) or reused in multiple forms at once, you should use the :class:`Symfony\\Component\\Form\\ChoiceList\\ChoiceList` diff --git a/reference/forms/types/options/constraints.rst.inc b/reference/forms/types/options/constraints.rst.inc index 7aab319f302..3e1af29f3ab 100644 --- a/reference/forms/types/options/constraints.rst.inc +++ b/reference/forms/types/options/constraints.rst.inc @@ -1,7 +1,7 @@ ``constraints`` ~~~~~~~~~~~~~~~ -**type**: ``array`` or :class:`Symfony\\Component\\Validator\\Constraint` **default**: ``null`` +**type**: ``array`` or :class:`Symfony\\Component\\Validator\\Constraint` **default**: ``[]`` Allows you to attach one or more validation constraints to a specific field. For more information, see :ref:`Adding Validation `. diff --git a/reference/forms/types/options/empty_data_description.rst.inc b/reference/forms/types/options/empty_data_description.rst.inc index 90e111fb202..e654a7037df 100644 --- a/reference/forms/types/options/empty_data_description.rst.inc +++ b/reference/forms/types/options/empty_data_description.rst.inc @@ -15,14 +15,12 @@ This will still render an empty text box, but upon submission the ``John Doe`` value will be set. Use the ``data`` or ``placeholder`` options to show this initial value in the rendered form. -If a form is compound, you can set ``empty_data`` as an array, object or -closure. See the :doc:`/form/use_empty_data` article for more details about -these options. - .. note:: - If you want to set the ``empty_data`` option for your entire form class, - see the :doc:`/form/use_empty_data` article. + If a form is compound, you can set ``empty_data`` as an array, object or + closure. This option can be set for your entire form class, see the + :doc:`/form/use_empty_data` article for more details about these + options. .. caution:: diff --git a/reference/forms/types/options/group_by.rst.inc b/reference/forms/types/options/group_by.rst.inc index ca747683662..161be9140ee 100644 --- a/reference/forms/types/options/group_by.rst.inc +++ b/reference/forms/types/options/group_by.rst.inc @@ -35,7 +35,7 @@ This groups the dates that are within 3 days into "Soon" and everything else int a "Later" ````: .. image:: /_images/reference/form/choice-example5.png - :align: center + :alt: A choice list with "now" and "tomorrow" grouped under "Soon", and "1 week" and "1 month" grouped under "Later". If you return ``null``, the option won't be grouped. You can also pass a string "property path" that will be called to get the group. See the `choice_label`_ for diff --git a/reference/forms/types/options/placeholder.rst.inc b/reference/forms/types/options/placeholder.rst.inc index 5920cefbb52..e36b4bce546 100644 --- a/reference/forms/types/options/placeholder.rst.inc +++ b/reference/forms/types/options/placeholder.rst.inc @@ -1,7 +1,7 @@ ``placeholder`` ~~~~~~~~~~~~~~~ -**type**: ``string`` or ``boolean`` +**type**: ``string`` or ``TranslatableMessage`` or ``boolean`` This option determines whether or not a special "empty" option (e.g. "Choose an option") will appear at the top of a select widget. This option only @@ -14,6 +14,9 @@ applies if the ``multiple`` option is set to false. $builder->add('states', ChoiceType::class, [ 'placeholder' => 'Choose an option', + + // or if you want to translate the text + 'placeholder' => new TranslatableMessage('form.placeholder.select_option', [], 'form'), ]); * Guarantee that no "empty" value option is displayed:: diff --git a/reference/forms/types/options/preferred_choices.rst.inc b/reference/forms/types/options/preferred_choices.rst.inc index 8cb1278136d..0a4457d0313 100644 --- a/reference/forms/types/options/preferred_choices.rst.inc +++ b/reference/forms/types/options/preferred_choices.rst.inc @@ -42,7 +42,7 @@ be especially useful if your values are objects:: This will "prefer" the "now" and "tomorrow" choices only: .. image:: /_images/reference/form/choice-example3.png - :align: center + :alt: A choice list with "now" and "tomorrow" on top, separated by a line from "1 week" and "1 month". Finally, if your values are objects, you can also specify a property path string on the object that will return true or false. diff --git a/reference/forms/types/password.rst b/reference/forms/types/password.rst index e3e11ecb02f..3e92c48aeda 100644 --- a/reference/forms/types/password.rst +++ b/reference/forms/types/password.rst @@ -76,6 +76,8 @@ The default value is ``''`` (the empty string). .. 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 diff --git a/reference/forms/types/percent.rst b/reference/forms/types/percent.rst index 803badd2971..f3bc56356cb 100644 --- a/reference/forms/types/percent.rst +++ b/reference/forms/types/percent.rst @@ -124,6 +124,8 @@ The default value is ``''`` (the empty string). .. 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 diff --git a/reference/forms/types/radio.rst b/reference/forms/types/radio.rst index 72acd123af3..7702b87cbad 100644 --- a/reference/forms/types/radio.rst +++ b/reference/forms/types/radio.rst @@ -60,6 +60,8 @@ These options inherit from the :doc:`FormType `: .. 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 diff --git a/reference/forms/types/range.rst b/reference/forms/types/range.rst index 95e4bcff64c..294023ce0c6 100644 --- a/reference/forms/types/range.rst +++ b/reference/forms/types/range.rst @@ -69,6 +69,8 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/mapped.rst.inc .. include:: /reference/forms/types/options/required.rst.inc diff --git a/reference/forms/types/search.rst b/reference/forms/types/search.rst index d99b8fefc0a..32db9b3eccb 100644 --- a/reference/forms/types/search.rst +++ b/reference/forms/types/search.rst @@ -4,8 +4,6 @@ SearchType Field This renders an ```` field, which is a text box with special functionality supported by some browsers. -Read about the input search field at `DiveIntoHTML5.info`_ - +---------------------------+----------------------------------------------------------------------+ | Rendered as | ``input search`` field | +---------------------------+----------------------------------------------------------------------+ @@ -54,6 +52,8 @@ The default value is ``''`` (the empty string). .. 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 @@ -63,5 +63,3 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/row_attr.rst.inc .. include:: /reference/forms/types/options/trim.rst.inc - -.. _`DiveIntoHTML5.info`: http://diveintohtml5.info/forms.html#type-search diff --git a/reference/forms/types/tel.rst b/reference/forms/types/tel.rst index cca1c52a4be..675f8e3f5cd 100644 --- a/reference/forms/types/tel.rst +++ b/reference/forms/types/tel.rst @@ -60,6 +60,8 @@ The default value is ``''`` (the empty string). .. 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 diff --git a/reference/forms/types/text.rst b/reference/forms/types/text.rst index dd690c6e6df..0527c5405e7 100644 --- a/reference/forms/types/text.rst +++ b/reference/forms/types/text.rst @@ -46,6 +46,8 @@ an empty string, explicitly set the ``empty_data`` option to an empty string. .. 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 diff --git a/reference/forms/types/textarea.rst b/reference/forms/types/textarea.rst index e642cbdb4db..687d1bb225b 100644 --- a/reference/forms/types/textarea.rst +++ b/reference/forms/types/textarea.rst @@ -52,6 +52,8 @@ an empty string, explicitly set the ``empty_data`` option to an empty string. .. 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 diff --git a/reference/forms/types/time.rst b/reference/forms/types/time.rst index b45b0eab561..2ce41c6ff74 100644 --- a/reference/forms/types/time.rst +++ b/reference/forms/types/time.rst @@ -69,14 +69,14 @@ 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('startTime', 'time', [ + $builder->add('startTime', TimeType::class, [ 'placeholder' => 'Select a value', ]); Alternatively, you can use an array that configures different placeholder values for the hour, minute and second fields:: - $builder->add('startTime', 'time', [ + $builder->add('startTime', TimeType::class, [ 'placeholder' => [ 'hour' => 'Hour', 'minute' => 'Minute', 'second' => 'Second', ], diff --git a/reference/forms/types/timezone.rst b/reference/forms/types/timezone.rst index 7e0d5b8beb0..f11ee72f4d5 100644 --- a/reference/forms/types/timezone.rst +++ b/reference/forms/types/timezone.rst @@ -125,6 +125,8 @@ The actual default value of this option depends on other field options: .. 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 diff --git a/reference/forms/types/ulid.rst b/reference/forms/types/ulid.rst index 9ad8e7a6fee..33ab0ed6f9f 100644 --- a/reference/forms/types/ulid.rst +++ b/reference/forms/types/ulid.rst @@ -62,6 +62,8 @@ The default value is ``''`` (the empty string). .. 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 diff --git a/reference/forms/types/url.rst b/reference/forms/types/url.rst index b75a2b1db0c..5f97fcb89a4 100644 --- a/reference/forms/types/url.rst +++ b/reference/forms/types/url.rst @@ -67,6 +67,8 @@ The default value is ``''`` (the empty string). .. 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 diff --git a/reference/forms/types/uuid.rst b/reference/forms/types/uuid.rst index 6c0cd576cae..0b95d9db50d 100644 --- a/reference/forms/types/uuid.rst +++ b/reference/forms/types/uuid.rst @@ -62,6 +62,8 @@ The default value is ``''`` (the empty string). .. 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 diff --git a/reference/twig_reference.rst b/reference/twig_reference.rst index 5d13a79abaf..1c20264352c 100644 --- a/reference/twig_reference.rst +++ b/reference/twig_reference.rst @@ -120,8 +120,10 @@ the application is installed (e.g. in case the project is accessed in a host subdirectory) and the optional asset package base path. Symfony provides various cache busting implementations via the -:ref:`reference-framework-assets-version`, :ref:`reference-assets-version-strategy`, -and :ref:`reference-assets-json-manifest-path` configuration options. +:ref:`assets.version `, +:ref:`assets.version_strategy `, +and :ref:`assets.json_manifest_path ` +configuration options. .. seealso:: @@ -389,9 +391,18 @@ humanize ``text`` **type**: ``string`` -Makes a technical name human readable (i.e. replaces underscores by spaces -or transforms camelCase text like ``helloWorld`` to ``hello world`` -and then capitalizes the string). +Transforms the given string into a human readable string (by replacing underscores +with spaces, capitalizing the string, etc.) It's useful e.g. when displaying +the names of PHP properties/variables to end users: + +.. code-block:: twig + + {{ 'dateOfBirth'|humanize }} {# renders: Date of birth #} + {{ 'DateOfBirth'|humanize }} {# renders: Date of birth #} + {{ 'date-of-birth'|humanize }} {# renders: Date-of-birth #} + {{ 'date_of_birth'|humanize }} {# renders: Date of birth #} + {{ 'date of birth'|humanize }} {# renders: Date of birth #} + {{ 'Date Of Birth'|humanize }} {# renders: Date of birth #} .. _reference-twig-filter-trans: diff --git a/routing.rst b/routing.rst index ad7062e5fa5..9828835e7c7 100644 --- a/routing.rst +++ b/routing.rst @@ -70,6 +70,7 @@ do so, create a :doc:`controller class ` like the following: namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class BlogController extends AbstractController @@ -89,6 +90,7 @@ do so, create a :doc:`controller class ` like the following: namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class BlogController extends AbstractController @@ -1668,7 +1670,8 @@ when importing the routes. # trailing_slash_on_root: false # you can optionally exclude some files/subdirectories when loading annotations - # exclude: '../../src/Controller/{DebugEmailController}.php' + # (the value must be a string or an array of PHP glob patterns) + # exclude: '../../src/Controller/{Debug*Controller.php}' .. code-block:: xml @@ -1683,12 +1686,13 @@ when importing the routes. the 'prefix' value is added to the beginning of all imported route URLs the 'name-prefix' value is added to the beginning of all imported route names the 'exclude' option defines the files or subdirectories ignored when loading annotations + (the value must be a PHP glob pattern and you can repeat this option any number of times) --> + exclude="../../src/Controller/{Debug*Controller.php}"> en|es|fr @@ -1714,7 +1718,8 @@ when importing the routes. false, // the optional fourth argument is used to exclude some files // or subdirectories when loading annotations - '../../src/Controller/{DebugEmailController}.php' + // (the value must be a string or an array of PHP glob patterns) + '../../src/Controller/{Debug*Controller.php}' ) // this is added to the beginning of all imported route URLs ->prefix('/blog') @@ -2094,7 +2099,6 @@ host name: ; }; - The value of the ``host`` option can include parameters (which is useful in multi-tenant applications) and these parameters can be validated too with ``requirements``: @@ -2412,6 +2416,16 @@ with a locale. This can be done by defining a different prefix for each locale ; }; +.. note:: + + If a route being imported includes the special :ref:`_locale ` + parameter in its own definition, Symfony will only import it for that locale + and not for the other configured locale prefixes. + + E.g. if a route contains ``locale: 'en'`` in its definition and it's being + imported with ``en`` (prefix: empty) and ``nl`` (prefix: ``/nl``) locales, + that route will be available only in ``en`` locale and not in ``nl``. + Another common requirement is to host the website on a different domain according to the locale. This can be done by defining a different host for each locale. @@ -2560,8 +2574,11 @@ It will help you understand and hopefully fixing unexpected behavior in your app Generating URLs --------------- -Routing systems are bidirectional: 1) they associate URLs with controllers (as -explained in the previous sections); 2) they generate URLs for a given route. +Routing systems are bidirectional: + +1. they associate URLs with controllers (as explained in the previous sections); +2. they generate URLs for a given route. + Generating URLs from routes allows you to not write the ```` values manually in your HTML templates. Also, if the URL of some route changes, you only have to update the route configuration and all links will be updated. @@ -2654,11 +2671,11 @@ the :class:`Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface` class class SomeService { - private $router; + private $urlGenerator; - public function __construct(UrlGeneratorInterface $router) + public function __construct(UrlGeneratorInterface $urlGenerator) { - $this->router = $router; + $this->urlGenerator = $urlGenerator; } public function someMethod() @@ -2666,20 +2683,20 @@ the :class:`Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface` class // ... // generate a URL with no route arguments - $signUpPage = $this->router->generate('sign_up'); + $signUpPage = $this->urlGenerator->generate('sign_up'); // generate a URL with route arguments - $userProfilePage = $this->router->generate('user_profile', [ + $userProfilePage = $this->urlGenerator->generate('user_profile', [ 'username' => $user->getUserIdentifier(), ]); // generated URLs are "absolute paths" by default. Pass a third optional // argument to generate different URLs (e.g. an "absolute URL") - $signUpPage = $this->router->generate('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL); + $signUpPage = $this->urlGenerator->generate('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL); // when a route is localized, Symfony uses by default the current request locale // pass a different '_locale' value if you want to set the locale explicitly - $signUpPageInDutch = $this->router->generate('sign_up', ['_locale' => 'nl']); + $signUpPageInDutch = $this->urlGenerator->generate('sign_up', ['_locale' => 'nl']); } } @@ -2979,8 +2996,7 @@ defined as annotations: controllers: resource: '../../src/Controller/' type: annotation - defaults: - schemes: [https] + schemes: [https] .. code-block:: xml @@ -2991,9 +3007,7 @@ defined as annotations: xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd"> - - HTTPS - + .. code-block:: php diff --git a/routing/custom_route_loader.rst b/routing/custom_route_loader.rst index 78fd55f99aa..22e603fc3a7 100644 --- a/routing/custom_route_loader.rst +++ b/routing/custom_route_loader.rst @@ -240,7 +240,7 @@ you do. The resource name itself is not actually used in the example:: { private $isLoaded = false; - public function load($resource, string $type = null) + public function load($resource, ?string $type = null) { if (true === $this->isLoaded) { throw new \RuntimeException('Do not add the "extra" loader twice'); @@ -267,7 +267,7 @@ you do. The resource name itself is not actually used in the example:: return $routes; } - public function supports($resource, string $type = null) + public function supports($resource, ?string $type = null) { return 'extra' === $type; } @@ -412,7 +412,7 @@ configuration file - you can call the class AdvancedLoader extends Loader { - public function load($resource, string $type = null) + public function load($resource, ?string $type = null) { $routes = new RouteCollection(); @@ -426,7 +426,7 @@ configuration file - you can call the return $routes; } - public function supports($resource, string $type = null) + public function supports($resource, ?string $type = null) { return 'advanced_extra' === $type; } diff --git a/security.rst b/security.rst index 5b38acb53ea..4528d0d03b6 100644 --- a/security.rst +++ b/security.rst @@ -232,6 +232,13 @@ from the `MakerBundle`_: } } +.. tip:: + + Starting in `MakerBundle`_: v1.57.0 - You can pass either ``--with-uuid`` or + ``--with-ulid`` to ``make:user``. Leveraging Symfony's :doc:`Uid Component `, + this generates a ``User`` entity with the ``id`` type as :ref:`Uuid ` + or :ref:`Ulid ` instead of ``int``. + .. versionadded:: 5.3 The :class:`Symfony\\Component\\Security\\Core\\User\\PasswordAuthenticatedUserInterface` @@ -245,6 +252,11 @@ to create the tables by :ref:`creating and running a migration + + + @@ -557,11 +576,15 @@ will be able to authenticate (e.g. login form, API token, etc). return static function (SecurityConfig $security) { // ... + + // the order in which firewalls are defined is very important, as the + // request will be handled by the first firewall whose pattern matches $security->firewall('dev') ->pattern('^/(_(profiler|wdt)|css|images|js)/') ->security(false) ; + // a firewall with no pattern should be defined last because it will match all requests $security->firewall('main') ->lazy(true) @@ -576,25 +599,30 @@ will be able to authenticate (e.g. login form, API token, etc). Only one firewall is active on each request: Symfony uses the ``pattern`` key to find the first match (you can also :doc:`match by host or other things `). +Here, all real URLs are handled by the ``main`` firewall (no ``pattern`` key means +it matches *all* URLs). The ``dev`` firewall is really a fake firewall: it makes sure that you don't accidentally block Symfony's dev tools - which live under URLs like ``/_profiler`` and ``/_wdt``. -All *real* URLs are handled by the ``main`` firewall (no ``pattern`` key means -it matches *all* URLs). A firewall can have many modes of authentication, -in other words, it enables many ways to ask the question "Who are you?". - Often, the user is unknown (i.e. not logged in) when they first visit your website. If you visit your homepage right now, you *will* have access and you'll see that you're visiting a page behind the firewall in the toolbar: .. image:: /_images/security/anonymous_wdt.png - :align: center + :alt: The Symfony profiler toolbar where the Security information shows "Authenticated: no" and "Firewall name: main" Visiting a URL under a firewall doesn't necessarily require you to be authenticated (e.g. the login form has to be accessible or some parts of your application -are public). You'll learn how to restrict access to URLs, controllers or +are public). On the other hand, all pages that you want to be *aware* of a logged in +user have to be under the same firewall. So if you want to display a *"You are logged in +as ..."* message on every page, they all have to be included in the same firewall. + +The same firewall can have many modes of authentication. In other words, it +enables many ways to ask the question *"Who are you?"*. + +You'll learn how to restrict access to URLs, controllers or anything else within your firewall in the :ref:`access control ` section. @@ -650,6 +678,17 @@ Most websites have a login form where users authenticate using an identifier (e.g. email address or username) and a password. This functionality is provided by the built-in :class:`Symfony\\Component\\Security\\Http\Authenticator\\FormLoginAuthenticator`. +You can run the following command to create everything needed to add a login +form in your application: + +.. code-block:: terminal + + $ php bin/console make:security:form-login + +This command will create the required controller and template and it will also +update the security configuration. Alternatively, if you prefer to make these +changes manually, follow the next steps. + First, create a controller for the login form: .. code-block:: terminal @@ -794,10 +833,10 @@ Finally, create or update the template:
- + - + {# If you want to control the URL the user is redirected to on success #} @@ -827,7 +866,7 @@ The form can look like anything, but it usually follows some conventions: Actually, all of this can be configured under the ``form_login`` key. See :ref:`reference-security-firewall-form-login` for more details. -.. caution:: +.. danger:: This login form is currently not protected against CSRF attacks. Read :ref:`form_login-csrf` on how to protect your login form. @@ -1054,7 +1093,8 @@ token (or whatever you need to return) and return the JSON response: class ApiLoginController extends AbstractController { - #[Route('/api/login', name: 'api_login')] + - #[Route('/api/login', name: 'api_login')] + + #[Route('/api/login', name: 'api_login', methods: ['POST'])] - public function index(): Response + public function index(#[CurrentUser] ?User $user): Response { @@ -1367,8 +1407,15 @@ Limiting Login Attempts Login throttling was introduced in Symfony 5.2. -Symfony provides basic protection against `brute force login attacks`_. -You must enable this using the ``login_throttling`` setting: +Symfony provides basic protection against `brute force login attacks`_ thanks to +the :doc:`Rate Limiter component `. If you haven't used this +component in your application yet, install it before using this feature: + +.. code-block:: terminal + + $ composer require symfony/rate-limiter + +Then, enable this feature using the ``login_throttling`` setting: .. configuration-block:: @@ -1452,9 +1499,8 @@ You must enable this using the ``login_throttling`` setting: The ``login_throttling.interval`` option was introduced in Symfony 5.3. -Internally, Symfony uses the :doc:`Rate Limiter component ` -which by default uses Symfony's cache to store the previous login attempts. -However, you can implement a :ref:`custom storage `. +The RateLimiter component uses by default the Symfony cache to store the previous +login attempts. However, you can implement a :ref:`custom storage `. Login attempts are limited on ``max_attempts`` (default: 5) failed requests for ``IP address + username`` and ``5 * max_attempts`` @@ -1602,6 +1648,19 @@ and set the ``limiter`` option to its service ID: ; }; +Customize Successful and Failed Authentication Behavior +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to customize how the successful or failed authentication process is +handled, you don't have to overwrite the respective listeners globally. Instead, +you can set custom success failure handlers by implementing the +:class:`Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationSuccessHandlerInterface` +or the +:class:`Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationFailureHandlerInterface`. + +Read :ref:`how to customize your success handler ` +for more information about this. + .. _security-logging-out: Logging Out @@ -1705,7 +1764,7 @@ Next, you need to create a route for this URL (but not a controller): class SecurityController extends AbstractController { - #[Route('/logout', name: 'app_logout', methods: ['POST'])] + #[Route('/logout', name: 'app_logout', methods: ['GET'])] public function logout() { // controller can be blank: it will never be called! @@ -1718,7 +1777,7 @@ Next, you need to create a route for this URL (but not a controller): # config/routes.yaml app_logout: path: /logout - methods: POST + methods: GET .. code-block:: xml @@ -1729,7 +1788,7 @@ Next, you need to create a route for this URL (but not a controller): xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd"> - + .. code-block:: php @@ -1739,7 +1798,7 @@ Next, you need to create a route for this URL (but not a controller): return function (RoutingConfigurator $routes) { $routes->add('app_logout', '/logout') - ->methods(['POST']) + ->methods(['GET']) ; }; @@ -2531,6 +2590,8 @@ implement :class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface`. Then, your ``isEqualTo()`` method will be called when comparing users instead of the core logic. +.. _security-security-events: + Security Events --------------- @@ -2603,7 +2664,9 @@ Authentication Events .. raw:: html - + :class:`Symfony\\Component\\Security\\Http\\Event\\CheckPassportEvent` Dispatched after the authenticator created the :ref:`security passport `. @@ -2633,6 +2696,12 @@ Authentication Events Other Events ~~~~~~~~~~~~ +:class:`Symfony\\Component\\Security\\Http\\Event\\InteractiveLoginEvent` + Dispatched after authentication was fully successful only when the authenticator + implements :class:`Symfony\\Component\\Security\\Http\\Authenticator\\InteractiveAuthenticatorInterface`, + which indicates login requires explicit user action (e.g. a login form). + Listeners to this event can modify the response sent back to the user. + :class:`Symfony\\Component\\Security\\Http\\Event\\LogoutEvent` Dispatched just before a user logs out of your application. See :ref:`security-logging-out`. @@ -2665,7 +2734,7 @@ Frequently Asked Questions Sometimes authentication may be successful, but after redirecting, you're logged out immediately due to a problem loading the ``User`` from the session. To see if this is an issue, check your log file (``var/log/dev.log``) for - the log message: + the log message. **Cannot refresh token because user has changed** If you see this, there are two possible causes. First, there may be a problem @@ -2714,4 +2783,4 @@ Authorization (Denying Access) .. _`SymfonyCastsVerifyEmailBundle`: https://github.com/symfonycasts/verify-email-bundle .. _`HTTP Basic authentication`: https://en.wikipedia.org/wiki/Basic_access_authentication .. _`Login CSRF attacks`: https://en.wikipedia.org/wiki/Cross-site_request_forgery#Forging_login_requests -.. _`PHP date relative formats`: https://www.php.net/datetime.formats.relative +.. _`PHP date relative formats`: https://www.php.net/manual/en/datetime.formats.php#datetime.formats.relative diff --git a/security/access_control.rst b/security/access_control.rst index b8a5f557286..c467a294f00 100644 --- a/security/access_control.rst +++ b/security/access_control.rst @@ -137,33 +137,49 @@ For each incoming request, Symfony will decide 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``, ``port``, ``host`` or ``method`` are not specified for an entry, that -``access_control`` will match any ``ip``, ``port``, ``host`` or ``method``: - -+-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| URI | IP | PORT | HOST | METHOD | ``access_control`` | Why? | -+=================+=============+=============+=============+============+================================+=============================================================+ -| ``/admin/user`` | 127.0.0.1 | 80 | example.com | GET | rule #2 (``ROLE_USER_IP``) | The URI matches ``path`` and the IP matches ``ip``. | -+-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 127.0.0.1 | 80 | symfony.com | GET | rule #2 (``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`` | 127.0.0.1 | 8080 | symfony.com | GET | rule #1 (``ROLE_USER_PORT``) | The ``path``, ``ip`` and ``port`` match. | -+-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 168.0.0.1 | 80 | symfony.com | GET | rule #3 (``ROLE_USER_HOST``) | The ``ip`` doesn't match neither the first rule nor the | -| | | | | | | second rule. So the third rule (which matches) is used. | -+-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 168.0.0.1 | 80 | symfony.com | POST | rule #3 (``ROLE_USER_HOST``) | The third rule still matches. This would also match the | -| | | | | | | fourth rule (``ROLE_USER_METHOD``), but only the **first** | -| | | | | | | matched ``access_control`` is used. | -+-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 168.0.0.1 | 80 | example.com | POST | rule #4 (``ROLE_USER_METHOD``) | The ``ip`` and ``host`` don't match the first three | -| | | | | | | entries, but the fourth - ``ROLE_USER_METHOD`` - matches | -| | | | | | | and is used. | -+-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/foo`` | 127.0.0.1 | 80 | symfony.com | POST | matches no entries | This doesn't match any ``access_control`` rules, since its | -| | | | | | | URI doesn't match any of the ``path`` values. | -+-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ +``access_control`` will match any ``ip``, ``port``, ``host`` or ``method``. +See the following examples: + +Example #1: + * **URI** ``/admin/user`` + * **IP**: ``127.0.0.1``, **Port**: ``80``, **Host**: ``example.com``, **Method**: ``GET`` + * **Rule applied**: rule #2 (``ROLE_USER_IP``) + * **Why?** The URI matches ``path`` and the IP matches ``ip``. +Example #2: + * **URI** ``/admin/user`` + * **IP**: ``127.0.0.1``, **Port**: ``80``, **Host**: ``symfony.com``, **Method**: ``GET`` + * **Rule applied**: rule #2 (``ROLE_USER_IP``) + * **Why?** The ``path`` and ``ip`` still match. This would also match the + ``ROLE_USER_HOST`` entry, but *only* the **first** ``access_control`` match is used. +Example #3: + * **URI** ``/admin/user`` + * **IP**: ``127.0.0.1``, **Port**: ``8080``, **Host**: ``symfony.com``, **Method**: ``GET`` + * **Rule applied**: rule #1 (``ROLE_USER_PORT``) + * **Why?** The ``path``, ``ip`` and ``port`` match. +Example #4: + * **URI** ``/admin/user`` + * **IP**: ``168.0.0.1``, **Port**: ``80``, **Host**: ``symfony.com``, **Method**: ``GET`` + * **Rule applied**: rule #3 (``ROLE_USER_HOST``) + * **Why?** The ``ip`` doesn't match neither the first rule nor the second rule. + * So the third rule (which matches) is used. +Example #5: + * **URI** ``/admin/user`` + * **IP**: ``168.0.0.1``, **Port**: ``80``, **Host**: ``symfony.com``, **Method**: ``POST`` + * **Rule applied**: rule #3 (``ROLE_USER_HOST``) + * **Why?** The third rule still matches. This would also match the fourth rule + * (``ROLE_USER_METHOD``), but only the **first** matched ``access_control`` is used. +Example #6: + * **URI** ``/admin/user`` + * **IP**: ``168.0.0.1``, **Port**: ``80``, **Host**: ``example.com``, **Method**: ``POST`` + * **Rule applied**: rule #4 (``ROLE_USER_METHOD``) + * **Why?** The ``ip`` and ``host`` don't match the first three entries, but + * the fourth - ``ROLE_USER_METHOD`` - matches and is used. +Example #7: + * **URI** ``/foo`` + * **IP**: ``127.0.0.1``, **Port**: ``80``, **Host**: ``symfony.com``, **Method**: ``POST`` + * **Rule applied**: matches no entries + * **Why?** This doesn't match any ``access_control`` rules, since its URI + * doesn't match any of the ``path`` values. .. caution:: diff --git a/security/access_denied_handler.rst b/security/access_denied_handler.rst index 93448456cf0..6ab876d5c4a 100644 --- a/security/access_denied_handler.rst +++ b/security/access_denied_handler.rst @@ -40,7 +40,7 @@ unauthenticated user tries to access a protected resource:: $this->urlGenerator = $urlGenerator; } - public function start(Request $request, AuthenticationException $authException = null): RedirectResponse + public function start(Request $request, ?AuthenticationException $authException = null): RedirectResponse { // add a custom flash message and redirect to the login page $request->getSession()->getFlashBag()->add('note', 'You have to login in order to access this page.'); diff --git a/security/csrf.rst b/security/csrf.rst index fd89ff17ba9..752186e6bfc 100644 --- a/security/csrf.rst +++ b/security/csrf.rst @@ -72,6 +72,8 @@ protected forms. As an alternative, you can: load the CSRF token with an uncached AJAX request and replace the form field value with it. +.. _csrf-protection-forms: + CSRF Protection in Symfony Forms -------------------------------- @@ -82,7 +84,54 @@ protected against CSRF attacks. .. _form-csrf-customization: By default Symfony adds the CSRF token in a hidden field called ``_token``, but -this can be customized on a form-by-form basis:: +this can be customized (1) globally for all forms and (2) on a form-by-form basis. +Globally, you can configure it under the ``framework.form`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + form: + csrf_protection: + enabled: true + field_name: 'custom_token_name' + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->form()->csrfProtection() + ->enabled(true) + ->fieldName('custom_token_name') + ; + }; + +On a form-by-form basis, you can configure the CSRF protection in the ``setDefaults()`` +method of each form:: // src/Form/TaskType.php namespace App\Form; diff --git a/security/custom_authenticator.rst b/security/custom_authenticator.rst index 8dbeeaf287a..dcddbc03444 100644 --- a/security/custom_authenticator.rst +++ b/security/custom_authenticator.rst @@ -37,19 +37,24 @@ method that fits most use-cases:: */ public function supports(Request $request): ?bool { - return $request->headers->has('X-AUTH-TOKEN'); + // "auth-token" is an example of a custom, non-standard HTTP header used in this application + return $request->headers->has('auth-token'); } public function authenticate(Request $request): Passport { - $apiToken = $request->headers->get('X-AUTH-TOKEN'); + $apiToken = $request->headers->get('auth-token'); if (null === $apiToken) { // The token header was empty, authentication fails with HTTP Status // Code 401 "Unauthorized" throw new CustomUserMessageAuthenticationException('No API token provided'); } - return new SelfValidatingPassport(new UserBadge($apiToken)); + // implement your own logic to get the user identifier from `$apiToken` + // e.g. by looking up a user in the database using its API key + $userIdentifier = /** ... */; + + return new SelfValidatingPassport(new UserBadge($userIdentifier)); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response @@ -159,7 +164,7 @@ can define what happens in these cases: ``onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response`` If the user is authenticated, this method is called with the authenticated ``$token``. This method can return a response (e.g. - redirect the user to the homepage). + redirect the user to some page). If ``null`` is returned, the request continues like normal (i.e. the controller matching the login route is called). This is useful for API @@ -288,7 +293,6 @@ The following credential classes are supported by default: $apiToken )); - Self Validating Passport ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/security/entry_point.rst b/security/entry_point.rst index fb1fbe4ea41..9dfaf8bca8c 100644 --- a/security/entry_point.rst +++ b/security/entry_point.rst @@ -68,7 +68,6 @@ You can configure this using the ``entry_point`` setting: $security->enableAuthenticatorManager(true); // .... - // allow authentication using a form or HTTP basic $mainFirewall = $security->firewall('main'); $mainFirewall diff --git a/security/force_https.rst b/security/force_https.rst index 817adbdb50f..016ac59496e 100644 --- a/security/force_https.rst +++ b/security/force_https.rst @@ -74,8 +74,8 @@ access control: }; To make life easier while developing, you can also use an environment variable, -like ``requires_channel: '%env(SECURE_SCHEME)%'``. In your ``.env`` file, set -``SECURE_SCHEME`` to ``http`` by default, but override it to ``https`` on production. +like ``requires_channel: '%env(REQUIRED_SCHEME)%'``. In your ``.env`` file, set +``REQUIRED_SCHEME`` to ``http`` by default, but override it to ``https`` on production. See :doc:`/security/access_control` for more details about ``access_control`` in general. diff --git a/security/impersonating_user.rst b/security/impersonating_user.rst index 99ba88d2b25..8317b9c30bd 100644 --- a/security/impersonating_user.rst +++ b/security/impersonating_user.rst @@ -142,7 +142,7 @@ instance, to show a link to exit impersonation in a template: .. code-block:: html+twig {% if is_granted('IS_IMPERSONATOR') %} - Exit impersonation + Exit impersonation {% endif %} .. versionadded:: 5.1 @@ -309,17 +309,17 @@ logic you want:: namespace App\Security\Voter; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; - use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; class SwitchToCustomerVoter extends Voter { - private $security; + private $accessDecisionManager; - public function __construct(Security $security) + public function __construct(AccessDecisionManagerInterface $accessDecisionManager) { - $this->security = $security; + $this->accessDecisionManager = $accessDecisionManager; } protected function supports($attribute, $subject): bool @@ -337,12 +337,12 @@ logic you want:: } // you can still check for ROLE_ALLOWED_TO_SWITCH - if ($this->security->isGranted('ROLE_ALLOWED_TO_SWITCH')) { + if ($this->accessDecisionManager->decide($token, ['ROLE_ALLOWED_TO_SWITCH'])) { return true; } // check for any roles you want - if ($this->security->isGranted('ROLE_TECH_SUPPORT')) { + if ($this->accessDecisionManager->decide($token, ['ROLE_TECH_SUPPORT'])) { return true; } @@ -363,9 +363,13 @@ not this is allowed. If your voter isn't called, see :ref:`declaring-the-voter-a Events ------ -The firewall dispatches the ``security.switch_user`` event right after the impersonation -is completed. The :class:`Symfony\\Component\\Security\\Http\\Event\\SwitchUserEvent` is -passed to the listener, and you can use this to get the user that you are now impersonating. +the ``security.switch_user`` event is dispatched just before the impersonation +is fully completed. Your :doc:`listener or subscriber ` will +receive a :class:`Symfony\\Component\\Security\\Http\\Event\\SwitchUserEvent`, +which you can use to get the user that you are now impersonating. + +This event is also dispatched just before impersonation is fully exited. You can +use it to get the original impersonator user. The :ref:`locale-sticky-session` section does not update the locale when you impersonate a user. If you *do* want to be sure to update the locale when you diff --git a/security/ldap.rst b/security/ldap.rst index b984bdf749b..39cf26081c7 100644 --- a/security/ldap.rst +++ b/security/ldap.rst @@ -197,6 +197,11 @@ use the ``ldap`` user provider. ; }; +.. versionadded:: 5.4 + + The ``LdapUser::getExtraFields()`` method always returns an array of values. + In prior Symfony versions, ``LdapUserProvider`` threw an ``InvalidArgumentException`` + on multiple attributes. .. caution:: @@ -530,4 +535,3 @@ Configuration example for form login and query_string .. _`LDAP PHP extension`: https://www.php.net/manual/en/intro.ldap.php .. _`RFC4515`: https://datatracker.ietf.org/doc/rfc4515/ .. _`LDAP injection`: http://projects.webappsec.org/w/page/13246947/LDAP%20Injection - diff --git a/security/login_link.rst b/security/login_link.rst index 4dea64c7662..51f6f613f1b 100644 --- a/security/login_link.rst +++ b/security/login_link.rst @@ -20,16 +20,15 @@ my password, etc.) Using the Login Link Authenticator ---------------------------------- -This guide assumes you have setup security and have created a user object -in your application. Follow :doc:`the main security guide ` if -this is not yet the case. +This guide assumes you have :doc:`setup security and have created a user object ` +in your application. 1) Configure the Login Link Authenticator ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The login link authenticator is configured using the ``login_link`` option -under the firewall. You must configure a ``check_route`` and -``signature_properties`` when enabling this authenticator: +under the firewall and requires defining two options called ``check_route`` +and ``signature_properties`` (explained below): .. configuration-block:: @@ -82,7 +81,7 @@ contain at least one property of your ``User`` object that uniquely identifies this user (e.g. the user ID). Read more about this setting :ref:`further down below `. -The ``check_route`` must be an existing route and it will be used to +The ``check_route`` must be the name of an existing route and it will be used to generate the login link that will authenticate the user. You don't need a controller (or it can be empty) because the login link authenticator will intercept requests to this route: @@ -160,9 +159,8 @@ intercept requests to this route: 2) Generate the Login Link ~~~~~~~~~~~~~~~~~~~~~~~~~~ -Now that the authenticator is able to check the login links, you must -create a page where a user can request a login link and log in to your -website. +Now that the authenticator is able to check the login links, you can +create a page where a user can request a login link. The login link can be generated using the :class:`Symfony\\Component\\Security\\Http\\LoginLink\\LoginLinkHandlerInterface`. @@ -185,7 +183,7 @@ this interface:: */ public function requestLoginLink(LoginLinkHandlerInterface $loginLinkHandler, UserRepository $userRepository, Request $request) { - // check if login form is submitted + // check if form is submitted if ($request->isMethod('POST')) { // load the user in some way (e.g. using the form input) $email = $request->request->get('email'); @@ -199,8 +197,8 @@ this interface:: // ... send the link and return a response (see next section) } - // if it's not submitted, render the "login" form - return $this->render('security/login.html.twig'); + // if it's not submitted, render the form to request the "login link" + return $this->render('security/request_login_link.html.twig'); } // ... @@ -208,7 +206,7 @@ this interface:: .. code-block:: html+twig - {# templates/security/login.html.twig #} + {# templates/security/request_login_link.html.twig #} {% extends 'base.html.twig' %} {% block body %} @@ -279,7 +277,7 @@ number:: return $this->render('security/login_link_sent.html.twig'); } - return $this->render('security/login.html.twig'); + return $this->render('security/request_login_link.html.twig'); } // ... @@ -300,7 +298,7 @@ number:: This will send an email like this to the user: .. image:: /_images/security/login_link_email.png - :align: center + :alt: A default Symfony e-mail containing the text "Click on the button below to confirm you want to sign in" and the button with the login link. .. tip:: @@ -314,7 +312,7 @@ This will send an email like this to the user: class CustomLoginLinkNotification extends LoginLinkNotification { - public function asEmailMessage(EmailRecipientInterface $recipient, string $transport = null): ?EmailMessage + public function asEmailMessage(EmailRecipientInterface $recipient, ?string $transport = null): ?EmailMessage { $emailMessage = parent::asEmailMessage($recipient, $transport); @@ -666,7 +664,7 @@ user create this POST request (e.g. by clicking a button)::

Hi! You are about to login to ...

+ create the POST request --> @@ -693,6 +691,8 @@ from the first hash value and the ``kernel.secret`` container parameter. This allows Symfony to sign this final hash, which is contained in the login URL. The final hash is also a Base64 encoded SHA-256 hash. +.. _login-link_customize-success-handler: + Customizing the Success Handler ------------------------------- @@ -822,7 +822,7 @@ features such as the locale used to generate the link:: // ... } - return $this->render('security/login.html.twig'); + return $this->render('security/request_login_link.html.twig'); } // ... diff --git a/security/passwords.rst b/security/passwords.rst index f00cec6184c..b228058c7e3 100644 --- a/security/passwords.rst +++ b/security/passwords.rst @@ -294,6 +294,13 @@ you'll see a success message and a list of any other steps you need to do. $ php bin/console make:reset-password +.. tip:: + + Starting in `MakerBundle`_: v1.57.0 - You can pass either ``--with-uuid`` or + ``--with-ulid`` to ``make:reset-password``. Leveraging Symfony's :doc:`Uid Component `, + the entities will be generated with the ``id`` type as :ref:`Uuid ` + or :ref:`Ulid ` instead of ``int``. + You can customize the reset password bundle's behavior by updating the ``reset_password.yaml`` file. For more information on the configuration, check out the `SymfonyCastsResetPasswordBundle`_ guide. diff --git a/security/remember_me.rst b/security/remember_me.rst index 19b6cb44a7b..055c0a783cf 100644 --- a/security/remember_me.rst +++ b/security/remember_me.rst @@ -217,8 +217,9 @@ After logging in, you can use the security profiler to see if this badge is present: .. image:: /_images/security/profiler-badges.png + :alt: The Security page of the Symfony profiler, with the "Authenticators" tab showing the remember me badge in the passport object. -Without this badge, remember me will be not be activated (regardless of all +Without this badge, remember me will not be activated (regardless of all other settings). Add Remember Me Support to Custom Authenticators @@ -288,7 +289,6 @@ Persistent tokens The ``service`` option was introduced in Symfony 5.1. - .. _security-remember-me-signature: Using Signed Remember Me Tokens diff --git a/security/user_providers.rst b/security/user_providers.rst index ba7b169f0c9..cab94b76af8 100644 --- a/security/user_providers.rst +++ b/security/user_providers.rst @@ -77,24 +77,15 @@ the user provider uses :doc:`Doctrine ` to retrieve them. use App\Entity\User; use Symfony\Config\SecurityConfig; - $container->loadFromExtension('security', [ - 'providers' => [ - 'users' => [ - 'entity' => [ - // the class of the entity that represents users - 'class' => User::class, - // the property to query by - e.g. email, username, etc - 'property' => 'email', - - // optional: if you're using multiple Doctrine entity - // managers, this option defines which one to use - //'manager_name' => 'customer', - ], - ], - ], - + return static function (SecurityConfig $security): void { // ... - ]); + + $security->provider('app_user_provider') + ->entity() + ->class(User::class) + ->property('email') + ; + }; .. _authenticating-someone-with-a-custom-entity-provider: @@ -185,18 +176,16 @@ To finish this, remove the ``property`` key from the user provider in // config/packages/security.php use App\Entity\User; + use Symfony\Config\SecurityConfig; - $container->loadFromExtension('security', [ - 'providers' => [ - 'users' => [ - 'entity' => [ - 'class' => User::class, - ], - ], - ], - + return static function (SecurityConfig $security): void { // ... - ]); + + $security->provider('app_user_provider') + ->entity() + ->class(User::class) + ; + }; Now, whenever Symfony uses the user provider, the ``loadUserByIdentifier()`` method on your ``UserRepository`` will be called. @@ -217,18 +206,67 @@ including their passwords. Make sure the passwords are hashed properly. See After setting up hashing, you can configure all the user information in ``security.yaml``: -.. code-block:: yaml +.. configuration-block:: - # config/packages/security.yaml - security: - providers: - backend_users: - memory: - users: - john_admin: { password: '$2y$13$jxGxc ... IuqDju', roles: ['ROLE_ADMIN'] } - jane_admin: { password: '$2y$13$PFi1I ... rGwXCZ', roles: ['ROLE_ADMIN', 'ROLE_SUPER_ADMIN'] } + .. code-block:: yaml - # ... + # config/packages/security.yaml + security: + providers: + backend_users: + memory: + users: + john_admin: { password: '$2y$13$jxGxc ... IuqDju', roles: ['ROLE_ADMIN'] } + jane_admin: { password: '$2y$13$PFi1I ... rGwXCZ', roles: ['ROLE_ADMIN', 'ROLE_SUPER_ADMIN'] } + + # ... + + .. code-block:: xml + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + use App\Entity\User; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + $memoryProvider = $security->provider('app_user_provider')->memory(); + $memoryProvider + ->user('john_admin') + ->password('$2y$13$jxGxc ... IuqDju') + ->roles(['ROLE_ADMIN']) + ; + + $memoryProvider + ->user('jane_admin') + ->password('$2y$13$PFi1I ... rGwXCZ') + ->roles(['ROLE_ADMIN', 'ROLE_SUPER_ADMIN']) + ; + }; .. caution:: @@ -246,27 +284,99 @@ providers are configured is important because Symfony will look for users starting from the first provider and will keep looking for in the other providers until the user is found: -.. code-block:: yaml +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + providers: + backend_users: + ldap: + # ... + + legacy_users: + entity: + # ... - # config/packages/security.yaml - security: - # ... - providers: - backend_users: - ldap: - # ... + users: + entity: + # ... - legacy_users: - entity: - # ... + all_users: + chain: + providers: ['legacy_users', 'users', 'backend_users'] - users: - entity: - # ... + .. code-block:: xml - all_users: - chain: - providers: ['legacy_users', 'users', 'backend_users'] + + + + + + + + + + + + + + + + + + + + + + + + + + backend_users + legacy_users + users + + + + + + .. code-block:: php + + // config/packages/security.php + use App\Entity\User; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + $backendProvider = $security->provider('backend_users') + ->ldap() + // ... + ; + + $legacyProvider = $security->provider('legacy_users') + ->entity() + // ... + ; + + $userProvider = $security->provider('users') + ->entity() + // ... + ; + + $allProviders = $security->provider('all_users')->chain() + ->providers([$backendProvider, $legacyProvider, $userProvider]) + ; + }; .. _security-custom-user-provider: @@ -362,14 +472,52 @@ Most of the work is already done! Read the comments in the code and update the TODO sections to finish the user provider. When you're done, tell Symfony about the user provider by adding it in ``security.yaml``: -.. code-block:: yaml +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + providers: + # the name of your user provider can be anything + your_custom_user_provider: + id: App\Security\UserProvider + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + use App\Security\UserProvider; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... - # config/packages/security.yaml - security: - providers: - # the name of your user provider can be anything - your_custom_user_provider: - id: App\Security\UserProvider + $customProvider = $security->provider('your_custom_user_provider') + ->id(UserProvider::class) + // ... + ; + }; Lastly, update the ``config/packages/security.yaml`` file to set the ``provider`` key to ``your_custom_user_provider`` in all the firewalls which diff --git a/security/voters.rst b/security/voters.rst index a770e386c02..5019638fdf4 100644 --- a/security/voters.rst +++ b/security/voters.rst @@ -222,25 +222,25 @@ Checking for Roles inside a Voter --------------------------------- What if you want to call ``isGranted()`` from *inside* your voter - e.g. you want -to see if the current user has ``ROLE_SUPER_ADMIN``. That's possible by injecting -the :class:`Symfony\\Component\\Security\\Core\\Security` -into your voter. You can use this to, for example, *always* allow access to a user +to see if the current user has ``ROLE_SUPER_ADMIN``. That's possible by using an +:class:`access decision manager ` +inside your voter. You can use this to, for example, *always* allow access to a user with ``ROLE_SUPER_ADMIN``:: // src/Security/PostVoter.php // ... - use Symfony\Component\Security\Core\Security; + use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; class PostVoter extends Voter { // ... - private $security; + private $accessDecisionManager; - public function __construct(Security $security) + public function __construct(AccessDecisionManagerInterface $accessDecisionManager) { - $this->security = $security; + $this->accessDecisionManager = $accessDecisionManager; } protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool @@ -248,7 +248,7 @@ with ``ROLE_SUPER_ADMIN``:: // ... // ROLE_SUPER_ADMIN can do anything! The power! - if ($this->security->isGranted('ROLE_SUPER_ADMIN')) { + if ($this->accessDecisionManager->decide($token, ['ROLE_SUPER_ADMIN'])) { return true; } @@ -256,6 +256,25 @@ with ``ROLE_SUPER_ADMIN``:: } } +.. caution:: + + In the previous example, avoid using the following code to check if a role + is granted permission:: + + // DON'T DO THIS + use Symfony\Component\Security\Core\Security; + // ... + + if ($this->security->isGranted('ROLE_SUPER_ADMIN')) { + // ... + } + + The ``Security::isGranted()`` method inside a voter has a significant + drawback: it does not guarantee that the checks are performed on the same + token as the one in your voter. The token in the token storage might have + changed or could change in the meantime. Always use the ``AccessDecisionManager`` + instead. + If you're using the :ref:`default services.yaml configuration `, you're done! Symfony will automatically pass the ``security.helper`` service when instantiating your voter (thanks to autowiring). diff --git a/serializer.rst b/serializer.rst index 7bf972e908e..50bd0149a19 100644 --- a/serializer.rst +++ b/serializer.rst @@ -90,7 +90,7 @@ custom normalizers and/or encoders can also be loaded by tagging them as :ref:`serializer.encoder `. It's also possible to set the priority of the tag in order to decide the matching order. -.. caution:: +.. danger:: Always make sure to load the ``DateTimeNormalizer`` when serializing the ``DateTime`` or ``DateTimeImmutable`` classes to avoid excessive memory @@ -209,14 +209,16 @@ You can also specify the context on a per-property basis:: .. code-block:: yaml + # config/serializer/custom_config.yaml App\Model\Person: attributes: createdAt: - context: - datetime_format: 'Y-m-d' + contexts: + - { context: { datetime_format: 'Y-m-d' } } .. code-block:: xml + 'Y-m-d'], - denormalizationContext: [DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339], + denormalizationContext: [DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'], // To prevent to have the time from the moment of denormalization )] public $createdAt; diff --git a/serializer/custom_encoders.rst b/serializer/custom_encoders.rst index 432cb205b63..95f3131f418 100644 --- a/serializer/custom_encoders.rst +++ b/serializer/custom_encoders.rst @@ -53,11 +53,10 @@ create your own encoder that uses the ``Symfony\Component\Serializer\Encoder\ContextAwareDecoderInterface`` or ``Symfony\Component\Serializer\Encoder\ContextAwareEncoderInterface`` accordingly. - Registering it in your app -------------------------- -If you use the Symfony Framework. then you probably want to register this encoder +If you use the Symfony Framework, then you probably want to register this encoder as a service in your app. If you're using the :ref:`default services.yaml configuration `, that's done automatically! diff --git a/serializer/custom_normalizer.rst b/serializer/custom_normalizer.rst index c2c8c5d0bbf..dd02db39bb1 100644 --- a/serializer/custom_normalizer.rst +++ b/serializer/custom_normalizer.rst @@ -33,7 +33,7 @@ to customize the normalized data. To do that, leverage the ``ObjectNormalizer``: $this->normalizer = $normalizer; } - public function normalize($topic, string $format = null, array $context = []) + public function normalize($topic, ?string $format = null, array $context = []) { $data = $this->normalizer->normalize($topic, $format, $context); @@ -45,7 +45,7 @@ to customize the normalized data. To do that, leverage the ``ObjectNormalizer``: return $data; } - public function supportsNormalization($data, string $format = null, array $context = []) + public function supportsNormalization($data, ?string $format = null, array $context = []) { return $data instanceof Topic; } @@ -86,4 +86,3 @@ is called. as well the ones included in `API Platform`_ natively implement this interface. .. _`API Platform`: https://api-platform.com - diff --git a/service_container.rst b/service_container.rst index fa3681e06f9..8f3d53b6733 100644 --- a/service_container.rst +++ b/service_container.rst @@ -45,7 +45,6 @@ service's class or interface name. Want to :doc:`log ` something? No p } } - What other services are available? Find out by running: .. code-block:: terminal @@ -82,8 +81,8 @@ in the container. There are actually *many* more services in the container, and each service has a unique id in the container, like ``request_stack`` or ``router.default``. For a full list, you can run ``php bin/console debug:container``. But most of the time, - you won't need to worry about this. See :ref:`services-wire-specific-service`. - See :doc:`/service_container/debug`. + you won't need to worry about this. See :ref:`how to choose a specific service + `. See :doc:`/service_container/debug`. .. _service-container-creating-service: @@ -230,10 +229,11 @@ each time you ask for it. Thanks to this configuration, you can automatically use any classes from the ``src/`` directory as a service, without needing to manually configure - it. Later, you'll learn more about this in :ref:`service-psr4-loader`. + it. Later, you'll learn how to :ref:`import many services at once + ` with resource. - If you'd prefer to manually wire your service, that's totally possible: see - :ref:`services-explicitly-configure-wire-services`. + If you'd prefer to manually wire your service, you can + :ref:`use explicit configuration `. .. _service-container_limiting-to-env: @@ -512,7 +512,6 @@ pass here. No problem! In your configuration, you can explicitly set this argume ; }; - Thanks to this, the container will pass ``manager@example.com`` to the ``$adminEmail`` argument of ``__construct`` when creating the ``SiteUpdateManager`` service. The other arguments will still be autowired. @@ -840,8 +839,8 @@ argument for *any* service defined in this file! You can bind arguments by name (e.g. ``$adminEmail``), by type (e.g. ``Psr\Log\LoggerInterface``) or both (e.g. ``Psr\Log\LoggerInterface $requestLogger``). -The ``bind`` config can also be applied to specific services or when loading many -services at once (i.e. :ref:`service-psr4-loader`). +The ``bind`` config can also be applied to specific services or when +:ref:`loading many services at once `). Abstract Service Arguments -------------------------- @@ -947,6 +946,17 @@ you don't need to do *anything*: the service will be automatically loaded. Then, implements ``Twig\Extension\ExtensionInterface``. And thanks to ``autowire``, you can even add constructor arguments without any configuration. +Autoconfiguration also works with attributes. Some attributes like +:class:`Symfony\\Component\\Messenger\\Attribute\\AsMessageHandler`, +:class:`Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener` and +:class:`Symfony\\Component\\Console\\Attribute\\AsCommand` are registered +for autoconfiguration. Any class using these attributes will have tags applied +to them. + +.. versionadded:: 5.3 + + Autoconfiguration through attributes was introduced in Symfony 5.3. + Linting Service Definitions --------------------------- @@ -1026,6 +1036,26 @@ setting: ; }; +It is also possible to define a service as public thanks to the ``#[Autoconfigure]`` +attribute. This attribute must be used directly on the class of the service +you want to configure:: + + // src/Service/PublicService.php + namespace App\Service; + + use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + + #[Autoconfigure(public: true)] + class PublicService + { + // ... + } + +.. versionadded:: 5.3 + + The ``#[Autoconfigure]`` attribute was introduced in Symfony 5.3. PHP + attributes require at least PHP 8.0. + .. deprecated:: 5.1 As of Symfony 5.1, it is no longer possible to autowire the service @@ -1091,9 +1121,9 @@ key. For example, the default Symfony configuration contains this: This can be used to quickly make many classes available as services and apply some default configuration. The ``id`` of each service is its fully-qualified class name. You can override any service that's imported by using its id (class name) below -(e.g. see :ref:`services-manually-wire-args`). If you override a service, none of -the options (e.g. ``public``) are inherited from the import (but the overridden -service *does* still inherit from ``_defaults``). +(e.g. see :ref:`how to manually wire arguments `). +If you override a service, none of the options (e.g. ``public``) are inherited +from the import (but the overridden service *does* still inherit from ``_defaults``). You can also ``exclude`` certain paths. This is optional, but will slightly increase performance in the ``dev`` environment: excluded paths are not tracked and so modifying @@ -1288,7 +1318,7 @@ and ``site_update_manager.normal_users``. Thanks to the alias, if you type-hint ``SiteUpdateManager`` the first (``site_update_manager.superadmin``) will be passed. If you want to pass the second, you'll need to :ref:`manually wire the service ` -or to create a named ref:`autowiring alias `. +or to create a named :ref:`autowiring alias `. .. caution:: diff --git a/service_container/alias_private.rst b/service_container/alias_private.rst index e4f7604a846..8ccb131cf49 100644 --- a/service_container/alias_private.rst +++ b/service_container/alias_private.rst @@ -62,6 +62,26 @@ You can also control the ``public`` option on a service-by-service basis: ->public(); }; +It is also possible to define a service as public thanks to the ``#[Autoconfigure]`` +attribute. This attribute must be used directly on the class of the service +you want to configure:: + + // src/Service/Foo.php + namespace App\Service; + + use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + + #[Autoconfigure(public: true)] + class Foo + { + // ... + } + +.. versionadded:: 5.3 + + The ``#[Autoconfigure]`` attribute was introduced in Symfony 5.3. PHP + attributes require at least PHP 8.0. + .. _services-why-private: Private services are special because they allow the container to optimize whether diff --git a/service_container/autowiring.rst b/service_container/autowiring.rst index 85b649778bf..6e86ee9c6f2 100644 --- a/service_container/autowiring.rst +++ b/service_container/autowiring.rst @@ -119,7 +119,6 @@ both services: ->autowire(); }; - Now, you can use the ``TwitterClient`` service immediately in a controller:: // src/Controller/DefaultController.php @@ -256,7 +255,6 @@ adding a service alias: $services->alias(Rot13Transformer::class, 'app.rot13.transformer'); }; - This creates a service "alias", whose id is ``App\Util\Rot13Transformer``. Thanks to this, autowiring sees this and uses it whenever the ``Rot13Transformer`` class is type-hinted. @@ -322,8 +320,8 @@ To fix that, add an :ref:`alias `: App\Util\Rot13Transformer: ~ - # the ``App\Util\Rot13Transformer`` service will be injected when - # an ``App\Util\TransformerInterface`` type-hint is detected + # the App\Util\Rot13Transformer service will be injected when + # an App\Util\TransformerInterface type-hint is detected App\Util\TransformerInterface: '@App\Util\Rot13Transformer' .. code-block:: xml @@ -360,7 +358,6 @@ To fix that, add an :ref:`alias `: $services->alias(TransformerInterface::class, Rot13Transformer::class); }; - Thanks to the ``App\Util\TransformerInterface`` alias, the autowiring subsystem knows that the ``App\Util\Rot13Transformer`` service should be injected when dealing with the ``TransformerInterface``. @@ -431,7 +428,7 @@ type hinted, but use the ``UppercaseTransformer`` implementation in some specific cases. To do so, you can create a normal alias from the ``TransformerInterface`` interface to ``Rot13Transformer``, and then create a *named autowiring alias* from a special string containing the -interface followed by a variable name matching the one you use when doing +interface followed by an argument name matching the one you use when doing the injection:: // src/Service/MastodonClient.php @@ -467,13 +464,13 @@ the injection:: App\Util\Rot13Transformer: ~ App\Util\UppercaseTransformer: ~ - # the ``App\Util\UppercaseTransformer`` service will be - # injected when an ``App\Util\TransformerInterface`` - # type-hint for a ``$shoutyTransformer`` argument is detected. + # the App\Util\UppercaseTransformer service will be + # injected when an App\Util\TransformerInterface + # type-hint for a $shoutyTransformer argument is detected App\Util\TransformerInterface $shoutyTransformer: '@App\Util\UppercaseTransformer' # If the argument used for injection does not match, but the - # type-hint still matches, the ``App\Util\Rot13Transformer`` + # type-hint still matches, the App\Util\Rot13Transformer # service will be injected. App\Util\TransformerInterface: '@App\Util\Rot13Transformer' @@ -530,7 +527,7 @@ the injection:: // the App\Util\UppercaseTransformer service will be // injected when an App\Util\TransformerInterface - // type-hint for a $shoutyTransformer argument is detected. + // type-hint for a $shoutyTransformer argument is detected $services->alias(TransformerInterface::class.' $shoutyTransformer', UppercaseTransformer::class); // If the argument used for injection does not match, but the @@ -556,15 +553,19 @@ If the argument is named ``$shoutyTransformer``, But, you can also manually wire any *other* service by specifying the argument under the arguments key. -Another possibility is to use the ``#[Target]`` attribute. By using this attribute -on the argument you want to autowire, you can define exactly which service to inject -by using its alias. Thanks to this, you're able to have multiple services implementing -the same interface and keep the argument name decorrelated of any implementation name -(like shown in the example above). +Another option is to use the ``#[Target]`` attribute. By adding this attribute +to the argument you want to autowire, you can specify which service to inject by +passing the name of the argument used in the named alias. This way, you can have +multiple services implementing the same interface and keep the argument name +separate from any implementation name (like shown in the example above). -Let's say you defined the ``app.uppercase_transformer`` alias for the -``App\Util\UppercaseTransformer`` service. You would be able to use the ``#[Target]`` -attribute like this:: +.. warning:: + + The ``#[Target]`` attribute only accepts the name of the argument used in the + named alias; it **does not** accept service ids or service aliases. + +Suppose you want to inject the ``App\Util\UppercaseTransformer`` service. You would use +the ``#[Target]`` attribute by passing the name of the ``$shoutyTransformer`` argument:: // src/Service/MastodonClient.php namespace App\Service; @@ -576,12 +577,25 @@ attribute like this:: { private $transformer; - public function __construct(#[Target('app.uppercase_transformer')] TransformerInterface $transformer) - { + public function __construct( + #[Target('shoutyTransformer')] TransformerInterface $transformer, + ) { $this->transformer = $transformer; } } +.. tip:: + + Since the ``#[Target]`` attribute normalizes the string passed to it to its + camelCased form, name variations (e.g. ``shouty.transformer``) also work. + +.. note:: + + Some IDEs will show an error when using ``#[Target]`` as in the previous example: + *"Attribute cannot be applied to a property because it does not contain the 'Attribute::TARGET_PROPERTY' flag"*. + The reason is that thanks to `PHP constructor promotion`_ this constructor + argument is both a parameter and a class property. You can safely ignore this error message. + .. versionadded:: 5.3 The ``#[Target]`` attribute was introduced in Symfony 5.3. @@ -737,3 +751,4 @@ over all code. .. _ROT13: https://en.wikipedia.org/wiki/ROT13 .. _service definition prototype: https://symfony.com/blog/new-in-symfony-3-3-psr-4-based-service-discovery +.. _`PHP constructor promotion`: https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion diff --git a/service_container/factories.rst b/service_container/factories.rst index a188bb2a046..3f13655c6cb 100644 --- a/service_container/factories.rst +++ b/service_container/factories.rst @@ -82,7 +82,6 @@ create its object: ->factory([NewsletterManagerStaticFactory::class, 'createNewsletterManager']); }; - .. note:: When using a factory to create services, the value chosen for class diff --git a/service_container/injection_types.rst b/service_container/injection_types.rst index 595ac79b185..d801ae0210d 100644 --- a/service_container/injection_types.rst +++ b/service_container/injection_types.rst @@ -364,15 +364,11 @@ Another possibility is setting public fields of the class directly:: }; There are mainly only disadvantages to using property injection, it is similar -to setter injection but with these additional important problems: +to setter injection but with this additional important problem: * You cannot control when the dependency is set at all, it can be changed at any point in the object's lifetime. -* You cannot use type hinting so you cannot be sure what dependency is injected - except by writing into the class code to explicitly test the class instance - before using it. - But, it is useful to know that this can be done with the service container, especially if you are working with code that is out of your control, such as in a third party library, which uses public properties for its dependencies. diff --git a/service_container/lazy_services.rst b/service_container/lazy_services.rst index bf45e100ef8..38d2f2186f0 100644 --- a/service_container/lazy_services.rst +++ b/service_container/lazy_services.rst @@ -23,7 +23,7 @@ until you interact with the proxy in some way. .. caution:: - Lazy services do not support `final`_ classes, but you can use + Lazy services do not support `final`_ or ``readonly`` classes, but you can use `Interface Proxifying`_ to work around this limitation. In PHP versions prior to 8.0 lazy services do not support parameters with @@ -82,7 +82,6 @@ You can mark the service as ``lazy`` by manipulating its definition: $services->set(AppExtension::class)->lazy(); }; - Once you inject the service into another service, a virtual `proxy`_ with the same signature of the class representing the service should be injected. The same happens when calling ``Container::get()`` directly. diff --git a/service_container/service_decoration.rst b/service_container/service_decoration.rst index 5d663fbc797..08bff60b534 100644 --- a/service_container/service_decoration.rst +++ b/service_container/service_decoration.rst @@ -309,7 +309,6 @@ the ``decoration_priority`` option. Its value is an integer that defaults to ->args([service('.inner')]); }; - The generated code will be the following:: $this->services[Foo::class] = new Baz(new Bar(new Foo())); diff --git a/service_container/service_subscribers_locators.rst b/service_container/service_subscribers_locators.rst index 1b152ac6d68..530519afd52 100644 --- a/service_container/service_subscribers_locators.rst +++ b/service_container/service_subscribers_locators.rst @@ -507,12 +507,15 @@ will share identical locators among all the services referencing them:: Indexing the Collection of Services ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Services passed to the service locator can define their own index using an -arbitrary attribute whose name is defined as ``index_by`` in the service locator. +By default, services passed to the service locator are indexed using their service +IDs. You can change this behavior with two options of the tagged locator (``index_by`` +and ``default_index_method``) which can be used independently or combined. -In the following example, the ``App\Handler\HandlerCollection`` locator receives -all services tagged with ``app.handler`` and they are indexed using the value -of the ``key`` tag attribute (as defined in the ``index_by`` locator option): +The ``index_by`` / ``indexAttribute`` Option +............................................ + +This option defines the name of the option/attribute that stores the value used +to index the services: .. configuration-block:: @@ -592,12 +595,13 @@ of the ``key`` tag attribute (as defined in the ``index_by`` locator option): $services->set(App\Handler\HandlerCollection::class) // inject all services tagged with app.handler as first argument - ->args([tagged_locator('app.handler', 'key')]) + ->args([tagged_locator('app.handler', indexAttribute: 'key')]) ; }; -Inside this locator you can retrieve services by index using the value of the -``key`` attribute. For example, to get the ``App\Handler\Two`` service:: +In this example, the ``index_by`` option is ``key``. All services define that +option/attribute, so that will be the value used to index the services. For example, +to get the ``App\Handler\Two`` service:: // src/Handler/HandlerCollection.php namespace App\Handler; @@ -608,31 +612,25 @@ Inside this locator you can retrieve services by index using the value of the { public function __construct(ServiceLocator $locator) { + // this value is defined in the `key` option of the service $handlerTwo = $locator->get('handler_two'); } // ... } -Instead of defining the index in the service definition, you can return its -value in a method called ``getDefaultIndexName()`` inside the class associated -to the service:: - - // src/Handler/One.php - namespace App\Handler; +If some service doesn't define the option/attribute configured in ``index_by``, +Symfony applies this fallback process: - class One - { - public static function getDefaultIndexName(): string - { - return 'handler_one'; - } +#. If the service class defines a static method called ``getDefaultName`` + (in this example, ``getDefaultKeyName()``), call it and use the returned value; +#. Otherwise, fall back to the default behavior and use the service ID. - // ... - } +The ``default_index_method`` Option +................................... -If you prefer to use another method name, add a ``default_index_method`` -attribute to the locator service defining the name of this custom method: +This option defines the name of the service class method that will be called to +get the value used to index the services: .. configuration-block:: @@ -647,7 +645,7 @@ attribute to the locator service defining the name of this custom method: class CommandBus { public function __construct( - #[TaggedLocator('app.handler', 'key', defaultIndexMethod: 'myOwnMethodName')] + #[TaggedLocator('app.handler', defaultIndexMethod: 'getLocatorKey')] ServiceLocator $locator ) { } @@ -659,8 +657,9 @@ attribute to the locator service defining the name of this custom method: services: # ... - App\HandlerCollection: - arguments: [!tagged_locator { tag: 'app.handler', index_by: 'key', default_index_method: 'myOwnMethodName' }] + App\Handler\HandlerCollection: + # inject all services tagged with app.handler as first argument + arguments: [!tagged_locator { tag: 'app.handler', default_index_method: 'getLocatorKey' }] .. code-block:: xml @@ -672,11 +671,11 @@ attribute to the locator service defining the name of this custom method: https://symfony.com/schema/dic/services/services-1.0.xsd"> - - + + @@ -687,17 +686,27 @@ attribute to the locator service defining the name of this custom method: namespace Symfony\Component\DependencyInjection\Loader\Configurator; return function(ContainerConfigurator $container) { - $container->services() - ->set(App\HandlerCollection::class) - ->args([tagged_locator('app.handler', 'key', 'myOwnMethodName')]) + $services = $container->services(); + // ... + + $services->set(App\Handler\HandlerCollection::class) + // inject all services tagged with app.handler as first argument + ->args([tagged_locator('app.handler', defaultIndexMethod: 'getLocatorKey')]) ; }; -.. note:: +If some service class doesn't define the method configured in ``default_index_method``, +Symfony will fall back to using the service ID as its index inside the locator. + +Combining the ``index_by`` and ``default_index_method`` Options +............................................................... + +You can combine both options in the same locator. Symfony will process them in +the following order: - Since code should not be responsible for defining how the locators are - going to be used, a configuration key (``key`` in the example above) must - be set so the custom method may be called as a fallback. +#. If the service defines the option/attribute configured in ``index_by``, use it; +#. If the service class defines the method configured in ``default_index_method``, use it; +#. Otherwise, fall back to using the service ID as its index inside the locator. .. _service-subscribers-service-subscriber-trait: diff --git a/service_container/shared.rst b/service_container/shared.rst index 435822fb25c..3dcad371400 100644 --- a/service_container/shared.rst +++ b/service_container/shared.rst @@ -11,6 +11,19 @@ in your service definition: .. configuration-block:: + .. code-block:: php-attributes + + // src/SomeNonSharedService.php + namespace App; + + use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + + #[Autoconfigure(shared: false)] + class SomeNonSharedService + { + // ... + } + .. code-block:: yaml # config/services.yaml diff --git a/service_container/synthetic_services.rst b/service_container/synthetic_services.rst index fc26c6848d3..c43a15034d0 100644 --- a/service_container/synthetic_services.rst +++ b/service_container/synthetic_services.rst @@ -71,7 +71,6 @@ configuration: ->synthetic(); }; - Now, you can inject the instance in the container using :method:`Container::set() `:: diff --git a/service_container/tags.rst b/service_container/tags.rst index 87f354434c2..18f22a9ffa8 100644 --- a/service_container/tags.rst +++ b/service_container/tags.rst @@ -44,7 +44,6 @@ example: ->tag('twig.extension'); }; - Services tagged with the ``twig.extension`` tag are collected during the initialization of TwigBundle and added to Twig as extensions. @@ -113,6 +112,11 @@ If you want to apply tags automatically for your own services, use the ->tag('app.custom_tag'); }; +.. caution:: + + If you're using PHP configuration, you need to call ``instanceof`` before + any service registration to make sure tags are correctly applied. + It is also possible to use the ``#[AutoconfigureTag]`` attribute directly on the base class or interface:: @@ -214,11 +218,64 @@ method:: } } +You can also make attributes usable on methods. To do so, update the previous +example and add ``Attribute::TARGET_METHOD``:: + + // src/Attribute/SensitiveElement.php + namespace App\Attribute; + + #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] + class SensitiveElement + { + // ... + } + +Then, update the :method:`Symfony\\Component\\DependencyInjection\\ContainerBuilder::registerAttributeForAutoconfiguration` +call to support ``ReflectionMethod``:: + + // src/Kernel.php + use App\Attribute\SensitiveElement; + + class Kernel extends BaseKernel + { + // ... + + protected function build(ContainerBuilder $container): void + { + // ... + + $container->registerAttributeForAutoconfiguration(SensitiveElement::class, static function ( + ChildDefinition $definition, + SensitiveElement $attribute, + // update the union type to support multiple types of reflection + // you can also use the "\Reflector" interface + \ReflectionClass|\ReflectionMethod $reflector): void { + if ($reflector instanceof \ReflectionMethod) { + // ... + } + } + ); + } + } + +.. tip:: + + You can also define an attribute to be usable on properties and parameters with + ``Attribute::TARGET_PROPERTY`` and ``Attribute::TARGET_PARAMETER``; then support + ``ReflectionProperty`` and ``ReflectionParameter`` in your + :method:`Symfony\\Component\\DependencyInjection\\ContainerBuilder::registerAttributeForAutoconfiguration` + callable. + .. versionadded:: 5.3 The :method:`Symfony\\Component\\DependencyInjection\\ContainerBuilder::registerAttributeForAutoconfiguration` method was introduced in Symfony 5.3. +.. versionadded:: 5.4 + + The support for autoconfigurable methods, properties and parameters was + introduced in Symfony 5.4. + Creating custom Tags -------------------- @@ -290,7 +347,6 @@ Then, define the chain as a service: $services->set(TransportChain::class); }; - Define Services with a Custom Tag ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -515,6 +571,51 @@ To answer this, change the service declaration: ; }; +.. tip:: + + The ``name`` attribute is used by default to define the name of the tag. + If you want to add a ``name`` attribute to some tag in XML or YAML formats, + you need to use this special syntax: + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + MailerSmtpTransport: + arguments: ['%mailer_host%'] + tags: + # this is a tag called 'app.mail_transport' + - { name: 'app.mail_transport', alias: 'smtp' } + # this is a tag called 'app.mail_transport' with two attributes ('name' and 'alias') + - app.mail_transport: { name: 'arbitrary-value', alias: 'smtp' } + + .. code-block:: xml + + + + + + + + %mailer_host% + + + + app.mail_transport + + + + + .. versionadded:: 5.1 + + The possibility to add the ``name`` attribute to a tag in XML and YAML + formats was introduced in Symfony 5.1. + .. tip:: In YAML format, you may provide the tag as a simple string as long as @@ -672,6 +773,14 @@ directly via PHP attributes: ; }; +.. note:: + + Some IDEs will show an error when using ``#[TaggedIterator]`` together + with the `PHP constructor promotion`_: + *"Attribute cannot be applied to a property because it does not contain the 'Attribute::TARGET_PROPERTY' flag"*. + The reason is that those constructor arguments are both parameters and class + properties. You can safely ignore this error message. + .. versionadded:: 5.3 The ``#[TaggedIterator]`` attribute was introduced in Symfony 5.3 and requires PHP 8. @@ -811,12 +920,15 @@ you can define it in the configuration of the collecting service: Tagged Services with Index ~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you want to retrieve a specific service within the injected collection -you can use the ``index_by`` and ``default_index_method`` options of the -argument in combination with ``!tagged_iterator``. +By default, tagged services are indexed using their service IDs. You can change +this behavior with two options of the tagged iterator (``index_by`` and +``default_index_method``) which can be used independently or combined. -Using the previous example, this service configuration creates a collection -indexed by the ``key`` attribute: +The ``index_by`` / ``indexAttribute`` Option +............................................ + +This option defines the name of the option/attribute that stores the value used +to index the services: .. configuration-block:: @@ -901,10 +1013,9 @@ indexed by the ``key`` attribute: ; }; -After compilation the ``HandlerCollection`` is able to iterate over your -application handlers. To retrieve a specific service from the iterator, call the -``iterator_to_array()`` function and then use the ``key`` attribute to get the -array element. For example, to retrieve the ``handler_two`` handler:: +In this example, the ``index_by`` option is ``key``. All services define that +option/attribute, so that will be the value used to index the services. For example, +to get the ``App\Handler\Two`` service:: // src/Handler/HandlerCollection.php namespace App\Handler; @@ -915,43 +1026,23 @@ array element. For example, to retrieve the ``handler_two`` handler:: { $handlers = $handlers instanceof \Traversable ? iterator_to_array($handlers) : $handlers; + // this value is defined in the `key` option of the service $handlerTwo = $handlers['handler_two']; } } -You can omit the index attribute (``key`` in the previous example) by setting -the ``index_by`` attribute on the ``tagged_iterator`` tag. In this case, you -must define a static method whose name follows the pattern: -``getDefaultName``. - -For example, if ``index_by`` is ``handler``, the method name must be -``getDefaultHandlerName()``: - -.. code-block:: yaml - - # config/services.yaml - services: - # ... - - App\HandlerCollection: - arguments: [!tagged_iterator { tag: 'app.handler', index_by: 'handler' }] +If some service doesn't define the option/attribute configured in ``index_by``, +Symfony applies this fallback process: -.. code-block:: php +#. If the service class defines a static method called ``getDefaultName`` + (in this example, ``getDefaultKeyName()``), call it and use the returned value; +#. Otherwise, fall back to the default behavior and use the service ID. - // src/Handler/One.php - namespace App\Handler; +The ``default_index_method`` Option +................................... - class One - { - // ... - public static function getDefaultHandlerName(): string - { - return 'handler_one'; - } - } - -You also can define the name of the static method to implement on each service -with the ``default_index_method`` attribute on the tagged argument: +This option defines the name of the service class method that will be called to +get the value used to index the services: .. configuration-block:: @@ -978,7 +1069,6 @@ with the ``default_index_method`` attribute on the tagged argument: # ... App\HandlerCollection: - # use getIndex() instead of getDefaultIndexName() arguments: [!tagged_iterator { tag: 'app.handler', default_index_method: 'getIndex' }] .. code-block:: xml @@ -994,7 +1084,6 @@ with the ``default_index_method`` attribute on the tagged argument: - set(HandlerCollection::class) ->args([ tagged_iterator('app.handler', null, 'getIndex'), @@ -1024,6 +1112,19 @@ with the ``default_index_method`` attribute on the tagged argument: ; }; +If some service class doesn't define the method configured in ``default_index_method``, +Symfony will fall back to using the service ID as its index inside the tagged services. + +Combining the ``index_by`` and ``default_index_method`` Options +............................................................... + +You can combine both options in the same collection of tagged services. Symfony +will process them in the following order: + +#. If the service defines the option/attribute configured in ``index_by``, use it; +#. If the service class defines the method configured in ``default_index_method``, use it; +#. Otherwise, fall back to using the service ID as its index inside the tagged services collection. + .. _tags_as-tagged-item: The ``#[AsTaggedItem]`` attribute @@ -1047,3 +1148,5 @@ be used directly on the class of the service you want to configure:: .. versionadded:: 5.3 The ``#[AsTaggedItem]`` attribute was introduced in Symfony 5.3. + +.. _`PHP constructor promotion`: https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion diff --git a/session.rst b/session.rst index 058c0984b8c..78f71b9d46d 100644 --- a/session.rst +++ b/session.rst @@ -110,13 +110,15 @@ By default, session attributes are key-value pairs managed with the :class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBag` class. -.. tip:: +Sessions are automatically started whenever you read, write or even check for +the existence of data in the session. This may hurt your application performance +because all users will receive a session cookie. In order to prevent starting +sessions for anonymous users, you must *completely* avoid accessing the session. + +.. note:: - Sessions are automatically started whenever you read, write or even check - for the existence of data in the session. This may hurt your application - performance because all users will receive a session cookie. In order to - prevent starting sessions for anonymous users, you must *completely* avoid - accessing the session. + Sessions will also be started when using features that rely on them internally, + such as the :ref:`CSRF protection in forms `. .. _flash-messages: @@ -169,19 +171,20 @@ For example, imagine you're processing a :doc:`form ` submission:: // add flash messages $flashes->add( - 'warning', - 'Your config file is writable, it should be set read-only' + 'notice', + 'Your changes were saved' ); - $flashes->add('error', 'Failed to update name'); - $flashes->add('error', 'Another error'); -After processing the request, the controller sets a flash message in the session -and then redirects. The message key (``notice`` in this example) can be anything: -you'll use this key to retrieve the message. +After processing the request, the controller sets a flash message in the +session and then redirects. The message key (``notice`` in this example) +can be anything. You'll use this key to retrieve the message. In the template of the next page (or even better, in your base layout template), read any flash messages from the session using the ``flashes()`` method provided -by the :ref:`Twig global app variable `: +by the :ref:`Twig global app variable `. +Alternatively, you can use the +:method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::peek` +method to retrieve the message while keeping it in the bag: .. configuration-block:: @@ -196,6 +199,13 @@ by the :ref:`Twig global app variable `: {% endfor %} + {# same but without clearing them from the flash bag #} + {% for message in app.session.flashbag.peek('notice') %} +
+ {{ message }} +
+ {% endfor %} + {# read and display several types of flash messages #} {% for label, messages in app.flashes(['success', 'warning']) %} {% for message in messages %} @@ -214,6 +224,15 @@ by the :ref:`Twig global app variable `: {% endfor %} {% endfor %} + {# or without clearing the flash bag #} + {% for label, messages in app.session.flashbag.peekAll() %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endfor %} + .. code-block:: php-standalone // display warnings @@ -221,6 +240,11 @@ by the :ref:`Twig global app variable `: echo '
'.$message.'
'; } + // display warnings without clearing them from the flash bag + foreach ($session->getFlashBag()->peek('warning', []) as $message) { + echo '
'.$message.'
'; + } + // display errors foreach ($session->getFlashBag()->get('error', []) as $message) { echo '
'.$message.'
'; @@ -233,16 +257,17 @@ by the :ref:`Twig global app variable `: } } + // display all flashes at once without clearing the flash bag + foreach ($session->getFlashBag()->peekAll() as $type => $messages) { + foreach ($messages as $message) { + echo '
'.$message.'
'; + } + } + It's common to use ``notice``, ``warning`` and ``error`` as the keys of the different types of flash messages, but you can use any key that fits your needs. -.. tip:: - - You can use the - :method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::peek` - method instead to retrieve the message while keeping it in the bag. - Configuration ------------- @@ -403,6 +428,15 @@ Check out the Symfony config reference to learn more about the other available ``session.auto_start = 1`` This directive should be turned off in ``php.ini``, in the web server directives or in ``.htaccess``. +The session cookie is also available in :ref:`the Response object `. +This is useful to get that cookie in the CLI context or when using PHP runners +like Roadrunner or Swoole. + +.. versionadded:: 5.4 + + Accessing to the session cookie in the ``Response`` object was introduced + in Symfony 5.4. + Session Idle Time/Keep Alive ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -532,7 +566,7 @@ a Symfony service for the connection to the Redis server: Redis: # you can also use \RedisArray, \RedisCluster or \Predis\Client classes - class: Redis + class: \Redis calls: - connect: - '%env(REDIS_HOST)%' @@ -667,6 +701,17 @@ and only the first one stored the CSRF token in the session. replace ``RedisSessionHandler`` by :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MemcachedSessionHandler`. +.. tip:: + + When using Redis with a DSN in the + :ref:`handler_id ` config option, you can + add the ``prefix`` and ``ttl`` options as query string parameters in the DSN. + + .. versionadded:: 5.4 + + The support for ``prefix`` and ``ttl`` options in a Redis DSN was + introduced in Symfony 5.4. + .. _session-database-pdo: Store Sessions in a Relational Database (MariaDB, MySQL, PostgreSQL) @@ -974,7 +1019,13 @@ working MongoDB connection in your Symfony application as explained in the `DoctrineMongoDBBundle configuration`_ article. Then, register a new handler service for ``MongoDbSessionHandler`` and pass it -the MongoDB connection as argument: +the MongoDB connection as argument, and the required parameters: + +``database``: + The name of the database + +``collection``: + The name of the collection .. configuration-block:: @@ -987,6 +1038,7 @@ the MongoDB connection as argument: Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler: arguments: - '@doctrine_mongodb.odm.default_connection' + - { database: '%env(MONGODB_DB)%', collection: 'sessions' } .. code-block:: xml @@ -1002,6 +1054,10 @@ the MongoDB connection as argument: doctrine_mongodb.odm.default_connection + + %env('MONGODB_DB')% + sessions + @@ -1019,6 +1075,7 @@ the MongoDB connection as argument: $services->set(MongoDbSessionHandler::class) ->args([ service('doctrine_mongodb.odm.default_connection'), + ['database' => '%env("MONGODB_DB")%', 'collection' => 'sessions'] ]) ; }; @@ -1068,13 +1125,6 @@ configuration option to tell Symfony to use this service as the session handler: ; }; -.. note:: - - MongoDB ODM 1.x only works with the legacy driver, which is no longer - supported by the Symfony session class. Install the ``alcaeus/mongo-php-adapter`` - package to retrieve the underlying ``\MongoDB\Client`` object or upgrade to - MongoDB ODM 2.0. - That's all! Symfony will now use your MongoDB server to read and write the session data. You do not need to do anything to initialize your session collection. However, you may want to add an index to improve garbage collection @@ -1103,7 +1153,11 @@ configure these values with the second argument passed to the Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler: arguments: - '@doctrine_mongodb.odm.default_connection' - - { id_field: '_guid', 'expiry_field': 'eol' } + - + database: '%env(MONGODB_DB)%' + collection: 'sessions' + id_field: '_guid' + expiry_field: 'eol' .. code-block:: xml @@ -1118,6 +1172,8 @@ configure these values with the second argument passed to the doctrine_mongodb.odm.default_connection + %env('MONGODB_DB')% + sessions _guid eol @@ -1138,7 +1194,12 @@ configure these values with the second argument passed to the $services->set(MongoDbSessionHandler::class) ->args([ service('doctrine_mongodb.odm.default_connection'), - ['id_field' => '_guid', 'expiry_field' => 'eol'], + [ + 'database' => '%env('MONGODB_DB')%', + 'collection' => 'sessions' + 'id_field' => '_guid', + 'expiry_field' => 'eol', + ], ]) ; }; @@ -1468,6 +1529,85 @@ library, but you can adapt it to any other library that you may be using:: } } +Another possibility to encrypt session data is to decorate the +``session.marshaller`` service, which points out to +:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MarshallingSessionHandler`. +You can decorate this handler with a marshaller that uses encryption, +like the :class:`Symfony\\Component\\Cache\\Marshaller\\SodiumMarshaller`. + +First, you need to generate a secure key and add it to your :doc:`secret +store ` as ``SESSION_DECRYPTION_FILE``: + +.. 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/services.yaml + services: + + # ... + Symfony\Component\Cache\Marshaller\SodiumMarshaller: + decorates: 'session.marshaller' + arguments: + - ['%env(file:resolve:SESSION_DECRYPTION_FILE)%'] + - '@.inner' + + .. code-block:: xml + + + + + + + + env(file:resolve:SESSION_DECRYPTION_FILE) + + + + + + + .. code-block:: php + + // config/services.php + use Symfony\Component\Cache\Marshaller\SodiumMarshaller; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + // ... + + return function(ContainerConfigurator $container) { + $services = $container->services(); + + // ... + + $services->set(SodiumMarshaller::class) + ->decorate('session.marshaller') + ->args([ + [env('file:resolve:SESSION_DECRYPTION_FILE')], + service('.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. + +.. versionadded:: 5.1 + + The :class:`Symfony\\Component\\Cache\\Marshaller\\SodiumMarshaller` + and :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MarshallingSessionHandler` + classes were introduced in Symfony 5.1. + Read-only Guest Sessions ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/setup.rst b/setup.rst index 9b7620d4164..2404c5c3738 100644 --- a/setup.rst +++ b/setup.rst @@ -20,9 +20,11 @@ Before creating your first Symfony application you must: * `Install Composer`_, which is used to install PHP packages. -Optionally, you can also `install Symfony CLI`_. This creates a binary called -``symfony`` that provides all the tools you need to develop and run your -Symfony application locally. +.. _setup-symfony-cli: + +Also, `install the Symfony CLI`_. This is optional, but it gives you a +helpful binary called ``symfony`` that provides all tools you need to +develop and run your Symfony application locally. The ``symfony`` binary also provides a tool to check if your computer meets all requirements. Open your console terminal and run this command: @@ -33,7 +35,7 @@ requirements. Open your console terminal and run this command: .. note:: - The Symfony CLI is written in Go and you can contribute to it in the + The Symfony CLI is open source, and you can contribute to it in the `symfony-cli/symfony-cli GitHub repository`_. .. _creating-symfony-applications: @@ -53,8 +55,8 @@ application: $ symfony new my_project_directory --version=5.4 The only difference between these two commands is the number of packages -installed by default. The ``--webapp`` option installs all the packages that you -usually need to build web applications, so the installation size will be bigger. +installed by default. The ``--webapp`` option installs extra packages to give +you everything you need to build a web application. If you're not using the Symfony binary, run these commands to create the new Symfony application using Composer: @@ -227,8 +229,8 @@ require --no-unpack ...`` option to disable unpacking. Checking Security Vulnerabilities --------------------------------- -The ``symfony`` binary created when you `install Symfony CLI`_ provides a command -to check whether your project's dependencies contain any known security +The ``symfony`` binary created when you installed the :ref:`Symfony CLI ` +provides a command to check whether your project's dependencies contain any known security vulnerability: .. code-block:: terminal @@ -312,7 +314,7 @@ Learn More .. _`Stellar Development with Symfony`: https://symfonycasts.com/screencast/symfony .. _`Install Composer`: https://getcomposer.org/download/ -.. _`install Symfony CLI`: https://symfony.com/download +.. _`install the Symfony CLI`: https://symfony.com/download .. _`symfony-cli/symfony-cli GitHub repository`: https://github.com/symfony-cli/symfony-cli .. _`The Symfony Demo Application`: https://github.com/symfony/demo .. _`Symfony Flex`: https://github.com/symfony/flex diff --git a/setup/docker.rst b/setup/docker.rst index 605afa64c19..c00192e08d4 100644 --- a/setup/docker.rst +++ b/setup/docker.rst @@ -19,7 +19,7 @@ Flex Recipes & Docker Configuration The :ref:`Flex recipe ` for some packages also include Docker configuration. For example, when you run ``composer require doctrine`` (to get ``symfony/orm-pack``), -your ``docker-compose.yml`` file will automatically be updated to include a +your ``compose.yaml`` file will automatically be updated to include a ``database`` service. The first time you install a recipe containing Docker config, Flex will ask you @@ -51,4 +51,10 @@ If you're using the :ref:`symfony binary web server ` then it can automatically detect your Docker services and expose them as environment variables. See :ref:`symfony-server-docker`. +.. note:: + + macOS users need to explicitly allow the default Docker socket to be used + for the Docker integration to work `as explained in the Docker documentation`_. + .. _`https://github.com/dunglas/symfony-docker`: https://github.com/dunglas/symfony-docker +.. _`as explained in the Docker documentation`: https://docs.docker.com/desktop/mac/permission-requirements/ diff --git a/setup/flex_private_recipes.rst b/setup/flex_private_recipes.rst index d4d3156466d..191dd6a4e02 100644 --- a/setup/flex_private_recipes.rst +++ b/setup/flex_private_recipes.rst @@ -224,11 +224,10 @@ computer, and execute the following command: .. code-block:: terminal - $ composer config --global --auth gitlab-oauth.gitlab.com [token] + $ composer config --global --auth gitlab-token.gitlab.com [token] Replace ``[token]`` with the value of your Gitlab personal access token. - Configure Your Project's ``composer.json`` File ----------------------------------------------- @@ -308,4 +307,3 @@ install the new private recipes, run the following command: .. _`release of version 1.16`: https://github.com/symfony/cli .. _`Symfony recipe files`: https://github.com/symfony/recipes/tree/flex/main - diff --git a/setup/symfony_server.rst b/setup/symfony_server.rst index c89a3e23f2a..f8b7c6e35c4 100644 --- a/setup/symfony_server.rst +++ b/setup/symfony_server.rst @@ -17,6 +17,17 @@ Installation The Symfony server is part of the ``symfony`` binary created when you `install Symfony`_ and has support for Linux, macOS and Windows. +.. tip:: + + The Symfony CLI supports auto completion for Bash, Zsh, or Fish shells. You + have to install the completion script *once*. Run ``symfony completion + --help`` for the installation instructions for your shell. After installing + and restarting your terminal, you're all set to use completion (by default, + by pressing the Tab key). + + The Symfony CLI will also provide completion for the ``composer`` command + and for the ``console`` command if it detects a Symfony project. + .. note:: You can view and contribute to the Symfony CLI source in the @@ -60,16 +71,12 @@ run the Symfony server in the background: On macOS, when starting the Symfony server you might see a warning dialog asking *"Do you want the application to accept incoming network connections?"*. - This happens when running unsigned appplications that are not listed in the + This happens when running unsigned applications that are not listed in the firewall list. The solution is to run this command that signs the Symfony binary: .. code-block:: terminal - # find the installed version of the Symfony binary - $ symfony version - - # change the path to the location of your Symfony binary and replace {version} too - $ sudo codesign --force --deep --sign - /opt/homebrew/Cellar/symfony-cli/{version}/bin/symfony + $ sudo codesign --force --deep --sign - $(whereis -q symfony) Enabling PHP-FPM ---------------- @@ -110,6 +117,20 @@ trust store, registers it in Firefox (this is required only for that browser) and creates a default certificate for ``localhost`` and ``127.0.0.1``. In other words, it does everything for you. +.. tip:: + + If you are doing this in WSL (Windows Subsystem for Linux), the newly created + local certificate authority needs to be manually imported in Windows. The file + is located in ``wsl`` at ``~/.symfony5/certs/default.p12``. The easiest way to + do so is to run the following command from ``wsl``: + + .. code-block:: terminal + + $ explorer.exe `wslpath -w $HOME/.symfony5/certs` + + In the file explorer window that just opened, double-click on the file + called ``default.p12``. + Before browsing your local application with HTTPS instead of HTTP, restart its server stopping and starting it again. @@ -209,6 +230,7 @@ If this is the first time you run the proxy, you must configure it as follows: * `Proxy settings in Ubuntu`_. #. Set the following URL as the value of the **Automatic Proxy Configuration**: + ``http://127.0.0.1:7080/proxy.pac`` Now run this command to start the proxy: @@ -226,6 +248,9 @@ If the proxy doesn't work as explained in the following sections, check these: * Some Operating Systems (e.g. macOS) don't apply by default the proxy settings to local hosts and domains. You may need to remove ``*.local`` and/or other IP addresses from that list. +* Windows Operating System **requires** ``localhost`` instead of ``127.0.0.1`` + when configuring the automatic proxy, otherwise you won't be able to access + your local domain from your browser running in Windows. Defining the Local Domain ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -269,7 +294,7 @@ domains work: # Example with Cypress $ https_proxy=$(symfony proxy:url) ./node_modules/bin/cypress open -.. note:: +.. caution:: Although env var names are always defined in uppercase, the ``https_proxy`` env var `is treated differently`_ than other env vars and its name must be @@ -292,7 +317,7 @@ server provides a ``run`` command to wrap them as follows: # compile Webpack assets using Symfony Encore ... but do that in the # background to not block the terminal - $ symfony run -d yarn encore dev --watch + $ symfony run -d npx encore dev --watch # continue working and running other commands... @@ -302,7 +327,7 @@ server provides a ``run`` command to wrap them as follows: # and you can also check if the command is still running $ symfony server:status Web server listening on ... - Command "yarn ..." running with PID ... + Command "npx ..." running with PID ... # stop the web server (and all the associated commands) when you are finished $ symfony server:stop @@ -310,11 +335,6 @@ server provides a ``run`` command to wrap them as follows: Configuration file ------------------ -.. caution:: - - This feature is experimental and could change or be removed at any time - without prior notice. - There are several options that you can set using a ``.symfony.local.yaml`` config file: .. code-block:: yaml @@ -335,6 +355,7 @@ There are several options that you can set using a ``.symfony.local.yaml`` confi no_tls: true # Use HTTP instead of HTTPS daemon: true # Run the server in the background use_gzip: true # Toggle GZIP compression + no_workers: true # Do not start workers .. caution:: @@ -342,6 +363,8 @@ There are several options that you can set using a ``.symfony.local.yaml`` confi using the ``proxy:domain:attach`` command for the current project when you start the server. +.. _symfony-server_configuring-workers: + Configuring Workers ~~~~~~~~~~~~~~~~~~~ @@ -353,9 +376,9 @@ If you like some processes to start automatically, along with the webserver # .symfony.local.yaml workers: # built-in command that builds and watches front-end assets - # yarn_encore_watch: - # cmd: ['yarn', 'encore', 'dev', '--watch'] - yarn_encore_watch: ~ + # npm_encore_watch: + # cmd: ['npx', 'encore', 'dev', '--watch'] + npm_encore_watch: ~ # built-in command that starts messenger consumer # messenger_consume_async: @@ -365,7 +388,15 @@ If you like some processes to start automatically, along with the webserver # you can also add your own custom commands build_spa: - cmd: ['yarn', '--cwd', './spa/', 'dev'] + cmd: ['npm', '--cwd', './spa/', 'dev'] + + # auto start Docker compose when starting server (available since Symfony CLI 5.7.0) + docker_compose: ~ + +.. tip:: + + You may want to not start workers on some environments like CI. You can use the + ``--no-workers`` option to start the server without starting workers. .. _symfony-server-docker: @@ -386,7 +417,7 @@ Consider the following configuration: .. code-block:: yaml - # docker-compose.yaml + # compose.yaml services: database: ports: [3306] @@ -399,12 +430,12 @@ variables accordingly with the service name (``database``) as a prefix: If the service is not in the supported list below, generic environment variables are set: ``PORT``, ``IP``, and ``HOST``. -If the ``docker-compose.yaml`` names do not match Symfony's conventions, add a +If the ``compose.yaml`` names do not match Symfony's conventions, add a label to override the environment variables prefix: .. code-block:: yaml - # docker-compose.yaml + # compose.yaml services: db: ports: [3306] @@ -469,7 +500,7 @@ check the "Symfony Server" section in the web debug toolbar; you'll see that .. code-block:: yaml - # docker-compose.yaml + # compose.yaml services: db: ports: [3306] @@ -483,10 +514,10 @@ its location, same as for ``docker-compose``: .. code-block:: bash # start your containers: - COMPOSE_FILE=docker/docker-compose.yaml COMPOSE_PROJECT_NAME=project_name docker-compose up -d + COMPOSE_FILE=docker/compose.yaml COMPOSE_PROJECT_NAME=project_name docker-compose up -d # run any Symfony CLI command: - COMPOSE_FILE=docker/docker-compose.yaml COMPOSE_PROJECT_NAME=project_name symfony var:export + COMPOSE_FILE=docker/compose.yaml COMPOSE_PROJECT_NAME=project_name symfony var:export .. note:: @@ -524,9 +555,9 @@ help debug any issues. .. _`symfony-cli/symfony-cli GitHub repository`: https://github.com/symfony-cli/symfony-cli .. _`Docker`: https://en.wikipedia.org/wiki/Docker_(software) .. _`Platform.sh`: https://symfony.com/cloud/ -.. _`Read Platform.sh for Symfony technical docs`: https://symfony.com/doc/master/cloud/intro.html +.. _`Read Platform.sh for Symfony technical docs`: https://symfony.com/doc/current/cloud/index.html .. _`Proxy settings in Windows`: https://www.dummies.com/computers/operating-systems/windows-10/how-to-set-up-a-proxy-in-windows-10/ .. _`Proxy settings in macOS`: https://support.apple.com/guide/mac-help/enter-proxy-server-settings-on-mac-mchlp2591/mac .. _`Proxy settings in Ubuntu`: https://help.ubuntu.com/stable/ubuntu-help/net-proxy.html.en -.. _`is treated differently`: https://ec.haxx.se/usingcurl/usingcurl-proxies#http_proxy-in-lower-case-only +.. _`is treated differently`: https://superuser.com/a/1799209 .. _`Docker compose CLI env var reference`: https://docs.docker.com/compose/reference/envvars/ diff --git a/setup/unstable_versions.rst b/setup/unstable_versions.rst index 6b30a0f785b..f8010440855 100644 --- a/setup/unstable_versions.rst +++ b/setup/unstable_versions.rst @@ -7,7 +7,6 @@ they are released as stable versions. Creating a New Project Based on an Unstable Symfony Version ----------------------------------------------------------- - Suppose that the Symfony 5.4 version hasn't been released yet and you want to create a new project to test its features. First, `install the Composer package manager`_. Then, open a command console, enter your project's directory and diff --git a/setup/upgrade_major.rst b/setup/upgrade_major.rst index ed81d4e21d7..7aa0d90c3b7 100644 --- a/setup/upgrade_major.rst +++ b/setup/upgrade_major.rst @@ -40,8 +40,8 @@ using a deprecated feature. When visiting your application in the in your browser, these notices are shown in the web dev toolbar: .. image:: /_images/install/deprecations-in-profiler.png - :align: center - :class: with-browser + :alt: The Logs page of the Symfony Profiler showing the deprecation notices. + :class: with-browser Ultimately, you should aim to stop using the deprecated functionality. Sometimes the warning might tell you exactly what to change. @@ -152,9 +152,10 @@ starting with ``symfony/`` to the new major version: + "symfony/console": "6.0.*", "...": "...", - "...": "A few libraries starting with - symfony/ follow their own versioning scheme. You - do not need to update these versions: you can + "...": "A few libraries starting with symfony/ follow their own + versioning scheme (e.g. symfony/polyfill-[...], + symfony/ux-[...], symfony/[...]-bundle). + You do not need to update these versions: you can upgrade them independently whenever you want", "symfony/monolog-bundle": "^3.5", }, @@ -170,17 +171,36 @@ this one. For instance, update it to ``6.0.*`` to upgrade to Symfony 6.0: "extra": { "symfony": { "allow-contrib": false, - - "require": "5.4.*" - + "require": "6.0.*" + - "require": "5.4.*" + + "require": "6.0.*" } } +.. tip:: + + If a more recent minor version is available (e.g. ``6.4``) you can use that + version directly and skip the older releases (``6.0``, ``6.1``, etc.). + Check the `maintained Symfony versions`_. + Next, use Composer to download new versions of the libraries: .. code-block:: terminal $ composer update "symfony/*" +A best practice after updating to a new major version is to clear the cache. +Instead of running the ``cache:clear`` command (which won't work if the application +is not bootable in the console after the upgrade) it's better to remove the entire +cache directory contents: + +.. code-block:: terminal + + # run this command on Linux and macOS + $ rm -rf var/cache/* + + # run this command on Windows + C:\> rmdir /s /q var\cache\* + .. include:: /setup/_update_dep_errors.rst.inc .. include:: /setup/_update_all_packages.rst.inc @@ -321,3 +341,4 @@ Classes in the ``vendor/`` directory are always ignored. .. _`PHP CS Fixer`: https://github.com/friendsofphp/php-cs-fixer .. _`Rector`: https://github.com/rectorphp/rector +.. _`maintained Symfony versions`: https://symfony.com/releases diff --git a/setup/web_server_configuration.rst b/setup/web_server_configuration.rst index 6723d0abaa3..e5a0c9e7fd9 100644 --- a/setup/web_server_configuration.rst +++ b/setup/web_server_configuration.rst @@ -36,7 +36,7 @@ listen on. Each pool can also be run under a different UID and GID: .. code-block:: ini - ; /etc/php/7.4/fpm/pool.d/www.conf + ; /etc/php/8.3/fpm/pool.d/www.conf ; a pool called www [www] @@ -44,7 +44,7 @@ listen on. Each pool can also be run under a different UID and GID: group = www-data ; use a unix domain socket - listen = /var/run/php/php7.4-fpm.sock + listen = /var/run/php/php8.3-fpm.sock ; or listen on a TCP connection ; listen = 127.0.0.1:9000 @@ -72,7 +72,7 @@ directive to pass requests for PHP files to PHP FPM: # when using PHP-FPM as a unix socket - SetHandler proxy:unix:/var/run/php/php7.4-fpm.sock|fcgi://dummy + SetHandler proxy:unix:/var/run/php/php8.3-fpm.sock|fcgi://dummy # when PHP-FPM is configured to use TCP # SetHandler proxy:fcgi://127.0.0.1:9000 @@ -95,6 +95,14 @@ directive to pass requests for PHP files to PHP FPM: CustomLog /var/log/apache2/project_access.log combined +.. note:: + + If you are doing some quick tests with Apache, you can also run + ``composer require symfony/apache-pack``. This package creates an ``.htaccess`` + file in the ``public/`` directory with the necessary rewrite rules needed to serve + the Symfony application. However, in production, it's recommended to move these + rules to the main Apache configuration file (as shown above) to improve performance. + Nginx ----- @@ -121,7 +129,7 @@ The **minimum configuration** to get your application running under Nginx is: location ~ ^/index\.php(/|$) { # when using PHP-FPM as a unix socket - fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; + fastcgi_pass unix:/var/run/php/php8.3-fpm.sock; # when PHP-FPM is configured to use TCP # fastcgi_pass 127.0.0.1:9000; @@ -197,9 +205,8 @@ When using Caddy on the server, you can use a configuration like this: encode zstd gzip file_server - # otherwise, use PHP-FPM (replace "unix//var/..." with "127.0.0.1:9000" when using TCP) - php_fastcgi unix//var/run/php/php7.4-fpm.sock { + php_fastcgi unix//var/run/php/php8.3-fpm.sock { # optionally set the value of the environment variables used in the application # env APP_ENV "prod" # env APP_SECRET "" diff --git a/templates.rst b/templates.rst index 47071654f54..dbdaf895c91 100644 --- a/templates.rst +++ b/templates.rst @@ -10,6 +10,16 @@ Twig: a flexible, fast, and secure template engine. Starting from Symfony 5.0, PHP templates are no longer supported. +Installation +------------ + +In applications using :ref:`Symfony Flex `, run the following command +to install both Twig language support and its integration with Symfony applications: + +.. code-block:: terminal + + $ composer require symfony/twig-bundle + .. _twig-language: Twig Templating Language @@ -592,7 +602,7 @@ Rendering a Template in Services Inject the ``twig`` Symfony service into your own services and use its ``render()`` method. When using :doc:`service autowiring ` you only need to add an argument in the service constructor and type-hint it with -the :class:`Twig\\Environment` class:: +the `Twig Environment`_:: // src/Service/SomeService.php namespace App\Service; @@ -1240,17 +1250,25 @@ and leaves the repeated contents and HTML structure to some parent templates. Read the `Twig template inheritance`_ docs to learn more about how to reuse parent block contents when overriding templates and other advanced features. -Output Escaping ---------------- +.. _output-escaping: +.. _xss-attacks: + +Output Escaping and XSS Attacks +------------------------------- Imagine that your template includes the ``Hello {{ name }}`` code to display the -user name. If a malicious user sets ```` as -their name and you output that value unchanged, the application will display a -JavaScript popup window. +user name and a malicious user sets the following as their name: + +.. code-block:: html -This is known as a `Cross-Site Scripting`_ (XSS) attack. And while the previous -example seems harmless, the attacker could write more advanced JavaScript code -to perform malicious actions. + My Name + + +You'll see ``My Name`` on screen but the attacker just secretly stole your cookies +so they can impersonate you on other websites. This is known as a `Cross-Site Scripting`_ +or XSS attack. To prevent this attack, use *"output escaping"* to transform the characters which have special meaning (e.g. replace ``<`` by the ``<`` HTML entity). @@ -1422,7 +1440,7 @@ Writing a Twig Extension `Twig Extensions`_ allow the creation of custom functions, filters, and more to use in your Twig templates. Before writing your own Twig extension, check if -the filter/function that you need is already implemented in: +the filter/function that you need is not already implemented in: * The `default Twig filters and functions`_; * The :doc:`Twig filters and functions added by Symfony `; @@ -1582,23 +1600,24 @@ If you're using the default ``services.yaml`` configuration, this will already work! Otherwise, :ref:`create a service ` for this class and :doc:`tag your service ` with ``twig.runtime``. -.. _`Twig`: https://twig.symfony.com -.. _`tags`: https://twig.symfony.com/doc/3.x/tags/index.html +.. _`Cross-Site Scripting`: https://en.wikipedia.org/wiki/Cross-site_scripting +.. _`default Twig filters and functions`: https://twig.symfony.com/doc/3.x/#reference .. _`filters`: https://twig.symfony.com/doc/3.x/filters/index.html .. _`functions`: https://twig.symfony.com/doc/3.x/functions/index.html -.. _`with_context`: https://twig.symfony.com/doc/3.x/functions/include.html -.. _`Twig template loader`: https://twig.symfony.com/doc/3.x/api.html#loaders -.. _`Twig raw filter`: https://twig.symfony.com/doc/3.x/filters/raw.html -.. _`Twig output escaping docs`: https://twig.symfony.com/doc/3.x/api.html#escaper-extension -.. _`snake case`: https://en.wikipedia.org/wiki/Snake_case -.. _`Twig template inheritance`: https://twig.symfony.com/doc/3.x/tags/extends.html -.. _`Twig block tag`: https://twig.symfony.com/doc/3.x/tags/block.html -.. _`Cross-Site Scripting`: https://en.wikipedia.org/wiki/Cross-site_scripting .. _`GitHub Actions`: https://docs.github.com/en/free-pro-team@latest/actions -.. _`UX Twig Component`: https://symfony.com/bundles/ux-twig-component/current/index.html -.. _`UX Live Component`: https://symfony.com/bundles/ux-live-component/current/index.html -.. _`Twig Extensions`: https://twig.symfony.com/doc/3.x/advanced.html#creating-an-extension -.. _`default Twig filters and functions`: https://twig.symfony.com/doc/3.x/#reference -.. _`official Twig extensions`: https://github.com/twigphp?q=extra .. _`global variables`: https://twig.symfony.com/doc/3.x/advanced.html#id1 .. _`hinclude.js`: https://mnot.github.io/hinclude/ +.. _`official Twig extensions`: https://github.com/twigphp?q=extra +.. _`snake case`: https://en.wikipedia.org/wiki/Snake_case +.. _`tags`: https://twig.symfony.com/doc/3.x/tags/index.html +.. _`Twig block tag`: https://twig.symfony.com/doc/3.x/tags/block.html +.. _`Twig Environment`: https://github.com/twigphp/Twig/blob/3.x/src/Environment.php +.. _`Twig Extensions`: https://twig.symfony.com/doc/3.x/advanced.html#creating-an-extension +.. _`Twig output escaping docs`: https://twig.symfony.com/doc/3.x/api.html#escaper-extension +.. _`Twig raw filter`: https://twig.symfony.com/doc/3.x/filters/raw.html +.. _`Twig template inheritance`: https://twig.symfony.com/doc/3.x/tags/extends.html +.. _`Twig template loader`: https://twig.symfony.com/doc/3.x/api.html#loaders +.. _`Twig`: https://twig.symfony.com +.. _`UX Live Component`: https://symfony.com/bundles/ux-live-component/current/index.html +.. _`UX Twig Component`: https://symfony.com/bundles/ux-twig-component/current/index.html +.. _`with_context`: https://twig.symfony.com/doc/3.x/functions/include.html diff --git a/testing.rst b/testing.rst index d3705a9984a..ae9a42b9b2c 100644 --- a/testing.rst +++ b/testing.rst @@ -97,7 +97,8 @@ You can run tests using the ``bin/phpunit`` command: .. tip:: In large test suites, it can make sense to create subdirectories for - each type of tests (e.g. ``tests/Unit/`` and ``tests/Functional/``). + each type of test (``tests/Unit/``, ``tests/Integration/``, + ``tests/Application/``, etc.). .. _integration-tests: @@ -225,7 +226,7 @@ need in your ``.env.test`` file: # .env.test # ... - DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name_test?serverVersion=5.7" + DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name_test?serverVersion=8.0.37" In the test environment, these env files are read (if vars are duplicated in them, files lower in the list override previous items): @@ -381,7 +382,7 @@ env var: .. code-block:: env # .env.test.local - DATABASE_URL="mysql://USERNAME:PASSWORD@127.0.0.1:3306/DB_NAME?serverVersion=5.7" + DATABASE_URL="mysql://USERNAME:PASSWORD@127.0.0.1:3306/DB_NAME?serverVersion=8.0.37" This assumes that each developer/machine uses a different database for the tests. If the test set-up is the same on each machine, use the ``.env.test`` @@ -398,6 +399,11 @@ After that, you can create the test database and all tables using: # create the tables/columns in the test database $ php bin/console --env=test doctrine:schema:create +.. tip:: + + You can run these commands to create the database during the + :doc:`test bootstrap process `. + .. tip:: A common practice is to append the ``_test`` suffix to the original @@ -595,13 +601,13 @@ returns a ``Crawler`` instance. The full signature of the ``request()`` method is:: - request( + public function request( string $method, string $uri, array $parameters = [], array $files = [], array $server = [], - string $content = null, + ?string $content = null, bool $changeHistory = true ): Crawler @@ -614,15 +620,50 @@ This allows you to create all types of requests you can think of: :ref:`framework.test ` option is enabled). This means you can override the service entirely if you need to. -.. caution:: +Multiple Requests in One Test +............................. + +After making a request, subsequent requests will make the client reboot the kernel. +This recreates the container from scratch to ensures that requests are isolated +and use new service objects each time. This behavior can have some unexpected +consequences: for example, the security token will be cleared, Doctrine entities +will be detached, etc. + +First, you can call the client's :method:`Symfony\\Bundle\\FrameworkBundle\\KernelBrowser::disableReboot` +method to reset the kernel instead of rebooting it. In practice, Symfony +will call the ``reset()`` method of every service tagged with ``kernel.reset``. +However, this will **also** clear the security token, detach Doctrine entities, etc. + +In order to solve this issue, create a :doc:`compiler pass ` +to remove the ``kernel.reset`` tag from some services in your test environment:: + + // src/Kernel.php + namespace App; + + use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; + use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\HttpKernel\Kernel as BaseKernel; + + class Kernel extends BaseKernel implements CompilerPassInterface + { + use MicroKernelTrait; + + // ... + + public function process(ContainerBuilder $container): void + { + if ('test' === $this->environment) { + // prevents the security token to be cleared + $container->getDefinition('security.token_storage')->clearTag('kernel.reset'); - Before each request, the client reboots the kernel, recreating - the container from scratch. - This ensures that every requests are "isolated" using "new" service objects. - Also, it means that entities loaded by Doctrine repositories will - be "detached", so they will need to be refreshed by the manager or - queried again from a repository. - You can disable this behavior by calling the :method:`disableReboot() ` method. + // prevents Doctrine entities to be detached + $container->getDefinition('doctrine')->clearTag('kernel.reset'); + + // ... + } + } + } Browsing the Site ................. @@ -718,6 +759,10 @@ You can pass any :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\TestBrowserToken` object and stores in the session of the test client. +To set a specific firewall (``main`` is set by default):: + + $client->loginUser($testUser, 'my_firewall'); + .. note:: By design, the ``loginUser()`` method doesn't work when using stateless firewalls. @@ -876,7 +921,7 @@ The second optional argument is used to override the default form field values. If you need access to the :class:`Symfony\\Component\\DomCrawler\\Form` object that provides helpful methods specific to forms (such as ``getUri()``, -``getValues()`` and ``getFields()``) use the ``Crawler::selectButton()`` method instead:: +``getValues()`` and ``getFiles()``) use the ``Crawler::selectButton()`` method instead:: $client = static::createClient(); $crawler = $client->request('GET', '/post/hello-world'); @@ -967,7 +1012,7 @@ Response Assertions Asserts that the response was successful (HTTP status is 2xx). ``assertResponseStatusCodeSame(int $expectedCode, string $message = '')`` Asserts a specific HTTP status code. -``assertResponseRedirects(string $expectedLocation = null, int $expectedCode = null, string $message = '')`` +``assertResponseRedirects(?string $expectedLocation = null, ?int $expectedCode = null, string $message = '')`` Asserts the response is a redirect response (optionally, you can check the target location and status code). ``assertResponseHasHeader(string $headerName, string $message = '')``/``assertResponseNotHasHeader(string $headerName, string $message = '')`` @@ -975,10 +1020,10 @@ Response Assertions ``assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = '')``/``assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = '')`` Asserts the given header does (not) contain the expected value on the response, e.g. ``assertResponseHeaderSame('content-type', 'application/octet-stream');``. -``assertResponseHasCookie(string $name, string $path = '/', string $domain = null, string $message = '')``/``assertResponseNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = '')`` +``assertResponseHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')``/``assertResponseNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')`` Asserts the given cookie is present in the response (optionally checking for a specific cookie path or domain). -``assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', string $domain = null, string $message = '')`` +``assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', ?string $domain = null, string $message = '')`` Asserts the given cookie is present and set to the expected value. ``assertResponseFormatSame(?string $expectedFormat, string $message = '')`` Asserts the response format returned by the @@ -1007,10 +1052,10 @@ Request Assertions Browser Assertions .................. -``assertBrowserHasCookie(string $name, string $path = '/', string $domain = null, string $message = '')``/``assertBrowserNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = '')`` +``assertBrowserHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')``/``assertBrowserNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')`` Asserts that the test Client does (not) have the given cookie set (meaning, the cookie was set by any response in the test). -``assertBrowserCookieValueSame(string $name, string $expectedValue, string $path = '/', string $domain = null, string $message = '')`` +``assertBrowserCookieValueSame(string $name, string $expectedValue, string $path = '/', ?string $domain = null, string $message = '')`` Asserts the given cookie in the test Client is set to the expected value. ``assertThatForClient(Constraint $constraint, string $message = '')`` @@ -1068,18 +1113,18 @@ Mailer Assertions Starting from Symfony 5.1, the following assertions no longer require to make a request with the ``Client`` in a test case extending the ``WebTestCase`` class. -``assertEmailCount(int $count, string $transport = null, string $message = '')`` +``assertEmailCount(int $count, ?string $transport = null, string $message = '')`` Asserts that the expected number of emails was sent. -``assertQueuedEmailCount(int $count, string $transport = null, string $message = '')`` +``assertQueuedEmailCount(int $count, ?string $transport = null, string $message = '')`` Asserts that the expected number of emails was queued (e.g. using the Messenger component). ``assertEmailIsQueued(MessageEvent $event, string $message = '')``/``assertEmailIsNotQueued(MessageEvent $event, string $message = '')`` Asserts that the given mailer event is (not) queued. Use - ``getMailerEvent(int $index = 0, string $transport = null)`` to + ``getMailerEvent(int $index = 0, ?string $transport = null)`` to retrieve a mailer event by index. ``assertEmailAttachmentCount(RawMessage $email, int $count, string $message = '')`` Asserts that the given email has the expected number of attachments. Use - ``getMailerMessage(int $index = 0, string $transport = null)`` to + ``getMailerMessage(int $index = 0, ?string $transport = null)`` to retrieve a specific email by index. ``assertEmailTextBodyContains(RawMessage $email, string $text, string $message = '')``/``assertEmailTextBodyNotContains(RawMessage $email, string $text, string $message = '')`` Asserts that the text body of the given email does (not) contain the diff --git a/testing/database.rst b/testing/database.rst index 64095eec01b..6c337ee07a3 100644 --- a/testing/database.rst +++ b/testing/database.rst @@ -1,4 +1,4 @@ -How to Test A Doctrine Repository +How to Test a Doctrine Repository ================================= .. seealso:: @@ -89,7 +89,7 @@ the employee which gets returned by the ``Repository``, which itself gets returned by the ``EntityManager``. This way, no real class is involved in testing. -Functional Testing of A Doctrine Repository +Functional Testing of a Doctrine Repository ------------------------------------------- In :ref:`functional tests ` you'll make queries to the diff --git a/translation.rst b/translation.rst index 6ea797c27f9..3004c68d991 100644 --- a/translation.rst +++ b/translation.rst @@ -33,8 +33,8 @@ The translation process has several steps: #. :ref:`Enable and configure ` Symfony's translation service; -#. Abstract strings (i.e. "messages") by wrapping them in calls to the - ``Translator`` (":ref:`translation-basic`"); +#. Abstract strings (i.e. "messages") by :ref:`wrapping them in calls + ` to the ``Translator``; #. :ref:`Create translation resources/files ` for each supported locale that translate each message in the application; @@ -84,10 +84,9 @@ are located: https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - '%kernel.project_dir%/translations' - - + @@ -165,8 +164,8 @@ different formats: 'Symfony is great' => "J'aime Symfony", ]; -For information on where these files should be located, see -:ref:`translation-resource-locations`. +You can find more information on where these files +:ref:`should be located `. 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 Symfony``. You can also translate @@ -252,8 +251,8 @@ To actually translate the message, Symfony uses the following process when using the ``trans()`` method: #. The ``locale`` of the current user, which is stored on the request is - determined; this is typically set via a ``_locale`` attribute on your routes - (see :ref:`translation-locale-url`); + determined; this is typically set via a ``_locale`` :ref:`attribute on + your routes `; #. A catalog of translated messages is loaded from translation resources defined for the ``locale`` (e.g. ``fr_FR``). Messages from the @@ -453,14 +452,23 @@ The ``translation:extract`` command looks for missing translations in: * Any PHP file/class that injects or :doc:`autowires ` the ``translator`` service and makes calls to the ``trans()`` method. * Any PHP file/class stored in the ``src/`` directory that creates - :ref:`translatable-objects` using the constructor or the ``t()`` method or calls - the ``trans()`` method. + :ref:`translatable objects ` using the constructor or + the ``t()`` method or calls the ``trans()`` method. .. versionadded:: 5.3 Support for extracting Translatable objects has been introduced in Symfony 5.3. +By default, when the ``translation:extract`` command creates new entries in the +translation file, it uses the same content as both the source and the pending +translation. The only difference is that the pending translation is prefixed by +``__``. You can customize this prefix using the ``--prefix`` option: + +.. code-block:: terminal + + $ php bin/console translation:extract --force --prefix="NEW_" fr + .. _translation-resource-locations: Translation Resource/File Names and Locations @@ -562,16 +570,21 @@ if you're generating translations with specialized programs or teams. ; }; -.. note:: +Translations of Doctrine Entities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Unlike the contents of templates, it's not practical to translate the contents +stored in Doctrine Entities using translation catalogs. Instead, use the +Doctrine `Translatable Extension`_ or the `Translatable Behavior`_. For more +information, read the documentation of those libraries. - You can also store translations in a database; it can be handled by - Doctrine through the `Translatable Extension`_ or the `Translatable Behavior`_ - (PHP 5.4+). For more information, see the documentation for these libraries. +Custom Translation Resources +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - For any other storage, you need to provide a custom class implementing the - :class:`Symfony\\Component\\Translation\\Loader\\LoaderInterface` - interface. See the :ref:`dic-tags-translation-loader` tag for more - information. +If your translations use a format not supported by Symfony or you store them +in a special way (e.g. not using files or Doctrine entities), you need to provide +a custom class implementing the :class:`Symfony\\Component\\Translation\\Loader\\LoaderInterface` +interface. See the :ref:`dic-tags-translation-loader` tag for more information. .. _translation-providers: @@ -597,13 +610,13 @@ Installing and Configuring a Third Party Provider Before pushing/pulling translations to a third-party provider, you must install the package that provides integration with that provider: -==================== =========================================================== -Provider Install with -==================== =========================================================== -Crowdin ``composer require symfony/crowdin-translation-provider`` -Loco (localise.biz) ``composer require symfony/loco-translation-provider`` -Lokalise ``composer require symfony/lokalise-translation-provider`` -==================== =========================================================== +====================== =========================================================== +Provider Install with +====================== =========================================================== +`Crowdin`_ ``composer require symfony/crowdin-translation-provider`` +`Loco (localise.biz)`_ ``composer require symfony/loco-translation-provider`` +`Lokalise`_ ``composer require symfony/lokalise-translation-provider`` +====================== =========================================================== Each library includes a :ref:`Symfony Flex recipe ` that will add a configuration example to your ``.env`` file. For example, suppose you want to @@ -628,13 +641,13 @@ pull translations via Loco. The *only* part you need to change is the This table shows the full list of available DSN formats for each provider: -===================== ========================================================== -Provider DSN -===================== ========================================================== -Crowdin crowdin://PROJECT_ID:API_TOKEN@ORGANIZATION_DOMAIN.default -Loco (localise.biz) loco://API_KEY@default -Lokalise lokalise://PROJECT_ID:API_KEY@default -===================== ========================================================== +====================== ============================================================== +Provider DSN +====================== ============================================================== +`Crowdin`_ ``crowdin://PROJECT_ID:API_TOKEN@ORGANIZATION_DOMAIN.default`` +`Loco (localise.biz)`_ ``loco://API_KEY@default`` +`Lokalise`_ ``lokalise://PROJECT_ID:API_KEY@default`` +====================== ============================================================== To enable a translation provider, customize the DSN in your ``.env`` file and configure the ``providers`` option: @@ -742,6 +755,19 @@ now use the following commands to push (upload) and pull (download) translations # check out the command help to see its options (format, domains, locales, intl-icu, etc.) $ php bin/console translation:pull --help +Creating Custom Providers +~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to using Symfony's built-in translation providers, you can create +your own providers. To do so, you need to create two classes: + +#. The first class must implement :class:`Symfony\\Component\\Translation\\Provider\\ProviderInterface`; +#. The second class needs to be a factory which will create instances of the first class. It must implement +:class:`Symfony\\Component\\Translation\\Provider\\ProviderFactoryInterface` (you can extend :class:`Symfony\\Component\\Translation\\Provider\\AbstractProviderFactory` to simplify its creation). + +After creating these two classes, you need to register your factory as a service +and tag it with :ref:`translation.provider_factory `. + .. _translation-locale: Handling the User's Locale @@ -1037,10 +1063,10 @@ unused translation messages templates: .. caution:: The extractors can't find messages translated outside templates (like form - labels or controllers) unless using :ref:`translatable-objects` or calling - the ``trans()`` method on a translator (since Symfony 5.3). Dynamic - translations using variables or expressions in templates are not - detected either: + labels or controllers) unless using :ref:`translatable objects + ` or calling the ``trans()`` method on a translator + (since Symfony 5.3). Dynamic translations using variables or expressions in + templates are not detected either: .. code-block:: twig @@ -1049,9 +1075,10 @@ unused translation messages templates: {{ message|trans }} Suppose your application's default_locale is ``fr`` and you have configured -``en`` as the fallback locale (see :ref:`translation-configuration` and -:ref:`translation-fallback` for how to configure these). And suppose -you've already setup some translations for the ``fr`` locale: +``en`` as the fallback locale (see :ref:`configuration +` and :ref:`fallback ` for +how to configure these). And suppose you've already set up some translations +for the ``fr`` locale: .. configuration-block:: @@ -1300,16 +1327,17 @@ Pseudo-localization translator The pseudolocalization translator is meant to be used for development only. -The following image shows the main menu of the interface of a popular Internet -service: +The following image shows a typical menu on a webpage: .. image:: /_images/translation/pseudolocalization-interface-original.png + :alt: A menu showing multiple items nicely aligned next to eachother. This other image shows the same menu when the user switches the language to Spanish. Unexpectedly, some text is cut and other contents are so long that they overflow and you can't see them: .. image:: /_images/translation/pseudolocalization-interface-translated.png + :alt: In Spanish, some menu items contain more letters which result in them being cut. These kind of errors are very common, because different languages can be longer or shorter than the original application language. Another common issue is to @@ -1414,10 +1442,14 @@ readable, contents to help you internationalize it. See for example the difference in the `Symfony Demo`_ application. This is the original page: .. image:: /_images/translation/pseudolocalization-symfony-demo-disabled.png + :alt: The Symfony demo login page. + :class: with-browser And this is the same page with pseudolocalization enabled: .. image:: /_images/translation/pseudolocalization-symfony-demo-enabled.png + :alt: The Symfony demo login page with pseudolocalization. + :class: with-browser Summary ------- @@ -1454,3 +1486,6 @@ Learn more .. _`GitHub Actions`: https://docs.github.com/en/free-pro-team@latest/actions .. _`pseudolocalization`: https://en.wikipedia.org/wiki/Pseudolocalization .. _`Symfony Demo`: https://github.com/symfony/demo +.. _`Crowdin`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Translation/Bridge/Crowdin/README.md +.. _`Loco (localise.biz)`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Translation/Bridge/Loco/README.md +.. _`Lokalise`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Translation/Bridge/Lokalise/README.md diff --git a/validation.rst b/validation.rst index 33b578216dd..8a68f29391a 100644 --- a/validation.rst +++ b/validation.rst @@ -730,6 +730,20 @@ 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. +Validating Object With Inheritance +---------------------------------- + +When you validate an object that extends another class, the validator +automatically validates constraints defined in the parent class as well. + +**The constraints defined in the parent properties will be applied to the child +properties even if the child properties override those constraints**. Symfony +will always merge the parent constraints for each property. + +You can't change this behavior, but you can overcome it by defining the parent +and the child constraints in different :doc:`validation groups ` +and then select the appropriate group when validating each object. + Debugging the Constraints ------------------------- diff --git a/validation/custom_constraint.rst b/validation/custom_constraint.rst index 9200e0d9dec..549de6e3234 100644 --- a/validation/custom_constraint.rst +++ b/validation/custom_constraint.rst @@ -41,7 +41,16 @@ First you need to create a Constraint class and extend :class:`Symfony\\Componen class ContainsAlphanumeric extends Constraint { public $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.'; - public $mode = 'strict'; // If the constraint has configuration options, define them as public properties + public $mode = 'strict'; + + // all configurable options must be passed to the constructor + public function __construct(?string $mode = null, ?string $message = null, ?array $groups = null, $payload = null) + { + parent::__construct([], $groups, $payload); + + $this->mode = $mode ?? $this->mode; + $this->message = $message ?? $this->message; + } } Add ``@Annotation`` or ``#[\Attribute]`` to the constraint class if you want to @@ -108,12 +117,14 @@ The validator class only has one required method ``validate()``:: // ... } - if (!preg_match('/^[a-zA-Z0-9]+$/', $value, $matches)) { - // the argument must be a string or an object implementing __toString() - $this->context->buildViolation($constraint->message) - ->setParameter('{{ string }}', $value) - ->addViolation(); + if (preg_match('/^[a-zA-Z0-9]+$/', $value, $matches)) { + return; } + + // the argument must be a string or an object implementing __toString() + $this->context->buildViolation($constraint->message) + ->setParameter('{{ string }}', $value) + ->addViolation(); } } @@ -233,6 +244,231 @@ then your validator is already registered as a service and :doc:`tagged ` like any other service. +Constraint Validators with Custom Options +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to add some configuration options to your custom constraint, first +define those options as public properties on the constraint class: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Validator/Foo.php + namespace App\Validator; + + use Symfony\Component\Validator\Constraint; + + /** + * @Annotation + */ + class Foo extends Constraint + { + public $mandatoryFooOption; + public $message = 'This value is invalid'; + public $optionalBarOption = false; + + public function __construct( + $mandatoryFooOption, + ?string $message = null, + ?bool $optionalBarOption = null, + ?array $groups = null, + $payload = null, + array $options = [] + ) { + if (\is_array($mandatoryFooOption)) { + $options = array_merge($mandatoryFooOption, $options); + } elseif (null !== $mandatoryFooOption) { + $options['value'] = $mandatoryFooOption; + } + + parent::__construct($options, $groups, $payload); + + $this->message = $message ?? $this->message; + $this->optionalBarOption = $optionalBarOption ?? $this->optionalBarOption; + } + + public function getDefaultOption() + { + // If no associative array is passed to the constructor this + // property is set instead. + + return 'mandatoryFooOption'; + } + + public function getRequiredOptions() + { + // return names of options which must be set. + + return ['mandatoryFooOption']; + } + } + + .. code-block:: php-attributes + + // src/Validator/Foo.php + namespace App\Validator; + + use Symfony\Component\Validator\Constraint; + + #[\Attribute] + class Foo extends Constraint + { + public $mandatoryFooOption; + public $message = 'This value is invalid'; + public $optionalBarOption = false; + + public function __construct( + $mandatoryFooOption, + ?string $message = null, + ?bool $optionalBarOption = null, + ?array $groups = null, + $payload = null, + array $options = [] + ) { + if (\is_array($mandatoryFooOption)) { + $options = array_merge($mandatoryFooOption, $options); + } elseif (null !== $mandatoryFooOption) { + $options['value'] = $mandatoryFooOption; + } + + parent::__construct($options, $groups, $payload); + + $this->message = $message ?? $this->message; + $this->optionalBarOption = $optionalBarOption ?? $this->optionalBarOption; + } + + public function getDefaultOption() + { + return 'mandatoryFooOption'; + } + + public function getRequiredOptions() + { + return ['mandatoryFooOption']; + } + } + +Then, inside the validator class you can access these options directly via the +constraint class passes to the ``validate()`` method:: + + class FooValidator extends ConstraintValidator + { + public function validate($value, Constraint $constraint) + { + // access any option of the constraint + if ($constraint->optionalBarOption) { + // ... + } + + // ... + } + } + +When using this constraint in your own application, you can pass the value of +the custom options like you pass any other option in built-in constraints: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Entity/AcmeEntity.php + namespace App\Entity; + + use App\Validator as AcmeAssert; + use Symfony\Component\Validator\Constraints as Assert; + + class AcmeEntity + { + // ... + + /** + * @Assert\NotBlank + * @AcmeAssert\Foo( + * mandatoryFooOption="bar", + * optionalBarOption=true + * ) + */ + protected $name; + + // ... + } + + .. code-block:: php-attributes + + // src/Entity/AcmeEntity.php + namespace App\Entity; + + use App\Validator as AcmeAssert; + use Symfony\Component\Validator\Constraints as Assert; + + class AcmeEntity + { + // ... + + #[Assert\NotBlank] + #[AcmeAssert\Foo( + mandatoryFooOption: 'bar', + optionalBarOption: true + )] + protected $name; + + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\AcmeEntity: + properties: + name: + - NotBlank: ~ + - App\Validator\Foo: + mandatoryFooOption: bar + optionalBarOption: true + + .. code-block:: xml + + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Entity/AcmeEntity.php + namespace App\Entity; + + use App\Validator\ContainsAlphanumeric; + use Symfony\Component\Validator\Constraints\NotBlank; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class AcmeEntity + { + public $name; + + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addPropertyConstraint('name', new NotBlank()); + $metadata->addPropertyConstraint('name', new Foo([ + 'mandatoryFooOption' => 'bar', + 'optionalBarOption' => true, + ])); + } + } + Create a Reusable Set of Constraints ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -388,7 +624,7 @@ A class constraint validator must be applied to the class itself: Testing Custom Constraints -------------------------- -Use the :class:`Symfony\\Component\\Validator\\Test\\ConstraintValidatorTestCase`` +Use the :class:`Symfony\\Component\\Validator\\Test\\ConstraintValidatorTestCase` class to simplify writing unit tests for your custom constraints:: // tests/Validator/ContainsAlphanumericValidatorTest.php @@ -424,7 +660,7 @@ class to simplify writing unit tests for your custom constraints:: ->assertRaised(); } - public function provideInvalidConstraints(): iterable + public function provideInvalidConstraints(): \Generator { yield [new ContainsAlphanumeric(message: 'myMessage')]; // ... diff --git a/validation/translations.rst b/validation/translations.rst index 3f7f461aacd..721273562c1 100644 --- a/validation/translations.rst +++ b/validation/translations.rst @@ -135,5 +135,64 @@ Now, create a ``validators`` catalog file in the ``translations/`` directory: 'author.name.not_blank' => 'Please enter an author name.', ]; -You may need to clear your cache (even in the dev environment) after creating this -file for the first time. +You may need to clear your cache (even in the dev environment) after creating +this file for the first time. + +Custom Translation Domain +------------------------- + +The default translation domain can be changed globally using the +``FrameworkBundle`` configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/validator.yaml + framework: + validation: + translation_domain: validation_errors + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/validator.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + // ... + $framework + ->validation() + ->translationDomain('validation_errors') + ; + }; + +Or it can be customized for a specific violation from a constraint validator:: + + public function validate($value, Constraint $constraint): void + { + // validation logic + + $this->context->buildViolation($constraint->message) + ->setParameter('{{ string }}', $value) + ->setTranslationDomain('validation_errors') + ->addViolation(); + } diff --git a/web_link.rst b/web_link.rst index fb81376cba3..c19164db572 100644 --- a/web_link.rst +++ b/web_link.rst @@ -19,6 +19,16 @@ servers (Apache, nginx, Caddy, etc.) support this, but you can also use the `Docker installer and runtime for Symfony`_ created by Kévin Dunglas, from the Symfony community. +Installation +------------ + +In applications using :ref:`Symfony Flex `, run the following command +to install the WebLink feature before using it: + +.. code-block:: terminal + + $ composer require symfony/web-link + Preloading Assets ----------------- diff --git a/workflow.rst b/workflow.rst index 02abd4da037..f3f04b3feea 100644 --- a/workflow.rst +++ b/workflow.rst @@ -33,6 +33,7 @@ step or stage in the process is called a *place*. You also define *transitions*, which describe the action needed to get from one place to another. .. image:: /_images/components/workflow/states_transitions.png + :alt: An example state diagram for a workflow, showing transitions and places. A set of places and transitions creates a **definition**. A workflow needs a ``Definition`` and a way to write the states to the objects (i.e. an @@ -189,6 +190,9 @@ The configured property will be used via its implemented getter/setter methods b { $this->currentPlace = $currentPlace; } + + // you don't need to set the initial marking in the constructor or any other method; + // this is configured in the workflow with the 'initial_marking' option } .. note:: @@ -225,6 +229,8 @@ what actions are allowed on a blog post:: use Symfony\Component\Workflow\Exception\LogicException; $post = new BlogPost(); + // you don't need to set the initial marking with code; this is configured + // in the workflow with the 'initial_marking' option $workflow = $this->container->get('workflow.blog_publishing'); $workflow->can($post, 'publish'); // False @@ -242,6 +248,41 @@ what actions are allowed on a blog post:: // See a specific available transition for the post in the current state $transition = $workflow->getEnabledTransition($post, 'publish'); +Using a multiple state marking store +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you are creating a :doc:`workflow `, +your marking store may need to contain multiple places at the same time. That's why, +if you are using Doctrine, the matching column definition should use the type ``json``:: + + // src/Entity/BlogPost.php + namespace App\Entity; + + use Doctrine\DBAL\Types\Types; + use Doctrine\ORM\Mapping as ORM; + + #[ORM\Entity] + class BlogPost + { + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private int $id; + + #[ORM\Column(type: Types::JSON)] + private array $currentPlaces; + + // ... + } + +.. caution:: + + You should not use the type ``simple_array`` for your marking store. Inside + a multiple state marking store, places are stored as keys with a value of one, + such as ``['draft' => 1]``. If the marking store contains only one place, + this Doctrine type will store its value only as a string, resulting in the + loss of the object's current place. + Accessing the Workflow in a Class --------------------------------- @@ -807,6 +848,89 @@ place:: } } +Creating Your Own Marking Store +------------------------------- + +You may need to implement your own store to execute some additional logic +when the marking is updated. For example, you may have some specific needs +to store the marking on certain workflows. To do this, you need to implement +the +:class:`Symfony\\Component\\Workflow\\MarkingStore\\MarkingStoreInterface`:: + + namespace App\Workflow\MarkingStore; + + use Symfony\Component\Workflow\Marking; + use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface; + + final class BlogPostMarkingStore implements MarkingStoreInterface + { + /** + * @param BlogPost $subject + */ + public function getMarking(object $subject): Marking + { + return new Marking([$subject->getCurrentPlace() => 1]); + } + + /** + * @param BlogPost $subject + */ + public function setMarking(object $subject, Marking $marking, array $context = []): void + { + $marking = key($marking->getPlaces()); + $subject->setCurrentPlace($marking); + } + } + +Once your marking store is implemented, you can configure your workflow to use +it: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/workflow.yaml + framework: + workflows: + blog_publishing: + # ... + marking_store: + service: 'App\Workflow\MarkingStore\BlogPostMarkingStore' + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/workflow.php + use App\Workflow\MarkingStore\ReflectionMarkingStore; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + + $blogPublishing = $framework->workflows()->workflows('blog_publishing'); + // ... + + $blogPublishing->markingStore() + ->service(BlogPostMarkingStore::class); + }; + Usage in Twig ------------- diff --git a/workflow/dumping-workflows.rst b/workflow/dumping-workflows.rst index d4d6adc3a74..d06c83edae5 100644 --- a/workflow/dumping-workflows.rst +++ b/workflow/dumping-workflows.rst @@ -36,14 +36,17 @@ to dump it as an image: The DOT image will look like this: .. image:: /_images/components/workflow/blogpost.png + :alt: A state diagram of the Symfony workflow created by DOT. The Mermaid image will look like this: .. image:: /_images/components/workflow/blogpost_mermaid.png + :alt: A state diagram of the Symfony workflow created by Mermaid. The PlantUML image will look like this: .. image:: /_images/components/workflow/blogpost_puml.png + :alt: A state diagram of the Symfony workflow created by PlantUML. If you are creating workflows outside of a Symfony application, use the ``GraphvizDumper`` or ``StateMachineGraphvizDumper`` class to create the DOT @@ -316,6 +319,7 @@ Below is the configuration for the pull request state machine with styling added The PlantUML image will look like this: .. image:: /_images/components/workflow/pull_request_puml_styled.png + :alt: A state diagram created by PlantUML with custom transition colors and descriptions. .. _`Graphviz`: https://www.graphviz.org .. _`Mermaid CLI`: https://github.com/mermaid-js/mermaid-cli diff --git a/workflow/workflow-and-state-machine.rst b/workflow/workflow-and-state-machine.rst index 10ec2fbf4b6..14ab7d0320a 100644 --- a/workflow/workflow-and-state-machine.rst +++ b/workflow/workflow-and-state-machine.rst @@ -25,11 +25,13 @@ Examples The simplest workflow looks like this. It contains two places and one transition. .. image:: /_images/components/workflow/simple.png + :alt: A simple state diagram showing a single transition between two places. Workflows could be more complicated when they describe a real business case. The workflow below describes the process to fill in a job application. .. image:: /_images/components/workflow/job_application.png + :alt: A complex state diagram showing many places with multiple possible transitions between them. When you fill in a job application in this example there are 4 to 7 steps depending on the job you are applying for. Some jobs require personality @@ -63,6 +65,7 @@ pull request. At any time, you can also "update" the pull request, which will result in another continuous integration run. .. image:: /_images/components/workflow/pull_request.png + :alt: A state diagram for the pull request process described previously. Below is the configuration for the pull request state machine. @@ -78,6 +81,7 @@ Below is the configuration for the pull request state machine. marking_store: type: 'method' property: 'currentPlace' + # The "supports" option is useful only if you are using Twig functions ('workflow_*') supports: - App\Entity\PullRequest initial_marking: start @@ -124,14 +128,12 @@ Below is the configuration for the pull request state machine. - - method - currentPlace - + start - App\Entity\PullRequest + - start + + App\Entity\PullRequest start coding @@ -199,6 +201,7 @@ Below is the configuration for the pull request state machine. $pullRequest ->type('state_machine') + // The "supports" option is useful only if you are using Twig functions ('workflow_*') ->supports(['App\Entity\PullRequest']) ->initialMarking(['start']); @@ -249,33 +252,6 @@ Below is the configuration for the pull request state machine. ->to(['review']); }; -In a Symfony application using the -:ref:`default services.yaml configuration `, -you can get this state machine by injecting the Workflow registry service:: - - // ... - use App\Entity\PullRequest; - use Symfony\Component\Workflow\Registry; - - class SomeService - { - private $workflows; - - public function __construct(Registry $workflows) - { - $this->workflows = $workflows; - } - - public function someMethod(PullRequest $pullRequest) - { - $stateMachine = $this->workflows->get($pullRequest, 'pull_request'); - $stateMachine->apply($pullRequest, 'wait_for_review'); - // ... - } - - // ... - } - Symfony automatically creates a service for each workflow (:class:`Symfony\\Component\\Workflow\\Workflow`) or state machine (:class:`Symfony\\Component\\Workflow\\StateMachine`) you have defined in your configuration. This means that you can use ``workflow.pull_request``