diff --git a/.doctor-rst.yaml b/.doctor-rst.yaml index bf037f27716..c7a17edd06c 100644 --- a/.doctor-rst.yaml +++ b/.doctor-rst.yaml @@ -2,8 +2,8 @@ rules: american_english: ~ argument_variable_must_match_type: arguments: - - { type: 'ContainerBuilder', name: 'containerBuilder' } - - { type: 'ContainerConfigurator', name: 'containerConfigurator' } + - { type: 'ContainerBuilder', name: 'container' } + - { type: 'ContainerConfigurator', name: 'container' } avoid_repetetive_words: ~ blank_line_after_anchor: ~ blank_line_after_directive: ~ @@ -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,23 +33,29 @@ 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: ~ php_prefix_before_bin_console: ~ + remove_trailing_whitespace: ~ replace_code_block_types: ~ replacement: ~ short_array_syntax: ~ @@ -60,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: @@ -75,19 +86,21 @@ rules: deprecated_directive_min_version: min_version: '5.0' +exclude_rule_for_file: + - path: configuration/multiple_kernels.rst + rule_name: replacement + # do not report as violation 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 @@ -97,6 +110,7 @@ whitelist: - '.. versionadded:: 0.2' # MercureBundle - '.. versionadded:: 3.6' # MonologBundle - '.. versionadded:: 3.8' # MonologBundle - - '// bin/console' + - '.. 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 835cf386072..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.44.1 + 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-20.04 + + 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 d45dc483946..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.2", + "version": "v0.21.0", "source": { "type": "git", "url": "https://github.com/symfony-tools/docs-builder.git", - "reference": "6486fd734bb151a05f592b06ac1569c62d338a08" + "reference": "7ab92db15e9be7d6af51b86db87c7e41a14ba18b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony-tools/docs-builder/zipball/6486fd734bb151a05f592b06ac1569c62d338a08", - "reference": "6486fd734bb151a05f592b06ac1569c62d338a08", + "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.2" + "source": "https://github.com/symfony-tools/docs-builder/tree/v0.21.0" }, - "time": "2023-04-04T06:17:34+00:00" + "time": "2023-07-11T15:21:07+00:00" }, { "name": "symfony/console", diff --git a/_images/components/serializer/serializer_workflow.svg b/_images/components/serializer/serializer_workflow.svg index f3906506878..b6e9c254778 100644 --- a/_images/components/serializer/serializer_workflow.svg +++ b/_images/components/serializer/serializer_workflow.svg @@ -1 +1,283 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/components/serializer/serializer_workflow.dia b/_images/sources/components/serializer/serializer_workflow.dia index 6cb44280d0d..3e2ea62558f 100644 Binary files a/_images/sources/components/serializer/serializer_workflow.dia and b/_images/sources/components/serializer/serializer_workflow.dia differ diff --git a/_images/sources/doctrine/mapping_relations.dia b/_images/sources/doctrine/mapping_relations.dia new file mode 100644 index 00000000000..5703e1b781c Binary files /dev/null and b/_images/sources/doctrine/mapping_relations.dia differ diff --git a/_images/sources/doctrine/mapping_relations_proxy.dia b/_images/sources/doctrine/mapping_relations_proxy.dia new file mode 100644 index 00000000000..1f491e9e2ef Binary files /dev/null and b/_images/sources/doctrine/mapping_relations_proxy.dia differ diff --git a/_images/sources/doctrine/mapping_single_entity.dia b/_images/sources/doctrine/mapping_single_entity.dia new file mode 100644 index 00000000000..5a9dc21889c Binary files /dev/null and b/_images/sources/doctrine/mapping_single_entity.dia differ diff --git a/_images/sources/form/data-transformer-types.dia b/_images/sources/form/data-transformer-types.dia new file mode 100644 index 00000000000..972b973a36d Binary files /dev/null and b/_images/sources/form/data-transformer-types.dia differ diff --git a/_images/sources/form/form_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/_images/translation/pseudolocalization-interface-original.png b/_images/translation/pseudolocalization-interface-original.png new file mode 100644 index 00000000000..d89f4e63a24 Binary files /dev/null and b/_images/translation/pseudolocalization-interface-original.png differ diff --git a/_images/translation/pseudolocalization-interface-translated.png b/_images/translation/pseudolocalization-interface-translated.png new file mode 100644 index 00000000000..496d5a0f86f Binary files /dev/null and b/_images/translation/pseudolocalization-interface-translated.png differ diff --git a/_images/translation/pseudolocalization-symfony-demo-disabled.png b/_images/translation/pseudolocalization-symfony-demo-disabled.png new file mode 100644 index 00000000000..1a7472bd41f Binary files /dev/null and b/_images/translation/pseudolocalization-symfony-demo-disabled.png differ diff --git a/_images/translation/pseudolocalization-symfony-demo-enabled.png b/_images/translation/pseudolocalization-symfony-demo-enabled.png new file mode 100644 index 00000000000..a23300a7271 Binary files /dev/null and b/_images/translation/pseudolocalization-symfony-demo-enabled.png differ diff --git a/best_practices.rst b/best_practices.rst index 159868118b3..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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -463,4 +470,4 @@ you must set up a redirection. .. _`feature toggles`: https://en.wikipedia.org/wiki/Feature_toggle .. _`smoke testing`: https://en.wikipedia.org/wiki/Smoke_testing_(software) .. _`Webpack`: https://webpack.js.org/ -.. _`PHPUnit data providers`: https://phpunit.readthedocs.io/en/9.5/writing-tests-for-phpunit.html#data-providers +.. _`PHPUnit data providers`: https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#data-providers 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 d5c73209f26..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/`` @@ -442,8 +444,8 @@ The end user can provide values in any configuration file: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $containerConfigurator) { - $containerConfigurator->parameters() + return static function (ContainerConfigurator $container) { + $container->parameters() ->set('acme_blog.author.email', 'fabien@example.com') ; }; @@ -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/configuration.rst b/bundles/configuration.rst index e25d6e01036..a30b6310ec1 100644 --- a/bundles/configuration.rst +++ b/bundles/configuration.rst @@ -217,7 +217,7 @@ force validation (e.g. if an additional option was passed, an exception will be thrown):: // src/Acme/SocialBundle/DependencyInjection/AcmeSocialExtension.php - public function load(array $configs, ContainerBuilder $containerBuilder) + public function load(array $configs, ContainerBuilder $container) { $configuration = new Configuration(); @@ -259,15 +259,15 @@ In your extension, you can load this and dynamically set its arguments:: use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; - public function load(array $configs, ContainerBuilder $containerBuilder) + public function load(array $configs, ContainerBuilder $container) { - $loader = new XmlFileLoader($containerBuilder, new FileLocator(dirname(__DIR__).'/Resources/config')); + $loader = new XmlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config')); $loader->load('services.xml'); $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - $definition = $containerBuilder->getDefinition('acme.social.twitter_client'); + $definition = $container->getDefinition('acme.social.twitter_client'); $definition->replaceArgument(0, $config['twitter']['client_id']); $definition->replaceArgument(1, $config['twitter']['client_secret']); } @@ -288,7 +288,7 @@ In your extension, you can load this and dynamically set its arguments:: class AcmeHelloExtension extends ConfigurableExtension { // note that this method is called loadInternal and not load - protected function loadInternal(array $mergedConfig, ContainerBuilder $containerBuilder) + protected function loadInternal(array $mergedConfig, ContainerBuilder $container) { // ... } @@ -304,7 +304,7 @@ In your extension, you can load this and dynamically set its arguments:: (e.g. by overriding configurations and using :phpfunction:`isset` to check for the existence of a value). Be aware that it'll be very hard to support XML:: - public function load(array $configs, ContainerBuilder $containerBuilder) + public function load(array $configs, ContainerBuilder $container) { $config = []; // let resources override the previous set value diff --git a/bundles/extension.rst b/bundles/extension.rst index 2a8a5965451..74659cd98b6 100644 --- a/bundles/extension.rst +++ b/bundles/extension.rst @@ -34,7 +34,7 @@ This is how the extension of an AcmeHelloBundle should look like:: class AcmeHelloExtension extends Extension { - public function load(array $configs, ContainerBuilder $containerBuilder) + public function load(array $configs, ContainerBuilder $container) { // ... you'll load the files here later } @@ -89,10 +89,10 @@ For instance, assume you have a file called ``services.xml`` in the use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; // ... - public function load(array $configs, ContainerBuilder $containerBuilder) + public function load(array $configs, ContainerBuilder $container) { $loader = new XmlFileLoader( - $containerBuilder, + $container, new FileLocator(__DIR__.'/../Resources/config') ); $loader->load('services.xml'); @@ -115,7 +115,7 @@ they are compiled when generating the application cache to improve the overall performance. Define the list of annotated classes to compile in the ``addAnnotatedClassesToCompile()`` method:: - public function load(array $configs, ContainerBuilder $containerBuilder) + public function load(array $configs, ContainerBuilder $container) { // ... 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/bundles/prepend_extension.rst b/bundles/prepend_extension.rst index 6ad1ad758d3..35c277ec0e6 100644 --- a/bundles/prepend_extension.rst +++ b/bundles/prepend_extension.rst @@ -31,7 +31,7 @@ To give an Extension the power to do this, it needs to implement { // ... - public function prepend(ContainerBuilder $containerBuilder) + public function prepend(ContainerBuilder $container) { // ... } @@ -52,15 +52,15 @@ a configuration setting in multiple bundles as well as disable a flag in multipl in case a specific other bundle is not registered:: // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php - public function prepend(ContainerBuilder $containerBuilder) + public function prepend(ContainerBuilder $container) { // get all bundles - $bundles = $containerBuilder->getParameter('kernel.bundles'); + $bundles = $container->getParameter('kernel.bundles'); // determine if AcmeGoodbyeBundle is registered if (!isset($bundles['AcmeGoodbyeBundle'])) { // disable AcmeGoodbyeBundle in bundles $config = ['use_acme_goodbye' => false]; - foreach ($containerBuilder->getExtensions() as $name => $extension) { + foreach ($container->getExtensions() as $name => $extension) { switch ($name) { case 'acme_something': case 'acme_other': @@ -70,21 +70,21 @@ in case a specific other bundle is not registered:: // note that if the user manually configured // use_acme_goodbye to true in config/services.yaml // then the setting would in the end be true and not false - $containerBuilder->prependExtensionConfig($name, $config); + $container->prependExtensionConfig($name, $config); break; } } } // get the configuration of AcmeHelloExtension (it's a list of configuration) - $configs = $containerBuilder->getExtensionConfig($this->getAlias()); + $configs = $container->getExtensionConfig($this->getAlias()); // iterate in reverse to preserve the original order after prepending the config foreach (array_reverse($configs) as $config) { // check if entity_manager_name is set in the "acme_hello" configuration if (isset($config['entity_manager_name'])) { // prepend the acme_something settings with the entity_manager_name - $containerBuilder->prependExtensionConfig('acme_something', [ + $container->prependExtensionConfig('acme_something', [ 'entity_manager_name' => $config['entity_manager_name'], ]); } @@ -141,13 +141,13 @@ registered and the ``entity_manager_name`` setting for ``acme_hello`` is set to // config/packages/acme_something.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $containerConfigurator) { - $containerConfigurator->extension('acme_something', [ + return static function (ContainerConfigurator $container) { + $container->extension('acme_something', [ // ... 'use_acme_goodbye' => false, 'entity_manager_name' => 'non_default', ]); - $containerConfigurator->extension('acme_other', [ + $container->extension('acme_other', [ // ... 'use_acme_goodbye' => false, ]); diff --git a/cache.rst b/cache.rst index 48d3a250bd1..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 @@ -106,10 +108,11 @@ The Cache component comes with a series of adapters pre-configured: * :doc:`cache.adapter.apcu ` * :doc:`cache.adapter.array ` -* :doc:`cache.adapter.doctrine ` +* :doc:`cache.adapter.doctrine ` (deprecated) +* :doc:`cache.adapter.doctrine_dbal ` * :doc:`cache.adapter.filesystem ` * :doc:`cache.adapter.memcached ` -* :doc:`cache.adapter.pdo ` +* :doc:`cache.adapter.pdo ` * :doc:`cache.adapter.psr6 ` * :doc:`cache.adapter.redis ` * :ref:`cache.adapter.redis_tag_aware ` (Redis adapter optimized to work with tags) @@ -118,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:: @@ -130,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 - default_doctrine_provider: 'app.doctrine_cache' - # service: cache.psr6 + default_doctrine_dbal_provider: 'doctrine.dbal.default_connection' 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: 'doctrine.dbal.default_connection' + default_pdo_provider: 'app.my_pdo_service' + + services: + app.my_pdo_service: + class: \PDO + arguments: ['pgsql:host=localhost'] .. code-block:: xml @@ -154,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 - ->defaultDoctrineProvider('app.doctrine_cache') - // Service: cache.psr6 + + ->defaultDoctrineDbalProvider('doctrine.dbal.default_connection') ->defaultPsr6Provider('app.my_psr6_service') - // Service: cache.redis ->defaultRedisProvider('redis://localhost') - // Service: cache.memcached ->defaultMemcachedProvider('memcached://localhost') - // Service: cache.pdo - ->defaultPdoProvider('doctrine.dbal.default_connection') + ->defaultPdoProvider('app.my_pdo_service') + ; + + $container->services() + ->set('app.my_pdo_service', \PDO::class) + ->args(['pgsql:host=localhost']) ; }; @@ -384,8 +394,8 @@ with either :class:`Symfony\\Contracts\\Cache\\CacheInterface` or // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $containerConfigurator) { - $containerConfigurator->services() + return function(ContainerConfigurator $container) { + $container->services() // ... ->set('app.cache.adapter.redis') @@ -465,14 +475,13 @@ and use that when configuring the pool. use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Config\FrameworkConfig; - return static function (ContainerBuilder $containerBuilder, FrameworkConfig $framework) { + return static function (ContainerBuilder $container, FrameworkConfig $framework) { $framework->cache() ->pool('cache.my_redis') ->adapters(['cache.adapter.redis']) ->provider('app.my_custom_redis_provider'); - - $containerBuilder->register('app.my_custom_redis_provider', \Redis::class) + $container->register('app.my_custom_redis_provider', \Redis::class) ->setFactory([RedisAdapter::class, 'createConnection']) ->addArgument('redis://localhost') ->addArgument([ @@ -563,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; @@ -609,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 @@ -785,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 @@ -808,7 +816,7 @@ Then, register the ``SodiumMarshaller`` service using this key: - + @@ -825,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:: @@ -836,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/apcu_adapter.rst b/components/cache/adapters/apcu_adapter.rst index c85050e9b4c..99d76ce5d27 100644 --- a/components/cache/adapters/apcu_adapter.rst +++ b/components/cache/adapters/apcu_adapter.rst @@ -1,5 +1,3 @@ -.. _apcu-adapter: - APCu Cache Adapter ================== diff --git a/components/cache/adapters/array_cache_adapter.rst b/components/cache/adapters/array_cache_adapter.rst index 7429593f993..1d8cd87269a 100644 --- a/components/cache/adapters/array_cache_adapter.rst +++ b/components/cache/adapters/array_cache_adapter.rst @@ -15,7 +15,7 @@ method:: // until the current PHP process finishes) $defaultLifetime = 0, - // if ``true``, the values saved in the cache are serialized before storing them + // if true, the values saved in the cache are serialized before storing them $storeSerialized = true, // the maximum lifetime (in seconds) of the entire cache (after this time, the diff --git a/components/cache/adapters/chain_adapter.rst b/components/cache/adapters/chain_adapter.rst index 9a91234096e..586857d2e4d 100644 --- a/components/cache/adapters/chain_adapter.rst +++ b/components/cache/adapters/chain_adapter.rst @@ -1,5 +1,3 @@ -.. _component-cache-chain-adapter: - Chain Cache Adapter =================== diff --git a/components/cache/adapters/couchbasebucket_adapter.rst b/components/cache/adapters/couchbasebucket_adapter.rst index f1e0c13b2b0..172a8fe0f19 100644 --- a/components/cache/adapters/couchbasebucket_adapter.rst +++ b/components/cache/adapters/couchbasebucket_adapter.rst @@ -1,5 +1,3 @@ -.. _couchbase-adapter: - Couchbase Bucket Cache Adapter ============================== @@ -8,8 +6,8 @@ Couchbase Bucket Cache Adapter The Couchbase Bucket adapter was introduced in Symfony 5.1. This adapter stores the values in-memory using one (or more) `Couchbase server`_ -instances. Unlike the :ref:`APCu adapter `, and similarly to the -:ref:`Memcached adapter `, it is not limited to the current server's +instances. Unlike the :doc:`APCu adapter `, and similarly to the +:doc:`Memcached adapter `, it is not limited to the current server's shared memory; you can store contents independent of your PHP environment. The ability to utilize a cluster of servers to provide redundancy and/or fail-over is also available. @@ -41,7 +39,6 @@ the second and third parameters:: $defaultLifetime ); - Configure the Connection ------------------------ @@ -73,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 a0c5e28c9a8..296b7065f1d 100644 --- a/components/cache/adapters/couchbasecollection_adapter.rst +++ b/components/cache/adapters/couchbasecollection_adapter.rst @@ -1,5 +1,3 @@ -.. _couchbase-collection-adapter: - Couchbase Collection Cache Adapter ================================== @@ -8,8 +6,8 @@ Couchbase Collection Cache Adapter The Couchbase Collection adapter was introduced in Symfony 5.4. This adapter stores the values in-memory using one (or more) `Couchbase server`_ -instances. Unlike the :ref:`APCu adapter `, and similarly to the -:ref:`Memcached adapter `, it is not limited to the current server's +instances. Unlike the :doc:`APCu adapter `, and similarly to the +:doc:`Memcached adapter `, it is not limited to the current server's shared memory; you can store contents independent of your PHP environment. The ability to utilize a cluster of servers to provide redundancy and/or fail-over is also available. @@ -38,7 +36,6 @@ the second and third parameters:: $defaultLifetime ); - Configure the Connection ------------------------ @@ -70,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/doctrine_adapter.rst b/components/cache/adapters/doctrine_adapter.rst index 3b894e8388b..b345d310029 100644 --- a/components/cache/adapters/doctrine_adapter.rst +++ b/components/cache/adapters/doctrine_adapter.rst @@ -1,5 +1,3 @@ -.. _doctrine-adapter: - Doctrine Cache Adapter ====================== diff --git a/components/cache/adapters/doctrine_dbal_adapter.rst b/components/cache/adapters/doctrine_dbal_adapter.rst new file mode 100644 index 00000000000..fc04410bffc --- /dev/null +++ b/components/cache/adapters/doctrine_dbal_adapter.rst @@ -0,0 +1,43 @@ +Doctrine DBAL Cache Adapter +=========================== + +The Doctrine DBAL adapters store the cache items in a table of an SQL database. + +.. note:: + + This adapter implements :class:`Symfony\\Component\\Cache\\PruneableInterface`, + allowing for manual :ref:`pruning of expired cache entries ` + by calling the ``prune()`` method. + +The :class:`Symfony\\Component\\Cache\\Adapter\\DoctrineDbalAdapter` requires a +`Doctrine DBAL Connection`_, or `Doctrine DBAL URL`_ as its first parameter. +You can pass a namespace, default cache lifetime, and options array as the other +optional arguments:: + + use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter; + + $cache = new DoctrineDbalAdapter( + + // a Doctrine DBAL connection or DBAL URL + $databaseConnectionOrURL, + + // the string prefixed to the keys of the items stored in this cache + $namespace = '', + + // the default lifetime (in seconds) for cache items that do not define their + // own lifetime, with a value 0 causing items to be stored indefinitely (i.e. + // until the database table is truncated or its rows are otherwise deleted) + $defaultLifetime = 0, + + // an array of options for configuring the database table and connection + $options = [] + ); + +.. note:: + + DBAL Connection are lazy-loaded by default; some additional options may be + necessary to detect the database engine and version without opening the + connection. + +.. _`Doctrine DBAL Connection`: https://github.com/doctrine/dbal/blob/master/src/Connection.php +.. _`Doctrine DBAL URL`: https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url diff --git a/components/cache/adapters/filesystem_adapter.rst b/components/cache/adapters/filesystem_adapter.rst index 331dbb2dff6..26ef48af27c 100644 --- a/components/cache/adapters/filesystem_adapter.rst +++ b/components/cache/adapters/filesystem_adapter.rst @@ -1,10 +1,8 @@ -.. _component-cache-filesystem-adapter: - Filesystem Cache Adapter ======================== This adapter offers improved application performance for those who cannot install -tools like :ref:`APCu ` or :ref:`Redis ` in their +tools like :doc:`APCu ` or :doc:`Redis ` in their environment. It stores the cache item expiration and content as regular files in a collection of directories on a locally mounted filesystem. @@ -39,9 +37,10 @@ and cache root path as constructor parameters:: The overhead of filesystem IO often makes this adapter one of the *slower* choices. If throughput is paramount, the in-memory adapters - (:ref:`Apcu `, :ref:`Memcached `, and - :ref:`Redis `) or the database adapters - (:ref:`PDO `) are recommended. + (:doc:`Apcu `, :doc:`Memcached `, + and :doc:`Redis `) or the database adapters + (:doc:`Doctrine DBAL `, :doc:`PDO `) + are recommended. .. note:: @@ -64,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/memcached_adapter.rst b/components/cache/adapters/memcached_adapter.rst index f2de83251c9..d68d3e3b9ac 100644 --- a/components/cache/adapters/memcached_adapter.rst +++ b/components/cache/adapters/memcached_adapter.rst @@ -1,11 +1,9 @@ -.. _memcached-adapter: - Memcached Cache Adapter ======================= This adapter stores the values in-memory using one (or more) `Memcached server`_ -instances. Unlike the :ref:`APCu adapter `, and similarly to the -:ref:`Redis adapter `, it is not limited to the current server's +instances. Unlike the :doc:`APCu adapter `, and similarly to the +:doc:`Redis adapter `, it is not limited to the current server's shared memory; you can store contents independent of your PHP environment. The ability to utilize a cluster of servers to provide redundancy and/or fail-over is also available. diff --git a/components/cache/adapters/pdo_adapter.rst b/components/cache/adapters/pdo_adapter.rst new file mode 100644 index 00000000000..9cfbfd7bdfa --- /dev/null +++ b/components/cache/adapters/pdo_adapter.rst @@ -0,0 +1,54 @@ +PDO Cache Adapter +================= + +The PDO adapters store the cache items in a table of an SQL database. + +.. note:: + + This adapter implements :class:`Symfony\\Component\\Cache\\PruneableInterface`, + allowing for manual :ref:`pruning of expired cache entries ` + by calling the ``prune()`` method. + +The :class:`Symfony\\Component\\Cache\\Adapter\\PdoAdapter` requires a :phpclass:`PDO`, +or `DSN`_ as its first parameter. You can pass a namespace, +default cache lifetime, and options array as the other optional arguments:: + + use Symfony\Component\Cache\Adapter\PdoAdapter; + + $cache = new PdoAdapter( + + // a PDO connection or DSN for lazy connecting through PDO + $databaseConnectionOrDSN, + + // the string prefixed to the keys of the items stored in this cache + $namespace = '', + + // the default lifetime (in seconds) for cache items that do not define their + // own lifetime, with a value 0 causing items to be stored indefinitely (i.e. + // until the database table is truncated or its rows are otherwise deleted) + $defaultLifetime = 0, + + // an array of options for configuring the database table and connection + $options = [] + ); + +The table where values are stored is created automatically on the first call to +the :method:`Symfony\\Component\\Cache\\Adapter\\PdoAdapter::save` method. +You can also create this table explicitly by calling the +:method:`Symfony\\Component\\Cache\\Adapter\\PdoAdapter::createTable` method in +your code. + +.. deprecated:: 5.4 + + Using :class:`Symfony\\Component\\Cache\\Adapter\\PdoAdapter` with a + ``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. + +.. tip:: + + When passed a `Data Source Name (DSN)`_ string (instead of a database connection + class instance), the connection will be lazy-loaded when needed. + +.. _`DSN`: https://php.net/manual/pdo.drivers.php +.. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name diff --git a/components/cache/adapters/pdo_doctrine_dbal_adapter.rst b/components/cache/adapters/pdo_doctrine_dbal_adapter.rst deleted file mode 100644 index 3615d5a1bbf..00000000000 --- a/components/cache/adapters/pdo_doctrine_dbal_adapter.rst +++ /dev/null @@ -1,95 +0,0 @@ -.. _pdo-doctrine-adapter: - -PDO & Doctrine DBAL Cache Adapter -================================= - -The PDO and Doctrine DBAL adapters store the cache items in a table of an SQL database. - -.. note:: - - These adapters implement :class:`Symfony\\Component\\Cache\\PruneableInterface`, - allowing for manual :ref:`pruning of expired cache entries ` - by calling the ``prune()`` method. - -Using PHP PDO -------------- - -The :class:`Symfony\\Component\\Cache\\Adapter\\PdoAdapter` requires a :phpclass:`PDO`, -or `Data Source Name (DSN)`_ as its first parameter. You can pass a namespace, -default cache lifetime, and options array as the other optional arguments:: - - use Symfony\Component\Cache\Adapter\PdoAdapter; - - $cache = new PdoAdapter( - - // a PDO connection or DSN for lazy connecting through PDO - $databaseConnectionOrDSN, - - // the string prefixed to the keys of the items stored in this cache - $namespace = '', - - // the default lifetime (in seconds) for cache items that do not define their - // own lifetime, with a value 0 causing items to be stored indefinitely (i.e. - // until the database table is truncated or its rows are otherwise deleted) - $defaultLifetime = 0, - - // an array of options for configuring the database table and connection - $options = [] - ); - -The table where values are stored is created automatically on the first call to -the :method:`Symfony\\Component\\Cache\\Adapter\\PdoAdapter::save` method. -You can also create this table explicitly by calling the -:method:`Symfony\\Component\\Cache\\Adapter\\PdoAdapter::createTable` method in -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 - and will be removed in Symfony 6.0. - Use :class:`Symfony\\Component\\Cache\\Adapter\\DoctrineDbalAdapter` instead. - -.. tip:: - - When passed a `Data Source Name (DSN)`_ string (instead of a database connection - class instance), the connection will be lazy-loaded when needed. DBAL Connection - are lazy-loaded by default; some additional options may be necessary to detect - the database engine and version without opening the connection. - -Using Doctrine DBAL -------------------- - -The :class:`Symfony\\Component\\Cache\\Adapter\\DoctrineDbalAdapter` requires a -`Doctrine DBAL Connection`_, or `Doctrine DBAL URL`_ as its first parameter. -You can pass a namespace, default cache lifetime, and options array as the other -optional arguments:: - - use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter; - - $cache = new DoctrineDbalAdapter( - - // a Doctrine DBAL connection or DBAL URL - $databaseConnectionOrURL, - - // the string prefixed to the keys of the items stored in this cache - $namespace = '', - - // the default lifetime (in seconds) for cache items that do not define their - // own lifetime, with a value 0 causing items to be stored indefinitely (i.e. - // until the database table is truncated or its rows are otherwise deleted) - $defaultLifetime = 0, - - // an array of options for configuring the database table and connection - $options = [] - ); - -.. note:: - - DBAL Connection are lazy-loaded by default; some additional options may be - necessary to detect the database engine and version without opening the - connection. - -.. _`Doctrine DBAL Connection`: https://github.com/doctrine/dbal/blob/master/src/Connection.php -.. _`Doctrine DBAL URL`: https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url -.. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name diff --git a/components/cache/adapters/php_files_adapter.rst b/components/cache/adapters/php_files_adapter.rst index dce77657292..efd2cf0e964 100644 --- a/components/cache/adapters/php_files_adapter.rst +++ b/components/cache/adapters/php_files_adapter.rst @@ -1,9 +1,7 @@ -.. _component-cache-files-adapter: - PHP Files Cache Adapter ======================= -Similarly to :ref:`Filesystem Adapter `, this cache +Similarly to :doc:`Filesystem Adapter `, this cache implementation writes cache entries out to disk, but unlike the Filesystem cache adapter, the PHP Files cache adapter writes and reads back these cache files *as native PHP code*. For example, caching the value ``['my', 'cached', 'array']`` will write out a cache diff --git a/components/cache/adapters/redis_adapter.rst b/components/cache/adapters/redis_adapter.rst index 821fbb14050..2b00058c6bd 100644 --- a/components/cache/adapters/redis_adapter.rst +++ b/components/cache/adapters/redis_adapter.rst @@ -1,5 +1,3 @@ -.. _redis-adapter: - Redis Cache Adapter =================== @@ -12,8 +10,8 @@ Redis Cache Adapter This adapter stores the values in-memory using one (or more) `Redis server`_ instances. -Unlike the :ref:`APCu adapter `, and similarly to the -:ref:`Memcached adapter `, it is not limited to the current server's +Unlike the :doc:`APCu adapter `, and similarly to the +:doc:`Memcached adapter `, it is not limited to the current server's shared memory; you can store contents independent of your PHP environment. The ability to utilize a cluster of servers to provide redundancy and/or fail-over is also available. @@ -46,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)`_:: @@ -57,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; @@ -91,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( @@ -110,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 @@ -197,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. @@ -210,7 +189,7 @@ Available Options ``error``, ``distribute`` or ``slaves``. For ``\Predis\ClientInterface`` valid options are ``slaves`` or ``distribute``. -``ssl`` (type: ``bool``, default: ``null``) +``ssl`` (type: ``array``, default: ``null``) SSL context options. See `php.net/context.ssl`_ for more information. .. note:: @@ -252,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 8d05cd268d5..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 ------------------------- @@ -163,7 +162,7 @@ when all items are successfully deleted):: If the cache component is used inside a Symfony application, you can remove items from cache pools using the following commands (which reside within - the :ref:`framework bundle `): + the :doc:`framework bundle `): To remove *one specific item* from the *given pool*: @@ -192,7 +191,7 @@ Pruning Cache Items ------------------- Some cache pools do not include an automated mechanism for pruning expired cache items. -For example, the :ref:`FilesystemAdapter ` cache +For example, the :doc:`FilesystemAdapter ` cache does not remove expired cache items *until an item is explicitly requested and determined to be expired*, for example, via a call to ``Psr\Cache\CacheItemPoolInterface::getItem``. Under certain workloads, this can cause stale cache entries to persist well past their @@ -202,10 +201,11 @@ expired cache items. This shortcoming has been solved through the introduction of :class:`Symfony\\Component\\Cache\\PruneableInterface`, which defines the abstract method :method:`Symfony\\Component\\Cache\\PruneableInterface::prune`. The -:ref:`ChainAdapter `, -:ref:`FilesystemAdapter `, -:ref:`PdoAdapter `, and -:ref:`PhpFilesAdapter ` all implement this new interface, +:doc:`ChainAdapter `, +:doc:`DoctrineDbalAdapter `, and +:doc:`FilesystemAdapter `, +:doc:`PdoAdapter `, and +:doc:`PhpFilesAdapter ` all implement this new interface, allowing manual removal of stale cache items:: use Symfony\Component\Cache\Adapter\FilesystemAdapter; @@ -214,7 +214,7 @@ allowing manual removal of stale cache items:: // ... do some set and get operations $cache->prune(); -The :ref:`ChainAdapter ` implementation does not directly +The :doc:`ChainAdapter ` implementation does not directly contain any pruning logic itself. Instead, when calling the chain adapter's :method:`Symfony\\Component\\Cache\\Adapter\\ChainAdapter::prune` method, the call is delegated to all its compatible cache adapters (and those that do not implement ``PruneableInterface`` are @@ -242,7 +242,7 @@ silently ignored):: If the cache component is used inside a Symfony application, you can prune *all items* from *all pools* using the following command (which resides within - the :ref:`framework bundle `): + the :doc:`framework bundle `): .. code-block:: terminal 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 eaae06c4450..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() ; @@ -870,3 +870,8 @@ Otherwise the result is a clean array of configuration values:: $databaseConfiguration, $configs ); + +.. caution:: + + When processing the configuration tree, the processor assumes that the top + level array key (which matches the extension name) is already stripped off. 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/index.rst b/components/console/helpers/index.rst index 81cf8aae9bf..893652fb5ab 100644 --- a/components/console/helpers/index.rst +++ b/components/console/helpers/index.rst @@ -1,17 +1,6 @@ The Console Helpers =================== -.. toctree:: - :hidden: - - formatterhelper - processhelper - progressbar - questionhelper - table - debug_formatter - cursor - The Console component comes with some useful helpers. These helpers contain functions to ease some common tasks. 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 fb3f2acfe7b..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 @@ -81,6 +95,12 @@ The progress will then be displayed as a throbber: 1/3 [=========>------------------] 33% 3/3 [============================] 100% +.. tip:: + + An alternative to this is to use a + :doc:`/components/console/helpers/progressindicator` instead of a + progress bar. + Whenever your task is finished, don't forget to call :method:`Symfony\\Component\\Console\\Helper\\ProgressBar::finish` to ensure that the progress bar display is refreshed with a 100% completion. diff --git a/components/console/helpers/progressindicator.rst b/components/console/helpers/progressindicator.rst new file mode 100644 index 00000000000..d64ec6367b7 --- /dev/null +++ b/components/console/helpers/progressindicator.rst @@ -0,0 +1,124 @@ +Progress Indicator +================== + +Progress indicators are useful to let users know that a command isn't stalled. +Unlike :doc:`progress bars `, these +indicators are used when the command duration is indeterminate (e.g. long-running +commands, unquantifiable tasks, etc.) + +They work by instantiating the :class:`Symfony\\Component\\Console\\Helper\\ProgressIndicator` +class and advancing the progress as the command executes:: + + use Symfony\Component\Console\Helper\ProgressIndicator; + + // creates a new progress indicator + $progressIndicator = new ProgressIndicator($output); + + // starts and displays the progress indicator with a custom message + $progressIndicator->start('Processing...'); + + $i = 0; + while ($i++ < 50) { + // ... do some work + + // advances the progress indicator + $progressIndicator->advance(); + } + + // ensures that the progress indicator shows a final message + $progressIndicator->finish('Finished'); + +Customizing the Progress Indicator +---------------------------------- + +Built-in Formats +~~~~~~~~~~~~~~~~ + +By default, the information rendered on a progress indicator depends on the current +level of verbosity of the ``OutputInterface`` instance: + +.. code-block:: text + + # OutputInterface::VERBOSITY_NORMAL (CLI with no verbosity flag) + \ Processing... + | Processing... + / Processing... + - Processing... + + # OutputInterface::VERBOSITY_VERBOSE (-v) + \ Processing... (1 sec) + | Processing... (1 sec) + / Processing... (1 sec) + - Processing... (1 sec) + + # OutputInterface::VERBOSITY_VERY_VERBOSE (-vv) and OutputInterface::VERBOSITY_DEBUG (-vvv) + \ Processing... (1 sec, 6.0 MiB) + | Processing... (1 sec, 6.0 MiB) + / Processing... (1 sec, 6.0 MiB) + - Processing... (1 sec, 6.0 MiB) + +.. tip:: + + Call a command with the quiet flag (``-q``) to not display any progress indicator. + +Instead of relying on the verbosity mode of the current command, you can also +force a format via the second argument of the ``ProgressIndicator`` +constructor:: + + $progressIndicator = new ProgressIndicator($output, 'verbose'); + +The built-in formats are the following: + +* ``normal`` +* ``verbose`` +* ``very_verbose`` + +If your terminal doesn't support ANSI, use the ``no_ansi`` variants: + +* ``normal_no_ansi`` +* ``verbose_no_ansi`` +* ``very_verbose_no_ansi`` + +Custom Indicator Values +~~~~~~~~~~~~~~~~~~~~~~~ + +Instead of using the built-in indicator values, you can also set your own:: + + $progressIndicator = new ProgressIndicator($output, 'verbose', 100, ['⠏', '⠛', '⠹', '⢸', '⣰', '⣤', '⣆', '⡇']); + +The progress indicator will now look like this: + +.. code-block:: text + + ⠏ Processing... + ⠛ Processing... + ⠹ Processing... + ⢸ Processing... + +Customize Placeholders +~~~~~~~~~~~~~~~~~~~~~~ + +A progress indicator uses placeholders (a name enclosed with the ``%`` +character) to determine the output format. Here is a list of the +built-in placeholders: + +* ``indicator``: The current indicator; +* ``elapsed``: The time elapsed since the start of the progress indicator; +* ``memory``: The current memory usage; +* ``message``: used to display arbitrary messages in the progress indicator. + +For example, this is how you can customize the ``message`` placeholder:: + + ProgressIndicator::setPlaceholderFormatterDefinition( + 'message', + static function (ProgressIndicator $progressIndicator): string { + // Return any arbitrary string + return 'My custom message'; + } + ); + +.. note:: + + Placeholders customization is applied globally, which means that any + progress indicator displayed after the + ``setPlaceholderFormatterDefinition()`` call will be affected. 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.rst b/components/dependency_injection.rst index dcc98bf2450..a6d8521f03a 100644 --- a/components/dependency_injection.rst +++ b/components/dependency_injection.rst @@ -45,8 +45,8 @@ You can register this in the container as a service:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $containerBuilder = new ContainerBuilder(); - $containerBuilder->register('mailer', 'Mailer'); + $container = new ContainerBuilder(); + $container->register('mailer', 'Mailer'); An improvement to the class to make it more flexible would be to allow the container to set the ``transport`` used. If you change the class @@ -68,8 +68,8 @@ Then you can set the choice of transport in the container:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $containerBuilder = new ContainerBuilder(); - $containerBuilder + $container = new ContainerBuilder(); + $container ->register('mailer', 'Mailer') ->addArgument('sendmail'); @@ -83,9 +83,9 @@ the ``Mailer`` service's constructor argument:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $containerBuilder = new ContainerBuilder(); - $containerBuilder->setParameter('mailer.transport', 'sendmail'); - $containerBuilder + $container = new ContainerBuilder(); + $container->setParameter('mailer.transport', 'sendmail'); + $container ->register('mailer', 'Mailer') ->addArgument('%mailer.transport%'); @@ -112,14 +112,14 @@ not exist yet. Use the ``Reference`` class to tell the container to inject the use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; - $containerBuilder = new ContainerBuilder(); + $container = new ContainerBuilder(); - $containerBuilder->setParameter('mailer.transport', 'sendmail'); - $containerBuilder + $container->setParameter('mailer.transport', 'sendmail'); + $container ->register('mailer', 'Mailer') ->addArgument('%mailer.transport%'); - $containerBuilder + $container ->register('newsletter_manager', 'NewsletterManager') ->addArgument(new Reference('mailer')); @@ -144,14 +144,14 @@ If you do want to though then the container can call the setter method:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; - $containerBuilder = new ContainerBuilder(); + $container = new ContainerBuilder(); - $containerBuilder->setParameter('mailer.transport', 'sendmail'); - $containerBuilder + $container->setParameter('mailer.transport', 'sendmail'); + $container ->register('mailer', 'Mailer') ->addArgument('%mailer.transport%'); - $containerBuilder + $container ->register('newsletter_manager', 'NewsletterManager') ->addMethodCall('setMailer', [new Reference('mailer')]); @@ -160,11 +160,11 @@ like this:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $containerBuilder = new ContainerBuilder(); + $container = new ContainerBuilder(); // ... - $newsletterManager = $containerBuilder->get('newsletter_manager'); + $newsletterManager = $container->get('newsletter_manager'); Avoiding your Code Becoming Dependent on the Container ------------------------------------------------------ @@ -198,8 +198,8 @@ Loading an XML config file:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; - $containerBuilder = new ContainerBuilder(); - $loader = new XmlFileLoader($containerBuilder, new FileLocator(__DIR__)); + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(__DIR__)); $loader->load('services.xml'); Loading a YAML config file:: @@ -208,8 +208,8 @@ Loading a YAML config file:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; - $containerBuilder = new ContainerBuilder(); - $loader = new YamlFileLoader($containerBuilder, new FileLocator(__DIR__)); + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(__DIR__)); $loader->load('services.yaml'); .. note:: @@ -233,8 +233,8 @@ into a separate config file and load it in a similar way:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; - $containerBuilder = new ContainerBuilder(); - $loader = new PhpFileLoader($containerBuilder, new FileLocator(__DIR__)); + $container = new ContainerBuilder(); + $loader = new PhpFileLoader($container, new FileLocator(__DIR__)); $loader->load('services.php'); You can now set up the ``newsletter_manager`` and ``mailer`` services using @@ -287,13 +287,13 @@ config files: namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $containerConfigurator) { - $containerConfigurator->parameters() + return static function (ContainerConfigurator $container) { + $container->parameters() // ... ->set('mailer.transport', 'sendmail') ; - $services = $containerConfigurator->services(); + $services = $container->services(); $services->set('mailer', 'Mailer') ->args(['%mailer.transport%']) ; diff --git a/components/dependency_injection/_imports-parameters-note.rst.inc b/components/dependency_injection/_imports-parameters-note.rst.inc index 45a75652fda..d17d6d60b26 100644 --- a/components/dependency_injection/_imports-parameters-note.rst.inc +++ b/components/dependency_injection/_imports-parameters-note.rst.inc @@ -31,6 +31,6 @@ // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $containerConfigurator) { - $containerConfigurator->import('%kernel.project_dir%/somefile.yaml'); + return static function (ContainerConfigurator $container) { + $container->import('%kernel.project_dir%/somefile.yaml'); }; diff --git a/components/dependency_injection/compilation.rst b/components/dependency_injection/compilation.rst index 3880d6b5508..beedbf33853 100644 --- a/components/dependency_injection/compilation.rst +++ b/components/dependency_injection/compilation.rst @@ -61,10 +61,10 @@ A very simple extension may just load configuration files into the container:: class AcmeDemoExtension implements ExtensionInterface { - public function load(array $configs, ContainerBuilder $containerBuilder) + public function load(array $configs, ContainerBuilder $container) { $loader = new XmlFileLoader( - $containerBuilder, + $container, new FileLocator(__DIR__.'/../Resources/config') ); $loader->load('services.xml'); @@ -114,14 +114,14 @@ are loaded:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; - $containerBuilder = new ContainerBuilder(); - $containerBuilder->registerExtension(new AcmeDemoExtension); + $container = new ContainerBuilder(); + $container->registerExtension(new AcmeDemoExtension); - $loader = new YamlFileLoader($containerBuilder, new FileLocator(__DIR__)); + $loader = new YamlFileLoader($container, new FileLocator(__DIR__)); $loader->load('config.yaml'); // ... - $containerBuilder->compile(); + $container->compile(); .. note:: @@ -132,7 +132,7 @@ are loaded:: The values from those sections of the config files are passed into the first argument of the ``load()`` method of the extension:: - public function load(array $configs, ContainerBuilder $containerBuilder) + public function load(array $configs, ContainerBuilder $container) { $foo = $configs[0]['foo']; //fooValue $bar = $configs[0]['bar']; //barValue @@ -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:: @@ -158,7 +158,7 @@ you could access the config value this way:: use Symfony\Component\Config\Definition\Processor; // ... - public function load(array $configs, ContainerBuilder $containerBuilder) + public function load(array $configs, ContainerBuilder $container) { $configuration = new Configuration(); $processor = new Processor(); @@ -219,13 +219,13 @@ The processed config value can now be added as container parameters as if it were listed in a ``parameters`` section of the config file but with the additional benefit of merging multiple files and validation of the configuration:: - public function load(array $configs, ContainerBuilder $containerBuilder) + public function load(array $configs, ContainerBuilder $container) { $configuration = new Configuration(); $processor = new Processor(); $config = $processor->processConfiguration($configuration, $configs); - $containerBuilder->setParameter('acme_demo.FOO', $config['foo']); + $container->setParameter('acme_demo.FOO', $config['foo']); // ... } @@ -234,14 +234,14 @@ More complex configuration requirements can be catered for in the Extension classes. For example, you may choose to load a main service configuration file but also load a secondary one only if a certain parameter is set:: - public function load(array $configs, ContainerBuilder $containerBuilder) + public function load(array $configs, ContainerBuilder $container) { $configuration = new Configuration(); $processor = new Processor(); $config = $processor->processConfiguration($configuration, $configs); $loader = new XmlFileLoader( - $containerBuilder, + $container, new FileLocator(__DIR__.'/../Resources/config') ); $loader->load('services.xml'); @@ -263,11 +263,11 @@ file but also load a secondary one only if a certain parameter is set:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $containerBuilder = new ContainerBuilder(); + $container = new ContainerBuilder(); $extension = new AcmeDemoExtension(); - $containerBuilder->registerExtension($extension); - $containerBuilder->loadFromExtension($extension->getAlias()); - $containerBuilder->compile(); + $container->registerExtension($extension); + $container->loadFromExtension($extension->getAlias()); + $container->compile(); .. note:: @@ -292,11 +292,11 @@ method is called by implementing { // ... - public function prepend(ContainerBuilder $containerBuilder) + public function prepend(ContainerBuilder $container) { // ... - $containerBuilder->prependExtensionConfig($name, $config); + $container->prependExtensionConfig($name, $config); // ... } @@ -323,7 +323,7 @@ compilation:: class AcmeDemoExtension implements ExtensionInterface, CompilerPassInterface { - public function process(ContainerBuilder $containerBuilder) + public function process(ContainerBuilder $container) { // ... do something during the compilation } @@ -377,7 +377,7 @@ class implementing the ``CompilerPassInterface``:: class CustomPass implements CompilerPassInterface { - public function process(ContainerBuilder $containerBuilder) + public function process(ContainerBuilder $container) { // ... do something during the compilation } @@ -387,8 +387,8 @@ You then need to register your custom pass with the container:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $containerBuilder = new ContainerBuilder(); - $containerBuilder->addCompilerPass(new CustomPass()); + $container = new ContainerBuilder(); + $container->addCompilerPass(new CustomPass()); .. note:: @@ -418,7 +418,7 @@ For example, to run your custom pass after the default removal passes have been run, use:: // ... - $containerBuilder->addCompilerPass( + $container->addCompilerPass( new CustomPass(), PassConfig::TYPE_AFTER_REMOVING ); @@ -460,11 +460,11 @@ serves at dumping the compiled container:: require_once $file; $container = new ProjectServiceContainer(); } else { - $containerBuilder = new ContainerBuilder(); + $container = new ContainerBuilder(); // ... - $containerBuilder->compile(); + $container->compile(); - $dumper = new PhpDumper($containerBuilder); + $dumper = new PhpDumper($container); file_put_contents($file, $dumper->dump()); } @@ -487,11 +487,11 @@ dump it:: require_once $file; $container = new MyCachedContainer(); } else { - $containerBuilder = new ContainerBuilder(); + $container = new ContainerBuilder(); // ... - $containerBuilder->compile(); + $container->compile(); - $dumper = new PhpDumper($containerBuilder); + $dumper = new PhpDumper($container); file_put_contents( $file, $dumper->dump(['class' => 'MyCachedContainer']) @@ -519,12 +519,12 @@ application:: require_once $file; $container = new MyCachedContainer(); } else { - $containerBuilder = new ContainerBuilder(); + $container = new ContainerBuilder(); // ... - $containerBuilder->compile(); + $container->compile(); if (!$isDebug) { - $dumper = new PhpDumper($containerBuilder); + $dumper = new PhpDumper($container); file_put_contents( $file, $dumper->dump(['class' => 'MyCachedContainer']) @@ -554,14 +554,14 @@ for these resources and use them as metadata for the cache:: $containerConfigCache = new ConfigCache($file, $isDebug); if (!$containerConfigCache->isFresh()) { - $containerBuilder = new ContainerBuilder(); + $container = new ContainerBuilder(); // ... - $containerBuilder->compile(); + $container->compile(); - $dumper = new PhpDumper($containerBuilder); + $dumper = new PhpDumper($container); $containerConfigCache->write( $dumper->dump(['class' => 'MyCachedContainer']), - $containerBuilder->getResources() + $container->getResources() ); } diff --git a/components/dom_crawler.rst b/components/dom_crawler.rst index db91554f026..b8c484ab114 100644 --- a/components/dom_crawler.rst +++ b/components/dom_crawler.rst @@ -535,12 +535,12 @@ To work with multi-dimensional fields: .. code-block:: html
- - - - - - +
Pass an array of values:: diff --git a/components/event_dispatcher.rst b/components/event_dispatcher.rst index 5459d27bdb3..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 ............................. @@ -182,26 +171,25 @@ determine which instance is passed. use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; - use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\EventDispatcher\EventDispatcher; - $containerBuilder = new ContainerBuilder(new ParameterBag()); + $container = new ContainerBuilder(new ParameterBag()); // register the compiler pass that handles the 'kernel.event_listener' // and 'kernel.event_subscriber' service tags - $containerBuilder->addCompilerPass(new RegisterListenersPass()); + $container->addCompilerPass(new RegisterListenersPass()); - $containerBuilder->register('event_dispatcher', EventDispatcher::class); + $container->register('event_dispatcher', EventDispatcher::class); // registers an event listener - $containerBuilder->register('listener_service_id', \AcmeListener::class) + $container->register('listener_service_id', \AcmeListener::class) ->addTag('kernel.event_listener', [ 'event' => 'acme.foo.action', 'method' => 'onFooAction', ]); // registers an event subscriber - $containerBuilder->register('subscriber_service_id', \AcmeSubscriber::class) + $container->register('subscriber_service_id', \AcmeSubscriber::class) ->addTag('kernel.event_subscriber'); ``RegisterListenersPass`` resolves aliased class names which for instance @@ -213,21 +201,20 @@ determine which instance is passed. use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; - use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\EventDispatcher\EventDispatcher; - $containerBuilder = new ContainerBuilder(new ParameterBag()); - $containerBuilder->addCompilerPass(new AddEventAliasesPass([ + $container = new ContainerBuilder(new ParameterBag()); + $container->addCompilerPass(new AddEventAliasesPass([ \AcmeFooActionEvent::class => 'acme.foo.action', ])); - $containerBuilder->addCompilerPass(new RegisterListenersPass(), PassConfig::TYPE_BEFORE_REMOVING); + $container->addCompilerPass(new RegisterListenersPass(), PassConfig::TYPE_BEFORE_REMOVING); - $containerBuilder->register('event_dispatcher', EventDispatcher::class); + $container->register('event_dispatcher', EventDispatcher::class); // registers an event listener - $containerBuilder->register('listener_service_id', \AcmeListener::class) + $container->register('listener_service_id', \AcmeListener::class) ->addTag('kernel.event_listener', [ // will be translated to 'acme.foo.action' by RegisterListenersPass. 'event' => \AcmeFooActionEvent::class, @@ -261,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:: @@ -272,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 { @@ -294,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 .................. @@ -320,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 @@ -342,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; @@ -360,7 +357,7 @@ Take the following example of a subscriber that subscribes to the ['onKernelResponsePre', 10], ['onKernelResponsePost', -10], ], - OrderPlacedEvent::NAME => 'onStoreOrder', + OrderPlacedEvent::class => 'onPlacedOrder', ]; } @@ -374,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(); // ... } } @@ -419,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 @@ -482,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 2f7b874d7bf..f8af0c71090 100644 --- a/components/form.rst +++ b/components/form.rst @@ -507,11 +507,11 @@ done by passing a special form "view" object to your template (notice the {{ form_start(form) }} {{ form_widget(form) }} - +

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/type_guesser.rst b/form/type_guesser.rst index f89808d5e08..29c9cea0e21 100644 --- a/form/type_guesser.rst +++ b/form/type_guesser.rst @@ -13,6 +13,17 @@ type guessers. * :class:`Symfony\\Bridge\\Doctrine\\Form\\DoctrineOrmTypeGuesser` provided by the Doctrine bridge. +Guessers are used only in the following cases: + +* Using + :method:`Symfony\\Component\\Form\\FormFactoryInterface::createForProperty` + or + :method:`Symfony\\Component\\Form\\FormFactoryInterface::createBuilderForProperty`; +* Calling :method:`Symfony\\Component\\Form\\FormInterface::add` or + :method:`Symfony\\Component\\Form\\FormBuilderInterface::create` or + :method:`Symfony\\Component\\Form\\FormBuilderInterface::add` without an + explicit type, in a context where the parent form has defined a data class. + Create a PHPDoc Type Guesser ---------------------------- @@ -70,7 +81,7 @@ The ``TypeGuess`` constructor requires three options: * The type name (one of the :doc:`form types `); * Additional options (for instance, when the type is ``entity``, you also - want to set the ``class`` option). If no types are guessed, this should be + want to set the ``class`` option). If no options are guessed, this should be set to an empty array; * The confidence that the guessed type is correct. This can be one of the constants of the :class:`Symfony\\Component\\Form\\Guess\\Guess` class: @@ -162,11 +173,11 @@ set. .. caution:: - You should be very careful using the ``guessPattern()`` method. When the - type is a float, you cannot use it to determine a min or max value of the - float (e.g. you want a float to be greater than ``5``, ``4.512313`` is not valid - but ``length(4.512314) > length(5)`` is, so the pattern will succeed). In - this case, the value should be set to ``null`` with a ``MEDIUM_CONFIDENCE``. + You should be very careful using the ``guessMaxLength()`` method. When the + type is a float, you cannot determine a length (e.g. you want a float to be + less than ``5``, ``5.512313`` is not valid but + ``length(5.512314) > length(5)`` is, so the pattern will succeed). In this + case, the value should be set to ``null`` with a ``MEDIUM_CONFIDENCE``. Registering a Type Guesser -------------------------- diff --git a/form/unit_testing.rst b/form/unit_testing.rst index d67b5f3bae7..bcd82a1ee38 100644 --- a/form/unit_testing.rst +++ b/form/unit_testing.rst @@ -241,4 +241,4 @@ guessers using the :method:`Symfony\\Component\\Form\\Test\\FormIntegrationTestC and :method:`Symfony\\Component\\Form\\Test\\FormIntegrationTestCase::getTypeGuessers` methods. -.. _`PHPUnit data providers`: https://phpunit.readthedocs.io/en/9.5/writing-tests-for-phpunit.html#data-providers +.. _`PHPUnit data providers`: https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#data-providers 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 17223e15e10..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 -------------- @@ -461,7 +477,8 @@ Before using validation, add support for it in your application: $ composer require symfony/validator Validation is done by adding a set of rules, called (validation) constraints, -to a class. You can add them either to the entity class or to the form class. +to a class. You can add them either to the entity class or by using the +:ref:`constraints option ` of form types. To see the first approach - adding constraints to the entity - in action, add the validation constraints, so that the ``task`` field cannot be empty, @@ -567,9 +584,8 @@ object. That's it! If you re-submit the form with invalid data, you'll see the corresponding errors printed out with the form. -To see the second approach - adding constraints to the form - and to -learn more about the validation constraints, please refer to the -:doc:`Symfony validation documentation `. +To see the second approach - adding constraints to the form - refer to +:ref:`this section `. Both approaches can be used together. Form Validation Messages ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -755,8 +771,9 @@ Set the ``label`` option on fields to define their labels explicitly:: Changing the Action and HTTP Method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -By default, a form will be submitted via an HTTP POST request to the same -URL under which the form was rendered. When building the form in the controller, +By default, the ``
`` tag is rendered with a ``method="post"`` attribute, +and no ``action`` attribute. This means that the form is submitted via an HTTP +POST request to the same URL under which it was rendered. When building the form, use the ``setAction()`` and ``setMethod()`` methods to change this:: // src/Controller/TaskController.php @@ -823,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 @@ -1028,6 +1045,7 @@ Form Themes and Customization: /form/bootstrap4 /form/bootstrap5 + /form/tailwindcss /form/form_customization /form/form_themes diff --git a/frontend.rst b/frontend.rst index da0564a5550..b16c55937d4 100644 --- a/frontend.rst +++ b/frontend.rst @@ -6,7 +6,7 @@ Managing CSS and JavaScript Do you prefer video tutorials? Check out the `Webpack Encore screencast series`_. -Symfony ships with a pure-JavaScript library - called Webpack Encore - that makes +Symfony ships with a pure JavaScript library - called Webpack Encore - that makes it a joy to work with CSS and JavaScript. You can use it, use something else, or create static CSS and JS files in your ``public/`` directory directly and include them in your templates. @@ -18,7 +18,7 @@ Webpack Encore `Webpack Encore`_ is a simpler way to integrate `Webpack`_ into your application. It *wraps* Webpack, giving you a clean & powerful API for bundling JavaScript modules, -pre-processing CSS & JS and compiling and minifying assets. Encore gives you professional +pre-processing CSS & JS and compiling and minifying assets. Encore gives you a professional asset system that's a *delight* to use. Encore is inspired by `Webpacker`_ and `Mix`_, but stays in the spirit of Webpack: @@ -28,7 +28,7 @@ to solve the most common Webpack use cases. .. tip:: Encore is made by `Symfony`_ and works *beautifully* in Symfony applications. - But it can be used in any PHP application and even with other server side + But it can be used in any PHP application and even with other server-side programming languages! .. _encore-toc: @@ -45,7 +45,7 @@ Getting Started Adding more Features .................... -* :doc:`CSS Preprocessors: Sass, LESS, etc ` +* :doc:`CSS Preprocessors: Sass, LESS, etc. ` * :doc:`PostCSS and autoprefixing ` * :doc:`Enabling React.js ` * :doc:`Enabling Vue.js (vue-loader) ` @@ -92,15 +92,6 @@ Symfony UX Components Other Front-End Articles ------------------------ -.. toctree:: - :hidden: - :glob: - - frontend/assetic/index - frontend/encore/installation - frontend/encore/simple-example - frontend/encore/* - .. toctree:: :maxdepth: 1 :glob: diff --git a/frontend/_ux-libraries.rst.inc b/frontend/_ux-libraries.rst.inc index a40a51109f5..a9d8f15acde 100644 --- a/frontend/_ux-libraries.rst.inc +++ b/frontend/_ux-libraries.rst.inc @@ -19,6 +19,7 @@ (`see demo `_) * `ux-typed`_: Integration with `Typed`_ (`see demo `_) * `ux-vue`_: Render `Vue`_ component from Twig (`see demo `_) +* `ux-svelte`_: Render `Svelte`_ component from Twig. .. _`ux-autocomplete`: https://symfony.com/bundles/ux-autocomplete/current/index.html .. _`ux-chartjs`: https://symfony.com/bundles/ux-chartjs/current/index.html @@ -33,9 +34,11 @@ .. _`ux-twig-component`: https://symfony.com/bundles/ux-twig-component/current/index.html .. _`ux-typed`: https://symfony.com/bundles/ux-typed/current/index.html .. _`ux-vue`: https://symfony.com/bundles/ux-vue/current/index.html +.. _`ux-svelte`: https://symfony.com/bundles/ux-svelte/current/index.html .. _`Chart.js`: https://www.chartjs.org/ .. _`Swup`: https://swup.js.org/ .. _`React`: https://reactjs.org/ +.. _`Svelte`: https://svelte.dev/ .. _`Turbo Drive`: https://turbo.hotwired.dev/ .. _`Typed`: https://github.com/mattboldt/typed.js/ .. _`Vue`: https://vuejs.org/ diff --git a/frontend/create_ux_bundle.rst b/frontend/create_ux_bundle.rst index 8bc04725bcd..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 } } } @@ -70,28 +70,28 @@ In this case, the file located at ``[assets directory]/dist/controller.js`` will 1. Add the following to your ``package.json`` file: - .. code-block:: json - - { - "scripts": { - "build": "babel src --extensions .ts -d dist" - }, - "devDependencies": { - "@babel/cli": "^7.20.7", - "@babel/core": "^7.20.12", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/preset-env": "^7.20.2", - "@babel/preset-typescript": "^7.18.6", - "@hotwired/stimulus": "^3.2.1", - "typescript": "^4.9.5" - } - } - - 2. Run either ``npm install`` or ``yarn install`` to install the new dependencies. + .. code-block:: json + + { + "scripts": { + "build": "babel src --extensions .ts -d dist" + }, + "devDependencies": { + "@babel/cli": "^7.20.7", + "@babel/core": "^7.20.12", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/preset-env": "^7.20.2", + "@babel/preset-typescript": "^7.18.6", + "@hotwired/stimulus": "^3.2.1", + "typescript": "^4.9.5" + } + } + + 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: @@ -108,7 +108,7 @@ To use your controller in a template (e.g. one defined in your bundle) you can u ... -Don't forget to add ``symfony/webpack-encore-bundle:^1.12`` as a composer dependency to use +Don't forget to add ``symfony/stimulus-bundle:^2.9`` as a composer dependency to use Twig ``stimulus_*`` functions. .. tip:: diff --git a/frontend/custom_version_strategy.rst b/frontend/custom_version_strategy.rst index cdd4c6664be..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'; @@ -139,10 +139,9 @@ After creating the strategy PHP class, register it as a Symfony service. namespace Symfony\Component\DependencyInjection\Loader\Configurator; use App\Asset\VersionStrategy\GulpBusterVersionStrategy; - use Symfony\Component\DependencyInjection\Definition; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(GulpBusterVersionStrategy::class) ->args( @@ -153,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 52a4fa83b05..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()`` @@ -58,7 +50,6 @@ method in your ``webpack.config.js`` file: }) ; - Enabling HTTPS using the Symfony Web Server ------------------------------------------- @@ -84,6 +75,15 @@ server SSL certificate: + } + }) +.. note:: + + 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. + + This generates a new ``default.p12`` file suitable for use with recent Node.js versions. + CORS Issues ----------- @@ -116,6 +116,33 @@ your page. HMR works automatically with CSS (as long as you're using the ``dev-server`` and Encore 1.0 or higher) but only works with some JavaScript (like :doc:`Vue.js `). +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 + // ... + + Encore + // ... + + .configureDevServerOptions(options => { + options.liveReload = true; + options.static = { + watch: false + }; + options.watchFiles = { + paths: ['src/**/*.php', 'templates/**/*'], + }; + }) + +The ``static.watch`` option is required to disable the default reloading of +files from the static directory, as those files are already handled by HMR. + .. versionadded:: 1.0.0 Before Encore 1.0, you needed to pass a ``--hot`` flag at the command line 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 d41da8daf84..ce15976e464 100644 --- a/frontend/encore/simple-example.rst +++ b/frontend/encore/simple-example.rst @@ -5,10 +5,7 @@ After :doc:`installing Encore `, your app already has a few files, organized into an ``assets/`` directory: * ``assets/app.js`` -* ``assets/bootstrap.js`` -* ``assets/controllers.json`` * ``assets/styles/app.css`` -* ``assets/controllers/hello_controller.js`` With Encore, think of your ``app.js`` file like a standalone JavaScript application: it will *require* all of the dependencies it needs (e.g. jQuery or React), @@ -27,9 +24,6 @@ statements and create one final ``app.js`` (and ``app.css``) that contains *ever your app needs. Encore can do a lot more: minify files, pre-process Sass/LESS, support React, Vue.js, etc. -The other files - ``bootstrap.js``, ``controllers.json`` and ``hello_controller.js`` -relate to a topic you'll learn about soon: `Stimulus & Symfony UX`_. - Configuring Encore/Webpack -------------------------- @@ -62,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 @@ -185,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``: @@ -222,10 +211,18 @@ easy to attach behavior to HTML. It's powerful, and you will love it! Symfony even provides packages to add more features to Stimulus. These are called the Symfony UX Packages. -If you followed the setup instructions, you should already have Stimulus installed -and ready to go! In fact, that's the purpose of the ``assets/bootstrap.js`` file: -to initialize Stimulus and automatically load any "controllers" from the -``assets/controllers/`` directory. +To use Stimulus, first install StimulusBundle: + +.. code-block:: terminal + + $ composer require symfony/stimulus-bundle + +The Flex recipe should add several files/directories: + +* ``assets/bootstrap.js`` - initializes Stimulus; +* ``assets/controllers/`` - a directory where you'll put your Stimulus controllers; +* ``assets/controllers.json`` - file that helps load Stimulus controllers form UX + packages that you'll install. Let's look at a simple Stimulus example. In a Twig template, suppose you have: @@ -363,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 @@ -428,19 +421,13 @@ Encore. When you do, you'll see an error! .. code-block:: terminal > Error: Install sass-loader & sass to use enableSassLoader() - > yarn add sass-loader@^12.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@^12.0.0 sass --dev - $ yarn encore dev --watch - - # if you use the npm package manager - $ npm install sass-loader@^12.0.0 sass --save-dev + $ npm install sass-loader@^13.0.0 sass --save-dev $ npm run watch Your app now supports Sass. Encore also supports LESS and Stylus. See 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/frontend/ux.rst b/frontend/ux.rst index a43bcd8d028..98360893905 100644 --- a/frontend/ux.rst +++ b/frontend/ux.rst @@ -59,7 +59,7 @@ PHP package. For example: { "devDependencies": { "...": "", - "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets" + "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/assets" } } diff --git a/http_cache/cache_invalidation.rst b/http_cache/cache_invalidation.rst index 48d451d3154..8e0b022a5a1 100644 --- a/http_cache/cache_invalidation.rst +++ b/http_cache/cache_invalidation.rst @@ -123,8 +123,8 @@ Then, register the class as a service that :doc:`decorates services(); + return function (ContainerConfigurator $container) { + $services = $container->services(); $services->set(CacheKernel::class) ->decorate('http_cache') @@ -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 also pass the cookie contents as a string - 'Cookie' => 'flavor=chocolate; expires=Sat, 11 Feb 2023 12:18:13 GMT; Max-Age=86400; path=/' + // you can set multiple cookies at once separating them with a ; + 'Cookie' => 'flavor=chocolate; size=medium', + + // if needed, encode the cookie value to ensure that it contains valid characters + 'Cookie' => sprintf("%s=%s", 'foo', rawurlencode('...')), ], ]); @@ -738,7 +758,7 @@ original HTTP client:: $client = new RetryableHttpClient(HttpClient::create()); -The ``RetryableHttpClient`` uses a +The :class:`Symfony\\Component\\HttpClient\\RetryableHttpClient` uses a :class:`Symfony\\Component\\HttpClient\\Retry\\RetryStrategyInterface` to decide if the request should be retried, and to define the waiting time between each retry. @@ -776,7 +796,8 @@ called when new data is uploaded or downloaded and at least once per second:: ]); Any exceptions thrown from the callback will be wrapped in an instance of -``TransportExceptionInterface`` and will abort the request. +:class:`Symfony\\Contracts\\HttpClient\\Exception\\TransportExceptionInterface` +and will abort the request. HTTPS Certificates ~~~~~~~~~~~~~~~~~~ @@ -853,14 +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:: -``HttpClient::create()`` selects the cURL transport if the `cURL PHP extension`_ -is enabled and falls back to PHP streams otherwise. If you prefer to select -the transport explicitly, use the following classes to create the client:: + 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. 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; @@ -870,9 +905,12 @@ the transport 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -911,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. @@ -926,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 @@ -982,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 -------------------- @@ -1025,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`. @@ -1035,13 +1077,18 @@ 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 ~~~~~~~~~~~~~~~~~~~ -Call the ``stream()`` method of the HTTP client to get *chunks* of the -response sequentially instead of waiting for the entire response:: +Call the :method:`Symfony\\Contracts\\HttpClient\\HttpClientInterface::stream` +method to get *chunks* of the response sequentially instead of waiting for the +entire response:: $url = 'https://releases.ubuntu.com/18.04.1/ubuntu-18.04.1-desktop-amd64.iso'; $response = $client->request('GET', $url); @@ -1071,8 +1118,7 @@ Canceling Responses To abort a request (e.g. because it didn't complete in due time, or you want to fetch only the first bytes of the response, etc.), you can either use the -``cancel()`` method of -:class:`Symfony\\Contracts\\HttpClient\\ResponseInterface`:: +:method:`Symfony\\Contracts\\HttpClient\\ResponseInterface::cancel`:: $response->cancel(); @@ -1190,10 +1236,12 @@ If you look again at the snippet above, responses are read in requests' order. But maybe the 2nd response came back before the 1st? Fully asynchronous operations require being able to deal with the responses in whatever order they come back. -In order to do so, the ``stream()`` method of HTTP clients accepts a list of -responses to monitor. As mentioned :ref:`previously `, -this method yields response chunks as they arrive from the network. By replacing -the "foreach" in the snippet with this one, the code becomes fully async:: +In order to do so, the +:method:`Symfony\\Contracts\\HttpClient\\HttpClientInterface::stream` +accepts a list of responses to monitor. As mentioned +:ref:`previously `, this method yields response +chunks as they arrive from the network. By replacing the "foreach" in the +snippet with this one, the code becomes fully async:: foreach ($client->stream($responses) as $response => $chunk) { if ($chunk->isFirst()) { @@ -1330,7 +1378,8 @@ 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'); -``CachingHttpClient`` accepts a third argument to set the options of the ``HttpCache``. +: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 ---------------------------- @@ -1391,7 +1440,7 @@ The component is interoperable with four different abstractions for HTTP clients: `Symfony Contracts`_, `PSR-18`_, `HTTPlug`_ v1/v2 and native PHP streams. If your application uses libraries that need any of them, the component is compatible with all of them. They also benefit from :ref:`autowiring aliases ` -when the :ref:`framework bundle ` is used. +when the :doc:`framework bundle ` is used. If you are writing or maintaining a library that makes HTTP requests, you can decouple it from any specific HTTP client implementations by coding against @@ -1496,8 +1545,8 @@ it. As such, you should not use it in newly written code. The component is still interoperable with libraries that require it thanks to the :class:`Symfony\\Component\\HttpClient\\HttplugClient` class. Similarly to :class:`Symfony\\Component\\HttpClient\\Psr18Client` implementing relevant parts of PSR-17, -``HttplugClient`` also implements the factory methods defined in the related -``php-http/message-factory`` package. +:class:`Symfony\\Component\\HttpClient\\HttplugClient` also implements the factory methods +defined in the related ``php-http/message-factory`` package. .. code-block:: terminal @@ -1528,15 +1577,16 @@ that requires HTTPlug dependencies:: // [...] } -Because ``HttplugClient`` implements the three interfaces, you can use it this way:: +Because :class:`Symfony\\Component\\HttpClient\\HttplugClient` implements the +three interfaces, you can use it this way:: use Symfony\Component\HttpClient\HttplugClient; $httpClient = new HttplugClient(); $apiClient = new SomeSdk($httpClient, $httpClient, $httpClient); -If you'd like to work with promises, ``HttplugClient`` also implements the -``HttpAsyncClient`` interface. To use it, you need to install the +If you'd like to work with promises, :class:`Symfony\\Component\\HttpClient\\HttplugClient` +also implements the ``HttpAsyncClient`` interface. To use it, you need to install the ``guzzlehttp/promises`` package: .. code-block:: terminal @@ -1611,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(); } @@ -1627,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); } @@ -1716,20 +1766,24 @@ external service. By not making actual HTTP requests there is no need to worry a the service being online or the request changing state, for example deleting a resource. -``MockHttpClient`` implements the ``HttpClientInterface``, just like any actual -HTTP client in this component. When you type-hint with ``HttpClientInterface`` -your code will accept the real client outside tests, while replacing it with -``MockHttpClient`` in the test. +:class:`Symfony\\Component\\HttpClient\\MockHttpClient` implements the +:class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface`, just like any actual +HTTP client in this component. When you type-hint with +:class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface` your code will accept +the real client outside tests, while replacing it with +:class:`Symfony\\Component\\HttpClient\\MockHttpClient` in the test. -When the ``request`` method is used on ``MockHttpClient``, it will respond with -the supplied ``MockResponse``. There are a few ways to use it, as described -below. +When the ``request`` method is used on :class:`Symfony\\Component\\HttpClient\\MockHttpClient`, +it will respond with the supplied +:class:`Symfony\\Component\\HttpClient\\Response\\MockResponse`. There are a few ways to use +it, as described below. HTTP Client and Responses ~~~~~~~~~~~~~~~~~~~~~~~~~ -The first way of using ``MockHttpClient`` is to pass a list of responses to its -constructor. These will be yielded in order when requests are made:: +The first way of using :class:`Symfony\\Component\\HttpClient\\MockHttpClient` +is to pass a list of responses to its constructor. These will be yielded +in order when requests are made:: use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; @@ -1744,8 +1798,8 @@ constructor. These will be yielded in order when requests are made:: $response1 = $client->request('...'); // returns $responses[0] $response2 = $client->request('...'); // returns $responses[1] -Another way of using ``MockHttpClient`` is to pass a callback that generates the -responses dynamically when it's called:: +Another way of using :class:`Symfony\\Component\\HttpClient\\MockHttpClient` is to +pass a callback that generates the responses dynamically when it's called:: use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; @@ -1775,7 +1829,7 @@ assertions on the request before returning the mocked response:: }, ]; - $client = new MockHttpClient($expectedRequest); + $client = new MockHttpClient($expectedRequests); // ... @@ -1787,7 +1841,9 @@ assertions on the request before returning the mocked response:: .. tip:: Instead of using the first argument, you can also set the (list of) - responses or callbacks using the ``setResponseFactory()`` method:: + responses or callbacks using the + :method:`Symfony\\Component\\HttpClient\\MockHttpClient::setResponseFactory` + method:: $responses = [ new MockResponse($body1, $info1), @@ -1799,7 +1855,8 @@ assertions on the request before returning the mocked response:: .. versionadded:: 5.4 - The ``setResponseFactory()`` method was introduced in Symfony 5.4. + The :method:`Symfony\\Component\\HttpClient\\MockHttpClient::setResponseFactory` + method was introduced in Symfony 5.4. If you need to test responses with HTTP status codes different than 200, define the ``http_code`` option:: @@ -1815,10 +1872,12 @@ define the ``http_code`` option:: $response = $client->request('...'); The responses provided to the mock client don't have to be instances of -``MockResponse``. Any class implementing ``ResponseInterface`` will work (e.g. -``$this->createMock(ResponseInterface::class)``). +:class:`Symfony\\Component\\HttpClient\\Response\\MockResponse`. Any class +implementing :class:`Symfony\\Contracts\\HttpClient\\ResponseInterface` +will work (e.g. ``$this->createMock(ResponseInterface::class)``). -However, using ``MockResponse`` allows simulating chunked responses and timeouts:: +However, using :class:`Symfony\\Component\\HttpClient\\Response\\MockResponse` +allows simulating chunked responses and timeouts:: $body = function () { yield 'hello'; @@ -1910,7 +1969,8 @@ Then configure Symfony to use your callback: Testing Request Data ~~~~~~~~~~~~~~~~~~~~ -The ``MockResponse`` class comes with some helper methods to test the request: +The :class:`Symfony\\Component\\HttpClient\\Response\\MockResponse` class comes +with some helper methods to test the request: * ``getRequestMethod()`` - returns the HTTP method; * ``getRequestUrl()`` - returns the URL the request would be sent to; @@ -1919,8 +1979,9 @@ The ``MockResponse`` class comes with some helper methods to test the request: .. versionadded:: 5.2 - The ``getRequestMethod()`` and ``getRequestUrl()`` methods were introduced - in Symfony 5.2. + The :method:`Symfony\\Component\\HttpClient\\Response\\MockResponse::getRequestMethod` + and :method:`Symfony\\Component\\HttpClient\\Response\\MockResponse::getRequestUrl` + methods were introduced in Symfony 5.2. Usage example:: @@ -2038,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/index.rst b/index.rst index 288febd7ab8..d4663a94a68 100644 --- a/index.rst +++ b/index.rst @@ -8,11 +8,6 @@ Quick Tour Get started fast with the Symfony :doc:`Quick Tour `: -.. toctree:: - :hidden: - - quick_tour/index - * :doc:`quick_tour/the_big_picture` * :doc:`quick_tour/flex_recipes` * :doc:`quick_tour/the_architecture` @@ -68,11 +63,6 @@ Topics Components ---------- -.. toctree:: - :hidden: - - components/ - Read the :doc:`Components ` documentation. Reference Documents @@ -80,11 +70,6 @@ Reference Documents Get answers quickly with reference documents: -.. toctree:: - :hidden: - - reference/index - .. include:: /reference/map.rst.inc Contributing @@ -92,11 +77,6 @@ Contributing Contribute to Symfony: -.. toctree:: - :hidden: - - contributing/index - .. include:: /contributing/map.rst.inc Create your Own Framework @@ -104,9 +84,4 @@ Create your Own Framework Want to create your own framework based on Symfony? -.. toctree:: - :hidden: - - create_framework/index - .. include:: /create_framework/map.rst.inc 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/lock.rst b/lock.rst index 3e93173aedc..7a05152429e 100644 --- a/lock.rst +++ b/lock.rst @@ -50,6 +50,7 @@ this behavior by using the ``lock`` key like: lock: ['memcached://m1.docker', 'memcached://m2.docker'] lock: 'redis://r1.docker' lock: ['redis://r1.docker', 'redis://r2.docker'] + lock: 'rediss://r1.docker?ssl[verify_peer]=1&ssl[cafile]=...' lock: 'zookeeper://z1.docker' lock: 'zookeeper://z1.docker,z2.docker' lock: 'sqlite:///%kernel.project_dir%/var/lock.db' @@ -281,7 +282,7 @@ case version of its name suffixed by ``LockFactory``. For instance, the ``invoice`` lock can be injected by naming the argument ``$invoiceLockFactory`` and type-hinting it with -:class:`Symfony\\Component\\Lock\\LockFactory`: +:class:`Symfony\\Component\\Lock\\LockFactory`:: // src/Controller/PdfController.php namespace App\Controller; 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 14a3a36518c..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:: @@ -27,7 +19,7 @@ To use it, declare it as a service: Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler: ~ # optionally, configure the handler using the constructor arguments (shown values are default) - Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler: ~ + Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler: arguments: $endpoint: "http://127.0.0.1:9200" $index: "monolog" @@ -73,21 +65,24 @@ To use it, declare it as a service: // optionally, configure the handler using the constructor arguments (shown values are default) $container->register(ElasticsearchLogstashHandler::class) - ->setArguments( + ->setArguments([ '$endpoint' => "http://127.0.0.1:9200", '$index' => "monolog", '$client' => null, '$level' => Logger::DEBUG, '$bubble' => true, '$elasticsearchVersion' => '1.0.0', - ) + ]) ; .. versionadded:: 5.4 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 e6da3dbeb51..350a5646a31 100644 --- a/logging/monolog_email.rst +++ b/logging/monolog_email.rst @@ -292,7 +292,8 @@ 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']) ; @@ -321,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 712efa8a8c5..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 $containerConfigurator): void { - $containerConfigurator->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 @@ -215,6 +217,12 @@ Provider SMTP HTTP The usage of ``default_socket_timeout`` as the default timeout was introduced in Symfony 5.1. +.. note:: + + Besides SMTP, many 3rd party transports offer a web API to send emails. + To do so, you have to install (additionally to the bridge) + the HttpClient component via ``composer require symfony/http-client``. + .. note:: To use Google Gmail, you must have a Google Account with 2-Step-Verification (2FA) @@ -239,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 @@ -315,7 +323,6 @@ Other Options This option was introduced in Symfony 5.2. - ``local_domain`` The domain name to use in ``HELO`` command:: @@ -354,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 --------------------------- @@ -392,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 ~~~~~~~~~~~~~~~ @@ -745,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') }})

@@ -966,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: @@ -1044,6 +1083,15 @@ Before signing/encrypting messages, make sure to have: When using OpenSSL to generate certificates, make sure to add the ``-addtrust emailProtection`` command option. +.. caution:: + + Signing and encrypting messages require their contents to be fully rendered. + For example, the content of :ref:`templated emails ` is rendered + by a :class:`Symfony\\Component\\Mailer\\EventListener\\MessageListener`. + So, if you want to sign and/or encrypt such a message, you need to do it in + a :ref:`MessageEvent ` listener run after it (you need to set + a negative priority to your listener). + Signing Messages ~~~~~~~~~~~~~~~~ @@ -1302,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`). @@ -1385,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 @@ -1432,13 +1479,6 @@ is sent:: } } -.. tip:: - - When using a ``MessageEvent`` listener to - :doc:`sign the email contents `, run it as - late as possible (e.g. setting a negative priority for it) so the email - contents are not set or modified after signing them. - Development & Debugging ----------------------- @@ -1586,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 @@ -1596,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 64704f65b4e..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 - - # config/packages/doctrine.yaml - doctrine: - dbal: - schema_filter: '~^(?!messenger_messages)~' +If you want to change the default table name, pass a custom table name in the +DSN by using the ``table_name`` option: - .. code-block:: 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,25 +2399,21 @@ 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 ~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 1.11 - - The following Doctrine middleware was introduced in DoctrineBundle 1.11. - If you use Doctrine in your app, a number of optional middleware exist that you may want to use: @@ -2437,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'] @@ -2458,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 ------------------------------------- @@ -2589,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/messenger/multiple_buses.rst b/messenger/multiple_buses.rst index 08f788ec109..e96840fcb0d 100644 --- a/messenger/multiple_buses.rst +++ b/messenger/multiple_buses.rst @@ -204,8 +204,8 @@ you can determine the message bus based on an implemented interface: use App\MessageHandler\CommandHandlerInterface; use App\MessageHandler\QueryHandlerInterface; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); // ... 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 b053e0a88a7..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 @@ -349,11 +349,6 @@ Have fun! Go Deeper with HTTP & Framework Fundamentals -------------------------------------------- -.. toctree:: - :hidden: - - routing - .. toctree:: :maxdepth: 1 :glob: 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 8ae4d9dab36..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(), @@ -512,8 +519,8 @@ you'll need to configure the data collector explicitly: use App\DataCollector\RequestCollector; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(RequestCollector::class) ->tag('data_collector', [ diff --git a/quick_tour/the_architecture.rst b/quick_tour/the_architecture.rst index 3bd459d2e3e..f42b4205316 100644 --- a/quick_tour/the_architecture.rst +++ b/quick_tour/the_architecture.rst @@ -286,12 +286,12 @@ using the special ``when@`` keyword: use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework, ContainerConfigurator $containerConfigurator) { + return static function (FrameworkConfig $framework, ContainerConfigurator $container) { $framework->router() ->utf8(true) ; - if ('prod' === $containerConfigurator->env()) { + if ('prod' === $container->env()) { $framework->router() ->strictRequirements(null) ; 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 6ace5a6af67..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,14 +45,16 @@ 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 the previous example, +it can overload the server at the window edges. In this example, there were 6 accepted requests between 11:00 and 12:00. This is more significant with bigger limits. For instance, with 5,000 requests -per hour, a user could make the 4,999 requests in the last minute of some +per hour, a user could make 4,999 requests in the last minute of some hour and another 5,000 requests during the first minute of the next hour, making 9,999 requests in total in two minutes and possibly overloading the server. These periods of excessive usage are called "bursts". @@ -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 d2dec43171b..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`` @@ -299,6 +303,8 @@ This option is ``false`` by default and it's considered a legacy option. It was only useful in previous Symfony versions, when it was recommended to use bundles to organize the application code. +.. _doctrine_auto-mapping: + Custom Mapping Entities in a Bundle ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/reference/configuration/framework.rst b/reference/configuration/framework.rst index 53f3aa64960..4432563f597 100644 --- a/reference/configuration/framework.rst +++ b/reference/configuration/framework.rst @@ -1,5 +1,3 @@ -.. _framework-bundle-configuration: - Framework Configuration Reference (FrameworkBundle) =================================================== @@ -221,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`` @@ -313,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:: @@ -427,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 ................................ @@ -740,7 +743,7 @@ is disabled. This can be either a template name or the content itself. path .... -**type**: ``string`` **default**: ``'/_fragment'`` +**type**: ``string`` **default**: ``/_fragment`` The path prefix for fragments. The fragment listener will only be executed when the request starts with this path. @@ -884,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 .......... @@ -1021,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 ..... @@ -1052,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 .......... @@ -1071,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 ...... @@ -1102,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 ......... @@ -1117,7 +1093,7 @@ Use ``0`` to not limit the duration. max_duration ............ -**type**: ``float`` **default**: 0 +**type**: ``float`` **default**: ``0`` The maximum execution time, in seconds, that the request and the response are allowed to take. A value lower than or equal to 0 means it is unlimited. @@ -1140,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 ........... @@ -1152,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 .......... @@ -1223,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 .............. @@ -1357,7 +1385,7 @@ requests (and not on the subrequests). dsn ... -**type**: ``string`` **default**: ``'file:%kernel.cache_dir%/profiler'`` +**type**: ``string`` **default**: ``file:%kernel.cache_dir%/profiler`` The DSN where to store the profiling information. @@ -1543,6 +1571,40 @@ If the charset of your application is UTF-8 (as defined in the recommended setting it to ``true``. This will make non-UTF8 URLs to generate 404 errors. +secrets +~~~~~~~ + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` + +Whether to enable or not secrets managements. + +decryption_env_var +.................. + +**type**: ``string`` **default**: ``base64:default::SYMFONY_DECRYPTION_SECRET`` + +The env var name that contains the vault decryption secret. By default, this +value will be decoded from base64. + +local_dotenv_file +................. + +**type**: ``string`` **default**: ``%kernel.project_dir%/.env.%kernel.environment%.local`` + +The path to the local ``.env`` file. This file must contain the vault +decryption key, given by the ``decryption_env_var`` option. + +vault_directory +............... + +**type**: ``string`` **default**: ``%kernel.project_dir%/config/secrets/%kernel.runtime_environment%`` + +The directory to store the secret vault. By default, the path uses the current +environment. + .. _config-framework-session: session @@ -1553,7 +1615,7 @@ session storage_factory_id .................. -**type**: ``string`` **default**: ``'session.storage.factory.native'`` +**type**: ``string`` **default**: ``session.storage.factory.native`` The service ID used for creating the ``SessionStorageInterface`` that stores the session. This service is available in the Symfony application via the @@ -1567,57 +1629,129 @@ 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: handler_id .......... -**type**: ``string`` **default**: ``'session.handler.native_file'`` +**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. -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. +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 `: @@ -1654,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. By default, it's -blank, meaning the host name of the server which generated the cookie according -to the cookie specification. +This determines the domain to set in the session cookie. + +If not set, ``php.ini``'s `session.cookie_domain`_ directive will be relied on. cookie_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 @@ -1680,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'`` @@ -1695,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 ............... @@ -1715,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 .............. @@ -1737,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:: @@ -1870,11 +2011,22 @@ 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 +~~~ + +enabled +....... + +**type**: ``boolean`` **default**: ``false`` + +Whether to enable or not SSI support in your application. assets ~~~~~~ @@ -2384,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. @@ -2702,7 +2854,7 @@ annotations cache ..... -**type**: ``string`` **default**: ``'php_array'`` +**type**: ``string`` **default**: ``php_array`` This option can be one of the following values: @@ -2722,7 +2874,7 @@ a service id file_cache_dir .............. -**type**: ``string`` **default**: ``'%kernel.cache_dir%/annotations'`` +**type**: ``string`` **default**: ``%kernel.cache_dir%/annotations`` The directory to store cache files for annotations, in case ``annotations.cache`` is set to ``'file'``. @@ -2738,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 ~~~~~~~ @@ -2873,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 @@ -3117,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 """""" @@ -3184,6 +3335,12 @@ It's also useful when using `blue/green deployment`_ strategies and more generally, when you need to abstract out the actual deployment directory (for example, when warming caches offline). +.. note:: + + The ``prefix_seed`` option is used at compile time. This means + that any change made to this value after container's compilation + will have no effect. + .. versionadded:: 5.2 Starting from Symfony 5.2, the ``%kernel.container_class%`` parameter is no @@ -3356,8 +3513,8 @@ the `SMTP session`_. This value overrides any other recipient set in the code. // config/packages/mailer.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $containerConfigurator): void { - $containerConfigurator->extension('framework', [ + return static function (ContainerConfigurator $container): void { + $container->extension('framework', [ 'mailer' => [ 'dsn' => 'smtp://localhost:25', 'envelope' => [ @@ -3388,6 +3545,21 @@ header name and value the header value. For more information, see :ref:`Configuring Emails Globally ` +messenger +~~~~~~~~~ + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` + +Whether to enable or not Messenger. + +.. seealso:: + + For more details, see the :doc:`Messenger component ` + documentation. + web_link ~~~~~~~~ @@ -3493,7 +3665,7 @@ marking_store Each marking store can define any of these options: -* ``property`` (**type**: ``string`` **default**: ``'marking'``) +* ``property`` (**type**: ``string`` **default**: ``marking``) * ``service`` (**type**: ``string``) * ``type`` (**type**: ``string`` **allow value**: ``'method'``) @@ -3628,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 @@ -3636,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 27869dd074d..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 @@ -448,10 +443,13 @@ redirected to the ``default_target_path`` to avoid a redirection loop. For historical reasons, and to match the misspelling of the HTTP standard, the option is called ``use_referer`` instead of ``use_referrer``. -**Options Related to Logout Configuration** +logout +~~~~~~ + +You can configure logout options. invalidate_session -~~~~~~~~~~~~~~~~~~ +.................. **type**: ``boolean`` **default**: ``true`` @@ -466,14 +464,14 @@ the current firewall and not the other ones. .. _reference-security-logout-success-handler: ``path`` -~~~~~~~~ +........ **type**: ``string`` **default**: ``/logout`` The path which triggers logout. You need to set up a route with a matching path. target -~~~~~~ +...... **type**: ``string`` **default**: ``/`` @@ -482,7 +480,7 @@ starts with ``http://`` or ``https://``) or the route name (otherwise) to redirect after logout. success_handler -~~~~~~~~~~~~~~~ +............... .. deprecated:: 5.1 @@ -491,7 +489,7 @@ success_handler :class:`Symfony\\Component\\Security\\Http\\Event\\LogoutEvent` instead. -**type**: ``string`` **default**: ``'security.logout.success_handler'`` +**type**: ``string`` **default**: ``security.logout.success_handler`` The service ID used for handling a successful logout. The service must implement :class:`Symfony\\Component\\Security\\Http\\Logout\\LogoutSuccessHandlerInterface`. @@ -501,14 +499,14 @@ If it is set, the logout ``target`` option will be ignored. .. _reference-security-logout-csrf: csrf_parameter -~~~~~~~~~~~~~~ +.............. -**type**: ``string`` **default**: ``'_csrf_token'`` +**type**: ``string`` **default**: ``_csrf_token`` The name of the parameter that stores the CSRF token value. csrf_token_generator -~~~~~~~~~~~~~~~~~~~~ +.................... **type**: ``string`` **default**: ``null`` @@ -516,9 +514,9 @@ The ``id`` of the service used to generate the CSRF tokens. Symfony provides a default service whose ID is ``security.csrf.token_manager``. csrf_token_id -~~~~~~~~~~~~~ +............. -**type**: ``string`` **default**: ``'logout'`` +**type**: ``string`` **default**: ``logout`` An arbitrary string used to identify the token (and check its validity afterwards). diff --git a/reference/configuration/twig.rst b/reference/configuration/twig.rst index fc1d4886082..f0e8bd31454 100644 --- a/reference/configuration/twig.rst +++ b/reference/configuration/twig.rst @@ -36,17 +36,17 @@ compiled again automatically. autoescape ~~~~~~~~~~ -**type**: ``boolean`` or ``string`` **default**: ``'name'`` +**type**: ``boolean`` or ``string`` **default**: ``name`` 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`` @@ -83,7 +83,7 @@ called to determine the default escaping applied to the template. base_template_class ~~~~~~~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``'Twig\Template'`` +**type**: ``string`` **default**: ``Twig\Template`` Twig templates are compiled into PHP classes before using them to render contents. This option defines the base class from which all the template classes @@ -93,7 +93,7 @@ application harder to maintain. cache ~~~~~ -**type**: ``string`` | ``false`` **default**: ``'%kernel.cache_dir%/twig'`` +**type**: ``string`` | ``false`` **default**: ``%kernel.cache_dir%/twig`` Before using the Twig templates to render some contents, they are compiled into regular PHP code. Compilation is a costly process, so the result is cached in @@ -107,7 +107,7 @@ compiled again. charset ~~~~~~~ -**type**: ``string`` **default**: ``'%kernel.charset%'`` +**type**: ``string`` **default**: ``%kernel.charset%`` The charset used by the template files. By default it's the same as the value of the :ref:`kernel.charset container parameter `, @@ -160,7 +160,7 @@ If this option is ``false``, the ``dump()`` function doesn't output any contents default_path ~~~~~~~~~~~~ -**type**: ``string`` **default**: ``'%kernel.project_dir%/templates'`` +**type**: ``string`` **default**: ``%kernel.project_dir%/templates`` The path to the directory where Symfony will look for the application Twig templates by default. If you store the templates in more than one directory, use @@ -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/configuration/web_profiler.rst b/reference/configuration/web_profiler.rst index fc95fd96833..f0b11f47064 100644 --- a/reference/configuration/web_profiler.rst +++ b/reference/configuration/web_profiler.rst @@ -30,7 +30,7 @@ Configuration excluded_ajax_paths ~~~~~~~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``'^/((index|app(_[\w]+)?)\.php/)?_wdt'`` +**type**: ``string`` **default**: ``^/((index|app(_[\w]+)?)\.php/)?_wdt`` When the toolbar logs AJAX requests, it matches their URLs against this regular expression. If the URL matches, the request is not displayed in the toolbar. This diff --git a/reference/constraints.rst b/reference/constraints.rst index aa9829b535a..bb506bf4576 100644 --- a/reference/constraints.rst +++ b/reference/constraints.rst @@ -1,84 +1,6 @@ Validation Constraints Reference ================================ -.. toctree:: - :maxdepth: 1 - :hidden: - - constraints/NotBlank - constraints/Blank - constraints/NotNull - constraints/IsNull - constraints/IsTrue - constraints/IsFalse - constraints/Type - - constraints/Email - constraints/ExpressionLanguageSyntax - constraints/Length - constraints/Url - constraints/Regex - constraints/Hostname - constraints/Ip - constraints/Uuid - constraints/Ulid - constraints/Json - - constraints/EqualTo - constraints/NotEqualTo - constraints/IdenticalTo - constraints/NotIdenticalTo - constraints/LessThan - constraints/LessThanOrEqual - constraints/GreaterThan - constraints/GreaterThanOrEqual - constraints/Range - constraints/DivisibleBy - constraints/Unique - - constraints/Positive - constraints/PositiveOrZero - constraints/Negative - constraints/NegativeOrZero - - constraints/Date - constraints/DateTime - constraints/Time - constraints/Timezone - - constraints/Choice - constraints/Collection - constraints/Count - constraints/UniqueEntity - constraints/Language - constraints/Locale - constraints/Country - - constraints/File - constraints/Image - - constraints/CardScheme - constraints/Currency - constraints/Luhn - constraints/Iban - constraints/Bic - constraints/Isbn - constraints/Issn - constraints/Isin - - constraints/AtLeastOneOf - constraints/Sequentially - constraints/Compound - constraints/Callback - constraints/Expression - constraints/All - constraints/UserPassword - constraints/NotCompromisedPassword - constraints/Valid - constraints/Traverse - constraints/CssColor - constraints/Cascade - The Validator is designed to validate objects against *constraints*. In real life, a constraint could be: "The cake must not be burned". In Symfony, constraints are similar: They are assertions that a condition is 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/Collection.rst b/reference/constraints/Collection.rst index 62595aef75e..44d0319bb84 100644 --- a/reference/constraints/Collection.rst +++ b/reference/constraints/Collection.rst @@ -379,7 +379,7 @@ Options ``allowExtraFields`` ~~~~~~~~~~~~~~~~~~~~ -**type**: ``boolean`` **default**: false +**type**: ``boolean`` **default**: ``false`` If this option is set to ``false`` and the underlying collection contains one or more elements that are not included in the `fields`_ option, a validation @@ -388,7 +388,7 @@ error will be returned. If set to ``true``, extra fields are OK. ``allowMissingFields`` ~~~~~~~~~~~~~~~~~~~~~~ -**type**: ``boolean`` **default**: false +**type**: ``boolean`` **default**: ``false`` If this option is set to ``false`` and one or more fields from the `fields`_ option are not present in the underlying collection, a validation error diff --git a/reference/constraints/Count.rst b/reference/constraints/Count.rst index d4e7e796acc..b4d6982b0fb 100644 --- a/reference/constraints/Count.rst +++ b/reference/constraints/Count.rst @@ -115,7 +115,7 @@ Options ``divisibleBy`` ~~~~~~~~~~~~~~~ -**type**: ``integer`` **default**: null +**type**: ``integer`` **default**: ``null`` .. versionadded:: 5.1 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/DisableAutoMapping.rst b/reference/constraints/DisableAutoMapping.rst new file mode 100644 index 00000000000..463d47f5b4d --- /dev/null +++ b/reference/constraints/DisableAutoMapping.rst @@ -0,0 +1,118 @@ +DisableAutoMapping +================== + +This constraint allows to disable :ref:`Doctrine's auto mapping ` +on a class or a property. Automapping allows to determine validation rules based +on Doctrine's annotations and attributes. You may use this constraint when +automapping is globally enabled, but you still want to disable this feature for +a class or a property specifically. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\DisableAutoMapping` +========== =================================================================== + +Basic Usage +----------- + +In the following example, the +:class:`Symfony\\Component\\Validator\\Constraints\\DisableAutoMapping` +constraint will tell the validator to not gather constraints from Doctrine's +metadata: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Model/BookCollection.php + namespace App\Model; + + use App\Model\Author; + use App\Model\BookMetadata; + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Validator\Constraints as Assert; + + /** + * @Assert\DisableAutoMapping + */ + class BookCollection + { + /** + * @ORM\Column(nullable=false) + */ + protected $name = ''; + + /** + * @ORM\ManyToOne(targetEntity=Author::class) + */ + public Author $author; + + // ... + } + + .. code-block:: php-attributes + + // src/Model/BookCollection.php + namespace App\Model; + + use App\Model\Author; + use App\Model\BookMetadata; + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Validator\Constraints as Assert; + + #[Assert\DisableAutoMapping] + class BookCollection + { + #[ORM\Column(nullable: false)] + protected string $name = ''; + + #[ORM\ManyToOne(targetEntity: Author::class)] + public Author $author; + + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\BookCollection: + constraints: + - DisableAutoMapping: ~ + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // src/Entity/BookCollection.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class BookCollection + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addConstraint(new Assert\DisableAutoMapping()); + } + } + +Options +------- + +The ``groups`` option is not available for this constraint. + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/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/EnableAutoMapping.rst b/reference/constraints/EnableAutoMapping.rst new file mode 100644 index 00000000000..4ea5fd74f6d --- /dev/null +++ b/reference/constraints/EnableAutoMapping.rst @@ -0,0 +1,118 @@ +EnableAutoMapping +================= + +This constraint allows to enable :ref:`Doctrine's auto mapping ` +on a class or a property. Automapping allows to determine validation rules based +on Doctrine's annotations and attributes. You may use this constraint when +automapping is globally disabled, but you still want to enable this feature for +a class or a property specifically. + +========== =================================================================== +Applies to :ref:`property or method ` +Class :class:`Symfony\\Component\\Validator\\Constraints\\EnableAutoMapping` +========== =================================================================== + +Basic Usage +----------- + +In the following example, the +:class:`Symfony\\Component\\Validator\\Constraints\\EnableAutoMapping` +constraint will tell the validator to gather constraints from Doctrine's +metadata: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Model/BookCollection.php + namespace App\Model; + + use App\Model\Author; + use App\Model\BookMetadata; + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Validator\Constraints as Assert; + + /** + * @Assert\EnableAutoMapping + */ + class BookCollection + { + /** + * @ORM\Column(nullable=false) + */ + protected $name = ''; + + /** + * @ORM\ManyToOne(targetEntity=Author::class) + */ + public Author $author; + + // ... + } + + .. code-block:: php-attributes + + // src/Model/BookCollection.php + namespace App\Model; + + use App\Model\Author; + use App\Model\BookMetadata; + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Validator\Constraints as Assert; + + #[Assert\EnableAutoMapping] + class BookCollection + { + #[ORM\Column(nullable: false)] + protected string $name = ''; + + #[ORM\ManyToOne(targetEntity: Author::class)] + public Author $author; + + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\BookCollection: + constraints: + - EnableAutoMapping: ~ + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // src/Entity/BookCollection.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class BookCollection + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addConstraint(new Assert\EnableAutoMapping()); + } + } + +Options +------- + +The ``groups`` option is not available for this constraint. + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/EqualTo.rst b/reference/constraints/EqualTo.rst 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 65841a1e26c..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; } @@ -245,7 +245,7 @@ You can find a list of existing mime types on the `IANA website`_. When using this constraint on a :doc:`FileType field `, the value of the ``mimeTypes`` option is also used in the ``accept`` - attribute of the related ```` HTML element. + attribute of the related ```` HTML element. This behavior is applied only when using :ref:`form type guessing ` (i.e. the form type is not defined explicitly in the ``->add()`` method of 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 d4ecf423fd0..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/Traverse.rst b/reference/constraints/Traverse.rst index 2302139cbb9..01dcd4f779c 100644 --- a/reference/constraints/Traverse.rst +++ b/reference/constraints/Traverse.rst @@ -112,7 +112,7 @@ that all have constraints on their properties. /** * @var Collection|Book[] */ - #[ORM\ManyToMany(targetEntity: Book::class)] + #[ORM\ManyToMany(targetEntity: Book::class)] protected $books; // some other properties 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 59840bbfe9b..5ae4b29e8ed 100644 --- a/reference/constraints/UniqueEntity.rst +++ b/reference/constraints/UniqueEntity.rst @@ -126,6 +126,29 @@ between all of the rows in your user table: } } + // src/Form/Type/UserType.php + namespace App\Form\Type; + + // ... + // DON'T forget the following use statement!!! + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + + class UserType extends AbstractType + { + // ... + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + // ... + 'data_class' => User::class, + 'constraints' => [ + new UniqueEntity(fields: ['email']), + ], + ]); + } + } + .. caution:: This constraint doesn't provide any protection against `race conditions`_. @@ -188,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 @@ -217,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 { @@ -236,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 @@ -253,8 +276,8 @@ Consider this example: host port - + @@ -277,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', ])); } } @@ -297,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 a0c1324dd1e..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,27 +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:`Expression ` +* :doc:`GroupSequence ` +* :doc:`Sequentially ` +* :doc:`Traverse ` +* :doc:`Valid ` diff --git a/reference/dic_tags.rst b/reference/dic_tags.rst index e707808e7e2..16480b3fb3c 100644 --- a/reference/dic_tags.rst +++ b/reference/dic_tags.rst @@ -122,8 +122,8 @@ services: use App\Lock\PostgresqlLock; use App\Lock\SqliteLock; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set('app.mysql_lock', MysqlLock::class); $services->set('app.postgresql_lock', PostgresqlLock::class); @@ -184,8 +184,8 @@ the generic ``app.lock`` service can be defined as follows: use App\Lock\PostgresqlLock; use App\Lock\SqliteLock; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set('app.mysql_lock', MysqlLock::class); $services->set('app.postgresql_lock', PostgresqlLock::class); @@ -711,10 +711,10 @@ kernel.reset **Purpose**: Clean up services between requests -During the ``kernel.terminate`` event, Symfony looks for any service tagged -with the ``kernel.reset`` tag to reinitialize their state. This is done by -calling to the method whose name is configured in the ``method`` argument of -the tag. +In all main requests (not :ref:`sub-requests `) except +the first one, Symfony looks for any service tagged with the ``kernel.reset`` tag +to reinitialize their state. This is done by calling to the method whose name is +configured in the ``method`` argument of the tag. This is mostly useful when running your projects in application servers that reuse the Symfony application between requests to improve performance. This tag @@ -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.rst b/reference/forms/types.rst index aeb8d48ece9..26668d6d78a 100644 --- a/reference/forms/types.rst +++ b/reference/forms/types.rst @@ -1,58 +1,6 @@ Form Types Reference ==================== -.. toctree:: - :maxdepth: 1 - :hidden: - - types/text - types/textarea - types/email - types/integer - types/money - types/number - types/password - types/percent - types/search - types/url - types/range - types/tel - types/color - - types/choice - types/enum - types/entity - types/country - types/language - types/locale - types/timezone - types/currency - - types/date - types/dateinterval - types/datetime - types/time - types/birthday - types/week - - types/checkbox - types/file - types/radio - - types/uuid - types/ulid - - types/collection - types/repeated - - types/hidden - - types/button - types/reset - types/submit - - types/form - A form is composed of *fields*, each of which are built with the help of a field *type* (e.g. ``TextType``, ``ChoiceType``, etc). Symfony comes standard with a large list of field types that can be used in your application. 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 f44f25d7545..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()); }, ]); @@ -198,7 +198,7 @@ type:: entry_type ~~~~~~~~~~ -**type**: ``string`` **default**: ``'Symfony\Component\Form\Extension\Core\Type\TextType'`` +**type**: ``string`` **default**: ``Symfony\Component\Form\Extension\Core\Type\TextType`` This is the field type for each item in this collection (e.g. ``TextType``, ``ChoiceType``, etc). For example, if you have an array of email addresses, @@ -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 9a5f06c2a9e..9045bba8cc4 100644 --- a/reference/forms/types/email.rst +++ b/reference/forms/types/email.rst @@ -2,7 +2,7 @@ EmailType Field =============== The ``EmailType`` field is a text field that is rendered using the HTML5 -```` tag. +```` tag. +---------------------------+---------------------------------------------------------------------+ | Rendered as | ``input`` ``email`` field (a text box) | @@ -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/form.rst b/reference/forms/types/form.rst index 9ef474a0063..60d6bde2793 100644 --- a/reference/forms/types/form.rst +++ b/reference/forms/types/form.rst @@ -61,6 +61,8 @@ The actual default value of this option depends on other field options: * If ``data_class`` is not set and ``compound`` is ``false``, then ``''`` (empty string). +.. include:: /reference/forms/types/options/empty_data_description.rst.inc + ``is_empty_callback`` ~~~~~~~~~~~~~~~~~~~~~ @@ -68,8 +70,6 @@ The actual default value of this option depends on other field options: This callable takes form data and returns whether value is considered empty. -.. include:: /reference/forms/types/options/empty_data_description.rst.inc - .. _reference-form-option-error-bubbling: .. include:: /reference/forms/types/options/error_bubbling.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/help.rst.inc b/reference/forms/types/options/help.rst.inc index 86f84111c88..c69e99819b3 100644 --- a/reference/forms/types/options/help.rst.inc +++ b/reference/forms/types/options/help.rst.inc @@ -1,7 +1,7 @@ help ~~~~ -**type**: ``string`` or ``TranslatableMessage`` **default**: null +**type**: ``string`` or ``TranslatableMessage`` **default**: ``null`` Allows you to define a help message for the form field, which by default is rendered below the field:: 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 9da6407f881..294023ce0c6 100644 --- a/reference/forms/types/range.rst +++ b/reference/forms/types/range.rst @@ -2,7 +2,7 @@ RangeType Field =============== The ``RangeType`` field is a slider that is rendered using the HTML5 -```` tag. +```` tag. +---------------------------+---------------------------------------------------------------------+ | Rendered as | ``input`` ``range`` field (slider in HTML5 supported browser) | @@ -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 8eeefb053d5..32db9b3eccb 100644 --- a/reference/forms/types/search.rst +++ b/reference/forms/types/search.rst @@ -1,11 +1,9 @@ SearchType Field ================ -This renders an ```` field, which is a text box with +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/index.rst b/reference/index.rst index 82edbcc0130..38e0e38800e 100644 --- a/reference/index.rst +++ b/reference/index.rst @@ -1,26 +1,4 @@ Reference Documents =================== -.. toctree:: - :hidden: - - configuration/framework - configuration/doctrine - configuration/security - configuration/swiftmailer - configuration/twig - configuration/monolog - configuration/web_profiler - configuration/debug - - configuration/kernel - - forms/types - constraints - - twig_reference - - dic_tags - events - .. include:: /reference/map.rst.inc diff --git a/reference/twig_reference.rst b/reference/twig_reference.rst index 38d96910fd2..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:: @@ -330,7 +332,7 @@ absolute URLs instead of relative URLs. .. _reference-twig-function-t: t -~ +~~~ .. code-block:: twig @@ -365,6 +367,12 @@ explained in the article about :doc:`customizing form rendering ` * :ref:`form_row() ` * :ref:`form_rest() ` +* :ref:`field_name() ` +* :ref:`field_value() ` +* :ref:`field_label() ` +* :ref:`field_help() ` +* :ref:`field_errors() ` +* :ref:`field_choices() ` .. _reference-twig-filters: @@ -383,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 56f969cc331..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. @@ -2429,8 +2443,8 @@ locale. resource: '../../src/Controller/' type: annotation host: - en: 'https://www.example.com' - nl: 'https://www.example.nl' + en: 'www.example.com' + nl: 'www.example.nl' .. code-block:: xml @@ -2441,8 +2455,8 @@ locale. xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd"> - https://www.example.com - https://www.example.nl + www.example.com + www.example.nl @@ -2453,8 +2467,8 @@ locale. return function (RoutingConfigurator $routes) { $routes->import('../../src/Controller/', 'annotation') ->host([ - 'en' => 'https://www.example.com', - 'nl' => 'https://www.example.nl', + 'en' => 'www.example.com', + 'nl' => 'www.example.nl', ]) ; }; @@ -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 @@ -3055,11 +3069,6 @@ or, in Twig: Learn more about Routing ------------------------ -.. toctree:: - :hidden: - - controller - .. toctree:: :maxdepth: 1 :glob: diff --git a/routing/custom_route_loader.rst b/routing/custom_route_loader.rst index 7c050010ed5..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; } @@ -328,8 +328,8 @@ Now define a service for the ``ExtraLoader``: use App\Routing\ExtraLoader; - return static function (ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return static function (ContainerConfigurator $container) { + $services = $container->services(); $services->set(ExtraLoader::class) ->tag('routing.loader') @@ -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 fb0ad14e2ac..4528d0d03b6 100644 --- a/security.rst +++ b/security.rst @@ -125,32 +125,21 @@ from the `MakerBundle`_: use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; - /** - * @ORM\Entity(repositoryClass=UserRepository::class) - */ + #[ORM\Entity(repositoryClass: UserRepository::class)] class User implements UserInterface, PasswordAuthenticatedUserInterface { - /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] private $id; - /** - * @ORM\Column(type="string", length=180, unique=true) - */ + #[ORM\Column(type: 'string', length: 180, unique: true)] private $email; - /** - * @ORM\Column(type="json") - */ + #[ORM\Column(type: 'json')] private $roles = []; - /** - * @var string The hashed password - * @ORM\Column(type="string") - */ + #[ORM\Column(type: 'string')] private $password; public function getId(): ?int @@ -243,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` @@ -256,6 +252,11 @@ to create the tables by :ref:`creating and running a migration + + + @@ -568,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) @@ -587,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. @@ -659,7 +676,18 @@ Form Login 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 *form login authenticator*. +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: @@ -690,7 +718,7 @@ First, create a controller for the login form: } } -Then, enable the form login authenticator using the ``form_login`` setting: +Then, enable the ``FormLoginAuthenticator`` using the ``form_login`` setting: .. configuration-block:: @@ -783,8 +811,8 @@ Edit the login controller to render the login form: } } -Don't let this controller confuse you. Its job is only to *render* the form: -the ``form_login`` authenticator will handle the form *submission* automatically. +Don't let this controller confuse you. Its job is only to *render* the form. +The ``FormLoginAuthenticator`` will handle the form *submission* automatically. If the user submits an invalid email or password, that authenticator will store the error and redirect back to this controller, where we read the error (using ``AuthenticationUtils``) so that it can be displayed back to the user. @@ -805,13 +833,13 @@ Finally, create or update the template:
- + - + {# If you want to control the URL the user is redirected to on success - #} + #}
@@ -838,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. @@ -856,7 +884,7 @@ To review the whole process: #. The ``/login`` page renders login form via the route and controller created in this example; #. The user submits the login form to ``/login``; -#. The security system (i.e. the ``form_login`` authenticator) intercepts the +#. The security system (i.e. the ``FormLoginAuthenticator``) intercepts the request, checks the user's submitted credentials, authenticates the user if they are correct, and sends the user back to the login form if they are not. @@ -1065,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 { @@ -1378,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:: @@ -1463,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`` @@ -1583,7 +1618,7 @@ and set the ``limiter`` option to its service ID: use Symfony\Config\FrameworkConfig; use Symfony\Config\SecurityConfig; - return static function (ContainerBuilder $containerBuilder, FrameworkConfig $framework, SecurityConfig $security) { + return static function (ContainerBuilder $container, FrameworkConfig $framework, SecurityConfig $security) { $framework->rateLimiter() ->limiter('username_ip_login') ->policy('token_bucket') @@ -1599,7 +1634,7 @@ and set the ``limiter`` option to its service ID: ->interval('15 minutes') ; - $containerBuilder->register('app.login_rate_limiter', DefaultLoginRateLimiter::class) + $container->register('app.login_rate_limiter', DefaultLoginRateLimiter::class) ->setArguments([ // 1st argument is the limiter for IP new Reference('limiter.ip_login'), @@ -1613,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 @@ -1697,7 +1745,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={"GET"}) + * @Route("/logout", name="app_logout", methods={"POST"}) */ public function logout(): void { @@ -2542,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 --------------- @@ -2600,8 +2650,8 @@ for these events. use App\EventListener\LogoutSubscriber; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(LogoutSubscriber::class) ->tag('kernel.event_subscriber', [ @@ -2614,7 +2664,9 @@ Authentication Events .. raw:: html - + :class:`Symfony\\Component\\Security\\Http\\Event\\CheckPassportEvent` Dispatched after the authenticator created the :ref:`security passport `. @@ -2644,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`. @@ -2676,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 @@ -2725,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 81aae70c602..c467a294f00 100644 --- a/security/access_control.rst +++ b/security/access_control.rst @@ -91,8 +91,8 @@ Take the following ``access_control`` entries as an example: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Config\SecurityConfig; - return static function (ContainerBuilder $containerBuilder, SecurityConfig $security) { - $containerBuilder->setParameter('env(TRUSTED_IPS)', '10.0.0.1, 10.0.0.2'); + return static function (ContainerBuilder $container, SecurityConfig $security) { + $container->setParameter('env(TRUSTED_IPS)', '10.0.0.1, 10.0.0.2'); // ... $security->accessControl() @@ -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 a03cfc59c00..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; @@ -141,7 +190,7 @@ generate a CSRF token in the template and store it as a hidden form field:
{# the argument of csrf_token() is an arbitrary string used to generate the token #} - +
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/form_login.rst b/security/form_login.rst index ec8f4a1d373..a285f26fcf9 100644 --- a/security/form_login.rst +++ b/security/form_login.rst @@ -157,8 +157,8 @@ Defining the redirect URL via POST using a hidden form field:
{# ... #} - - + +
Using the Referring URL @@ -301,8 +301,8 @@ This option can also be set via the ``_failure_path`` request parameter:
{# ... #} - - + +
Customizing the Target and Failure Request Parameters @@ -380,7 +380,7 @@ are now fully customized:
{# ... #} - - - + + +
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 58fbeb6e959..055c0a783cf 100644 --- a/security/remember_me.rst +++ b/security/remember_me.rst @@ -124,7 +124,7 @@ checkbox must have a name of ``_remember_me``: {# ... your form fields #} @@ -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 @@ -266,12 +267,14 @@ Signature based tokens By default, the remember me cookie contains a signature based on properties of the user. If the properties change, the signature changes and already generated tokens are no longer considered valid. See - :ref:`security-remember-me-signature` for more information. + :ref:`how to use them ` for more + information. Persistent tokens Persistent tokens store any generated token (e.g. in a database). This allows you to invalidate tokens by changing the rows in the database. - See :ref:`security-remember-me-persistent` for more information. + See :ref:`how to store tokens ` for more + information. .. note:: @@ -286,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 b7b62efa843..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; @@ -312,7 +314,7 @@ to your class:: private $name; /** - * @ORM\Column(type="integer") + * @ORM\Column(type="text") * @Groups({"show_product"}) */ private $description; 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 47a421f1345..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: @@ -205,9 +204,9 @@ each time you ask for it. // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $containerConfigurator) { + return function(ContainerConfigurator $container) { // default configuration for services in *this* file - $services = $containerConfigurator->services() + $services = $container->services() ->defaults() ->autowire() // Automatically injects dependencies in your services. ->autoconfigure() // Automatically registers your services as commands, event subscribers, etc. @@ -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: @@ -346,8 +346,8 @@ made. To do that, you create a new class:: class SiteUpdateManager { - private $messageGenerator; - private $mailer; + private MessageGenerator $messageGenerator; + private MailerInterface $mailer; public function __construct(MessageGenerator $messageGenerator, MailerInterface $mailer) { @@ -500,7 +500,7 @@ pass here. No problem! In your configuration, you can explicitly set this argume use App\Service\SiteUpdateManager; - return function(ContainerConfigurator $containerConfigurator) { + return function(ContainerConfigurator $container) { // ... // same as before @@ -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. @@ -575,8 +574,8 @@ parameter and in PHP config use the ``service()`` function: use App\Service\MessageGenerator; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(MessageGenerator::class) // In versions earlier to Symfony 5.1 the service() function was called ref() @@ -682,7 +681,7 @@ But, you can control this and pass in a different logger: use App\Service\MessageGenerator; - return function(ContainerConfigurator $containerConfigurator) { + return function(ContainerConfigurator $container) { // ... same code as before // explicitly configure the service @@ -694,6 +693,12 @@ But, you can control this and pass in a different logger: This tells the container that the ``$logger`` argument to ``__construct`` should use service whose id is ``monolog.logger.request``. +For a list of possible logger services that can be used with autowiring, run: + +.. code-block:: terminal + + $ php bin/console debug:autowiring logger + .. _container-debug-container: For a full list of *all* possible services in the container, run: @@ -702,6 +707,31 @@ For a full list of *all* possible services in the container, run: $ php bin/console debug:container +Remove Services +--------------- + +A service can be removed from the service container if needed. This is useful +for example to make a service unavailable in some :ref:`configuration environment ` +(e.g. in the ``test`` environment): + +.. configuration-block:: + + .. code-block:: php + + // config/services_test.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\RemovedService; + + return function(ContainerConfigurator $containerConfigurator) { + $services = $containerConfigurator->services(); + + $services->remove(RemovedService::class); + }; + +Now, the container will not contain the ``App\RemovedService`` in the ``test`` +environment. + .. _services-binding: Binding Arguments by Name or Type @@ -778,13 +808,10 @@ You can also use the ``bind`` keyword to bind specific arguments by name or type // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - use App\Controller\LuckyController; use Psr\Log\LoggerInterface; - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services() + return function(ContainerConfigurator $container) { + $services = $container->services() ->defaults() // pass this value to any $adminEmail argument for any service // that's defined in this file (including controller arguments) @@ -812,8 +839,79 @@ 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 +-------------------------- + +Sometimes, the values of some service arguments can't be defined in the +configuration files because they are calculated at runtime using a +:doc:`compiler pass ` +or :doc:`bundle extension `. + +In those cases, you can use the ``abstract`` argument type to define at least +the name of the argument and some short description about its purpose: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\Service\MyService: + arguments: + $rootNamespace: !abstract 'should be defined by Pass' + + # ... + + .. code-block:: xml + + + + + + + + should be defined by Pass + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Service\MyService; + use Psr\Log\LoggerInterface; + use Symfony\Component\DependencyInjection\Definition; + use Symfony\Component\DependencyInjection\Reference; + + return function(ContainerConfigurator $container) { + $services = $container->services(); + + $services->set(MyService::class) + ->arg('$rootNamespace', abstract_arg('should be defined by Pass')) + ; + + // ... + }; + +If you don't replace the value of an abstract argument during runtime, a +``RuntimeException`` will be thrown with a message like +``Argument "$rootNamespace" of service "App\Service\MyService" is abstract: should be defined by Pass.`` + +.. versionadded:: 5.1 + + The abstract service arguments were introduced in Symfony 5.1. .. _services-autowire: @@ -848,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 --------------------------- @@ -918,7 +1027,7 @@ setting: use App\Service\PublicService; - return function(ContainerConfigurator $containerConfigurator) { + return function(ContainerConfigurator $container) { // ... same as code before // explicitly configure the service @@ -927,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 @@ -975,7 +1104,7 @@ key. For example, the default Symfony configuration contains this: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $containerConfigurator) { + return function(ContainerConfigurator $container) { // ... // makes classes in src/ available to be used as services @@ -992,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 @@ -1157,7 +1286,7 @@ admin email. In this case, each needs to have a unique service id: use App\Service\MessageGenerator; use App\Service\SiteUpdateManager; - return function(ContainerConfigurator $containerConfigurator) { + return function(ContainerConfigurator $container) { // ... // site_update_manager.superadmin is the service's id @@ -1187,7 +1316,9 @@ admin email. In this case, each needs to have a unique service id: In this case, *two* services are registered: ``site_update_manager.superadmin`` 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 `. + +If you want to pass the second, you'll need to :ref:`manually wire the service ` +or to create a named :ref:`autowiring alias `. .. caution:: diff --git a/service_container/alias_private.rst b/service_container/alias_private.rst index 44a8492a53d..8ccb131cf49 100644 --- a/service_container/alias_private.rst +++ b/service_container/alias_private.rst @@ -55,13 +55,33 @@ You can also control the ``public`` option on a service-by-service basis: use App\Service\Foo; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(Foo::class) ->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 @@ -127,8 +147,8 @@ services. use App\Mail\PhpMailer; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(PhpMailer::class) ->private(); @@ -275,8 +295,8 @@ The following example shows how to inject an anonymous service into another serv use App\AnonymousBar; use App\Foo; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(Foo::class) // In versions earlier to Symfony 5.1 the inline_service() function was called inline() @@ -327,8 +347,8 @@ Using an anonymous service as a factory looks like this: use App\AnonymousBar; use App\Foo; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(Foo::class) ->factory([inline_service(AnonymousBar::class), 'constructFoo']); @@ -373,8 +393,8 @@ or you decided not to maintain it anymore), you can deprecate its definition: use App\Service\OldService; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(OldService::class) ->deprecate( diff --git a/service_container/autowiring.rst b/service_container/autowiring.rst index 39fa1ba5299..6e86ee9c6f2 100644 --- a/service_container/autowiring.rst +++ b/service_container/autowiring.rst @@ -104,8 +104,8 @@ both services: .. code-block:: php // config/services.php - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services() + return function(ContainerConfigurator $container) { + $services = $container->services() ->defaults() ->autowire() ->autoconfigure() @@ -119,7 +119,6 @@ both services: ->autowire(); }; - Now, you can use the ``TwitterClient`` service immediately in a controller:: // src/Controller/DefaultController.php @@ -216,8 +215,8 @@ adding a service alias: # ... # but this fixes it! - # the ``app.rot13.transformer`` service will be injected when - # an ``App\Util\Rot13Transformer`` type-hint is detected + # the "app.rot13.transformer" service will be injected when + # an App\Util\Rot13Transformer type-hint is detected App\Util\Rot13Transformer: '@app.rot13.transformer' .. code-block:: xml @@ -243,7 +242,7 @@ adding a service alias: use App\Util\Rot13Transformer; - return function(ContainerConfigurator $containerConfigurator) { + return function(ContainerConfigurator $container) { // ... // the id is not a class, so it won't be used for autowiring @@ -251,12 +250,11 @@ adding a service alias: ->autowire(); // but this fixes it! - // the ``app.rot13.transformer`` service will be injected when - // an ``App\Util\Rot13Transformer`` type-hint is detected + // the "app.rot13.transformer" service will be injected when + // an App\Util\Rot13Transformer type-hint is detected $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 @@ -350,17 +348,16 @@ To fix that, add an :ref:`alias `: use App\Util\Rot13Transformer; use App\Util\TransformerInterface; - return function(ContainerConfigurator $containerConfigurator) { + return function(ContainerConfigurator $container) { // ... $services->set(Rot13Transformer::class); - // 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 $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``. @@ -423,13 +420,15 @@ Additionally, you can define several named autowiring aliases if you want to use one implementation in some cases, and another implementation in some other cases. +.. _autowiring-alias: + For instance, you may want to use the ``Rot13Transformer`` implementation by default when the ``TransformerInterface`` interface is 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 @@ -465,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' @@ -520,19 +519,19 @@ the injection:: use App\Util\TransformerInterface; use App\Util\UppercaseTransformer; - return function(ContainerConfigurator $containerConfigurator) { + return function(ContainerConfigurator $container) { // ... $services->set(Rot13Transformer::class)->autowire(); $services->set(UppercaseTransformer::class)->autowire(); - // 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 $services->alias(TransformerInterface::class.' $shoutyTransformer', UppercaseTransformer::class); // 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. $services->alias(TransformerInterface::class, Rot13Transformer::class); @@ -554,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). + +.. 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. -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:: +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; @@ -574,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. @@ -735,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/calls.rst b/service_container/calls.rst index a76cedbca2c..a40ca68e29c 100644 --- a/service_container/calls.rst +++ b/service_container/calls.rst @@ -66,7 +66,7 @@ To configure the container to call the ``setLogger`` method, use the ``calls`` k use App\Service\MessageGenerator; - return function(ContainerConfigurator $containerConfigurator) { + return function(ContainerConfigurator $container) { // ... $services->set(MessageGenerator::class) diff --git a/service_container/compiler_passes.rst b/service_container/compiler_passes.rst index 34eee2e67df..fda044a1195 100644 --- a/service_container/compiler_passes.rst +++ b/service_container/compiler_passes.rst @@ -22,9 +22,9 @@ Compiler passes are registered in the ``build()`` method of the application kern // ... - protected function build(ContainerBuilder $containerBuilder): void + protected function build(ContainerBuilder $container): void { - $containerBuilder->addCompilerPass(new CustomPass()); + $container->addCompilerPass(new CustomPass()); } } @@ -50,14 +50,14 @@ and process the services inside the ``process()`` method:: // ... - public function process(ContainerBuilder $containerBuilder): void + public function process(ContainerBuilder $container): void { // in this method you can manipulate the service container: // for example, changing some container service: - $containerBuilder->getDefinition('app.some_private_service')->setPublic(true); + $container->getDefinition('app.some_private_service')->setPublic(true); // or processing tagged services: - foreach ($containerBuilder->findTaggedServiceIds('some_tag') as $id => $tags) { + foreach ($container->findTaggedServiceIds('some_tag') as $id => $tags) { // ... } } @@ -79,11 +79,11 @@ method in the extension):: class MyBundle extends Bundle { - public function build(ContainerBuilder $containerBuilder): void + public function build(ContainerBuilder $container): void { - parent::build($containerBuilder); + parent::build($container); - $containerBuilder->addCompilerPass(new CustomPass()); + $container->addCompilerPass(new CustomPass()); } } diff --git a/service_container/configurators.rst b/service_container/configurators.rst index 055eb541ae8..1d289580815 100644 --- a/service_container/configurators.rst +++ b/service_container/configurators.rst @@ -169,8 +169,8 @@ all the classes are already loaded as services. All you need to do is specify th use App\Mail\GreetingCardManager; use App\Mail\NewsletterManager; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); // Registers all 4 classes as services, including App\Mail\EmailConfigurator $services->load('App\\', '../src/*'); @@ -239,8 +239,8 @@ Services can be configured via invokable configurators (replacing the use App\Mail\GreetingCardManager; use App\Mail\NewsletterManager; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); // Registers all 4 classes as services, including App\Mail\EmailConfigurator $services->load('App\\', '../src/*'); diff --git a/service_container/expression_language.rst b/service_container/expression_language.rst index 908ad68da5a..f1de823e47b 100644 --- a/service_container/expression_language.rst +++ b/service_container/expression_language.rst @@ -55,7 +55,7 @@ to another service: ``App\Mailer``. One way to do this is with an expression: use App\Mail\MailerConfiguration; use App\Mailer; - return function(ContainerConfigurator $containerConfigurator) { + return function(ContainerConfigurator $container) { // ... $services->set(MailerConfiguration::class); @@ -110,8 +110,8 @@ via a ``container`` variable. Here's another example: use App\Mailer; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(Mailer::class) ->args([expr("container.hasParameter('some_param') ? parameter('some_param') : 'default_value'")]); diff --git a/service_container/factories.rst b/service_container/factories.rst index 7c5c87dc004..3f13655c6cb 100644 --- a/service_container/factories.rst +++ b/service_container/factories.rst @@ -74,15 +74,14 @@ create its object: use App\Email\NewsletterManager; use App\Email\NewsletterManagerStaticFactory; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(NewsletterManager::class) // the first argument is the class and the second argument is the static method ->factory([NewsletterManagerStaticFactory::class, 'createNewsletterManager']); }; - .. note:: When using a factory to create services, the value chosen for class @@ -156,8 +155,8 @@ You can omit the class on the factory declaration: use App\Email\NewsletterManager; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); // Note that we are not using service() $services->set(NewsletterManager::class) @@ -218,8 +217,8 @@ Configuration of the service container then looks like this: use App\Email\NewsletterManager; use App\Email\NewsletterManagerFactory; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); // first, create a service for the factory $services->set(NewsletterManagerFactory::class); @@ -297,8 +296,8 @@ method name: use App\Email\NewsletterManager; use App\Email\NewsletterManagerFactory; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(NewsletterManager::class) ->factory(service(InvokableNewsletterManagerFactory::class)); @@ -357,8 +356,8 @@ previous examples takes the ``templating`` service as an argument: use App\Email\NewsletterManager; use App\Email\NewsletterManagerFactory; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(NewsletterManager::class) ->factory([service(NewsletterManagerFactory::class), 'createNewsletterManager']) diff --git a/service_container/import.rst b/service_container/import.rst index 2fed44e16a5..1e0fcfb2cee 100644 --- a/service_container/import.rst +++ b/service_container/import.rst @@ -116,12 +116,12 @@ a relative or absolute path to the imported file: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $containerConfigurator) { - $containerConfigurator->import('services/mailer.php'); + return function(ContainerConfigurator $container) { + $container->import('services/mailer.php'); // If you want to import a whole directory: - $containerConfigurator->import('services/'); + $container->import('services/'); - $services = $containerConfigurator->services() + $services = $container->services() ->defaults() ->autowire() ->autoconfigure() diff --git a/service_container/injection_types.rst b/service_container/injection_types.rst index d88e5139824..d801ae0210d 100644 --- a/service_container/injection_types.rst +++ b/service_container/injection_types.rst @@ -71,8 +71,8 @@ service container configuration: use App\Mail\NewsletterManager; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(NewsletterManager::class) // In versions earlier to Symfony 5.1 the service() function was called ref() @@ -274,8 +274,8 @@ that accepts the dependency:: use App\Mail\NewsletterManager; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(NewsletterManager::class) ->call('setMailer', [service('mailer')]); @@ -356,23 +356,19 @@ Another possibility is setting public fields of the class directly:: use App\Mail\NewsletterManager; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set('app.newsletter_manager', NewsletterManager::class) ->property('mailer', service('mailer')); }; 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 bdac2a0bc46..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 @@ -76,13 +76,12 @@ You can mark the service as ``lazy`` by manipulating its definition: use App\Twig\AppExtension; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $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. @@ -170,8 +169,8 @@ specific interfaces. use App\Twig\AppExtension; use Twig\Extension\ExtensionInterface; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(AppExtension::class) ->lazy() diff --git a/service_container/optional_dependencies.rst b/service_container/optional_dependencies.rst index 8317cd363df..86aa0c2eb22 100644 --- a/service_container/optional_dependencies.rst +++ b/service_container/optional_dependencies.rst @@ -38,8 +38,8 @@ if the service does not exist: use App\Newsletter\NewsletterManager; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(NewsletterManager::class) // In versions earlier to Symfony 5.1 the service() function was called ref() @@ -95,8 +95,8 @@ call if the service exists and remove the method call if it does not: use App\Newsletter\NewsletterManager; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(NewsletterManager::class) ->call('setLogger', [service('logger')->ignoreOnInvalid()]) diff --git a/service_container/parent_services.rst b/service_container/parent_services.rst index 9cab17e2254..b3792dc5a6a 100644 --- a/service_container/parent_services.rst +++ b/service_container/parent_services.rst @@ -119,8 +119,8 @@ avoid duplicated service definitions: use App\Repository\DoctrinePostRepository; use App\Repository\DoctrineUserRepository; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(BaseDoctrineRepository::class) ->abstract() @@ -229,8 +229,8 @@ the child class: use App\Repository\DoctrineUserRepository; // ... - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(BaseDoctrineRepository::class) // ... diff --git a/service_container/service_closures.rst b/service_container/service_closures.rst index 03e142b3455..990ba8b813c 100644 --- a/service_container/service_closures.rst +++ b/service_container/service_closures.rst @@ -55,6 +55,9 @@ argument of type ``service_closure``: App\Service\MyService: arguments: [!service_closure '@mailer'] + # In case the dependency is optional + # arguments: [!service_closure '@?mailer'] + .. code-block:: xml @@ -66,6 +69,11 @@ argument of type ``service_closure``: + + @@ -77,8 +85,8 @@ argument of type ``service_closure``: use App\Service\MyService; - return function (ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function (ContainerConfigurator $container) { + $services = $container->services(); $services->set(MyService::class) ->args([service_closure('mailer')]); @@ -104,7 +112,7 @@ a service closure by wrapping the service reference into an instance of use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; - public function process(ContainerBuilder $containerBuilder): void + public function process(ContainerBuilder $container): void { // ... diff --git a/service_container/service_decoration.rst b/service_container/service_decoration.rst index 1bf0bf86d1e..08bff60b534 100644 --- a/service_container/service_decoration.rst +++ b/service_container/service_decoration.rst @@ -41,8 +41,8 @@ When overriding an existing definition, the original service is lost: use App\Mailer; use App\NewMailer; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(Mailer::class); @@ -98,8 +98,8 @@ but keeps a reference of the old one as ``.inner``: use App\DecoratingMailer; use App\Mailer; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(Mailer::class); @@ -161,8 +161,8 @@ automatically changed to ``'.inner'``): use App\DecoratingMailer; use App\Mailer; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(Mailer::class); @@ -233,8 +233,8 @@ automatically changed to ``'.inner'``): use App\DecoratingMailer; use App\Mailer; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(Mailer::class); @@ -295,8 +295,8 @@ the ``decoration_priority`` option. Its value is an integer that defaults to // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(\Foo::class); @@ -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())); @@ -382,8 +381,8 @@ ordered services, each one decorating the next: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $containerConfigurator) { - $containerConfigurator->services() + return function(ContainerConfigurator $container) { + $container->services() ->stack('decorated_foo_stack', [ inline_service(\Baz::class)->args([service('.inner')]), inline_service(\Bar::class)->args([service('.inner')]), @@ -465,8 +464,8 @@ advanced example of composition: use App\Decorated; use App\Decorator; - return function(ContainerConfigurator $containerConfigurator) { - $containerConfigurator->services() + return function(ContainerConfigurator $container) { + $container->services() ->set('some_decorator', Decorator::class) ->stack('embedded_stack', [ @@ -583,8 +582,8 @@ Three different behaviors are available: use Symfony\Component\DependencyInjection\ContainerInterface; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(Foo::class); diff --git a/service_container/service_subscribers_locators.rst b/service_container/service_subscribers_locators.rst index 86389a71144..530519afd52 100644 --- a/service_container/service_subscribers_locators.rst +++ b/service_container/service_subscribers_locators.rst @@ -233,8 +233,8 @@ service type to a service. use App\CommandBus; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(CommandBus::class) ->tag('container.service_subscriber', ['key' => 'logger', 'id' => 'monolog.logger.event']); @@ -325,8 +325,8 @@ or directly via PHP attributes: use App\CommandBus; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(CommandBus::class) ->args([service_locator([ @@ -409,8 +409,8 @@ other services. To do so, create a new service definition using the use Symfony\Component\DependencyInjection\ServiceLocator; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set('app.command_handler_locator', ServiceLocator::class) // In versions earlier to Symfony 5.1 the service() function was called ref() @@ -471,8 +471,8 @@ Now you can inject the service locator in any other services: use App\CommandBus; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(CommandBus::class) ->args([service('app.command_handler_locator')]); @@ -490,7 +490,7 @@ will share identical locators among all the services referencing them:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; - public function process(ContainerBuilder $containerBuilder): void + public function process(ContainerBuilder $container): void { // ... @@ -499,20 +499,23 @@ will share identical locators among all the services referencing them:: 'logger' => new Reference('logger'), ]; - $myService = $containerBuilder->findDefinition(MyService::class); + $myService = $container->findDefinition(MyService::class); - $myService->addArgument(ServiceLocatorTagPass::register($containerBuilder, $locateableServices)); + $myService->addArgument(ServiceLocatorTagPass::register($container, $locateableServices)); } 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:: @@ -579,8 +582,8 @@ of the ``key`` tag attribute (as defined in the ``index_by`` locator option): // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(App\Handler\One::class) ->tag('app.handler', ['key' => 'handler_one']) @@ -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"> - - + + @@ -686,18 +685,28 @@ attribute to the locator service defining the name of this custom method: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $containerConfigurator) { - $containerConfigurator->services() - ->set(App\HandlerCollection::class) - ->args([tagged_locator('app.handler', 'key', 'myOwnMethodName')]) + return function(ContainerConfigurator $container) { + $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 003ad2914b7..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 @@ -33,8 +46,8 @@ in your service definition: use App\SomeNonSharedService; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(SomeNonSharedService::class) ->share(false); diff --git a/service_container/synthetic_services.rst b/service_container/synthetic_services.rst index 4dfec92709f..c43a15034d0 100644 --- a/service_container/synthetic_services.rst +++ b/service_container/synthetic_services.rst @@ -63,15 +63,14 @@ configuration: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); // synthetic services don't specify a class $services->set('app.synthetic_service') ->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 8777639cd60..18f22a9ffa8 100644 --- a/service_container/tags.rst +++ b/service_container/tags.rst @@ -37,14 +37,13 @@ example: use App\Twig\AppExtension; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(AppExtension::class) ->tag('twig.extension'); }; - Services tagged with the ``twig.extension`` tag are collected during the initialization of TwigBundle and added to Twig as extensions. @@ -103,8 +102,8 @@ If you want to apply tags automatically for your own services, use the use App\Security\CustomInterface; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); // this config only applies to the services created by this file $services @@ -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:: @@ -147,9 +151,9 @@ In a Symfony application, call this method in your kernel class:: { // ... - protected function build(ContainerBuilder $containerBuilder): void + protected function build(ContainerBuilder $container): void { - $containerBuilder->registerForAutoconfiguration(CustomInterface::class) + $container->registerForAutoconfiguration(CustomInterface::class) ->addTag('app.custom_tag') ; } @@ -163,9 +167,9 @@ In a Symfony bundle, call this method in the ``load()`` method of the { // ... - public function load(array $configs, ContainerBuilder $containerBuilder): void + public function load(array $configs, ContainerBuilder $container): void { - $containerBuilder->registerForAutoconfiguration(CustomInterface::class) + $container->registerForAutoconfiguration(CustomInterface::class) ->addTag('app.custom_tag') ; } @@ -202,11 +206,11 @@ method:: { // ... - protected function build(ContainerBuilder $containerBuilder): void + protected function build(ContainerBuilder $container): void { // ... - $containerBuilder->registerAttributeForAutoconfiguration(SensitiveElement::class, static function (ChildDefinition $definition, SensitiveElement $attribute, \ReflectionClass $reflector): void { + $container->registerAttributeForAutoconfiguration(SensitiveElement::class, static function (ChildDefinition $definition, SensitiveElement $attribute, \ReflectionClass $reflector): void { // Apply the 'app.sensitive_element' tag to all classes with SensitiveElement // attribute, and attach the token value to the tag $definition->addTag('app.sensitive_element', ['token' => $attribute->getToken()]); @@ -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 -------------------- @@ -284,13 +341,12 @@ Then, define the chain as a service: use App\Mail\TransportChain; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(TransportChain::class); }; - Define Services with a Custom Tag ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -338,8 +394,8 @@ For example, you may add the following transports as services: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(\MailerSmtpTransport::class) // the param() method was introduced in Symfony 5.2. @@ -374,17 +430,17 @@ container for any services with the ``app.mail_transport`` tag:: class MailTransportPass implements CompilerPassInterface { - public function process(ContainerBuilder $containerBuilder): void + public function process(ContainerBuilder $container): void { // always first check if the primary service is defined - if (!$containerBuilder->has(TransportChain::class)) { + if (!$container->has(TransportChain::class)) { return; } - $definition = $containerBuilder->findDefinition(TransportChain::class); + $definition = $container->findDefinition(TransportChain::class); // find all service IDs with the app.mail_transport tag - $taggedServices = $containerBuilder->findTaggedServiceIds('app.mail_transport'); + $taggedServices = $container->findTaggedServiceIds('app.mail_transport'); foreach ($taggedServices as $id => $tags) { // add the transport service to the TransportChain service @@ -411,9 +467,9 @@ or from your kernel:: { // ... - protected function build(ContainerBuilder $containerBuilder): void + protected function build(ContainerBuilder $container): void { - $containerBuilder->addCompilerPass(new MailTransportPass()); + $container->addCompilerPass(new MailTransportPass()); } } @@ -501,8 +557,8 @@ To answer this, change the service declaration: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(\MailerSmtpTransport::class) // the param() method was introduced in Symfony 5.2. @@ -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 @@ -545,7 +646,7 @@ use this, update the compiler:: class TransportCompilerPass implements CompilerPassInterface { - public function process(ContainerBuilder $containerBuilder): void + public function process(ContainerBuilder $container): void { // ... @@ -655,8 +756,8 @@ directly via PHP attributes: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(App\Handler\One::class) ->tag('app.handler') @@ -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. @@ -720,8 +829,8 @@ the number, the earlier the tagged service will be located in the collection: use App\Handler\One; - return function(ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function(ContainerConfigurator $container) { + $services = $container->services(); $services->set(One::class) ->tag('app.handler', ['priority' => 20]) @@ -796,8 +905,8 @@ you can define it in the configuration of the collecting service: use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; - return function (ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function (ContainerConfigurator $container) { + $services = $container->services(); // ... @@ -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. + +The ``index_by`` / ``indexAttribute`` Option +............................................ -Using the previous example, this service configuration creates a collection -indexed by the ``key`` attribute: +This option defines the name of the option/attribute that stores the value used +to index the services: .. configuration-block:: @@ -884,8 +996,8 @@ indexed by the ``key`` attribute: use App\Handler\Two; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; - return function (ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return function (ContainerConfigurator $container) { + $services = $container->services(); $services->set(One::class) ->tag('app.handler', ['key' => 'handler_one']); @@ -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,101 +1026,104 @@ 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']; } } -.. tip:: +If some service doesn't define the option/attribute configured in ``index_by``, +Symfony applies this fallback process: + +#. 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 +................................... + +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:: + + .. code-block:: php-attributes - Just like the priority, you can also implement a static - ``getDefaultIndexName()`` method in the handlers and omit the - index attribute (``key``):: + // src/HandlerCollection.php + namespace App; - // src/Handler/One.php - namespace App\Handler; + use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; - class One + class HandlerCollection { - // ... - public static function getDefaultIndexName(): string - { - return 'handler_one'; + public function __construct( + #[TaggedIterator('app.handler', defaultIndexMethod: 'getIndex')] + iterable $handlers + ) { } } - 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: - - .. configuration-block:: + .. code-block:: yaml - .. code-block:: php-attributes + # config/services.yaml + services: + # ... - // src/HandlerCollection.php - namespace App; + App\HandlerCollection: + arguments: [!tagged_iterator { tag: 'app.handler', default_index_method: 'getIndex' }] - use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; + .. code-block:: xml - class HandlerCollection - { - public function __construct( - #[TaggedIterator('app.handler', defaultIndexMethod: 'getIndex')] - iterable $handlers - ) { - } - } + + + - .. code-block:: yaml + + - # config/services.yaml - services: - # ... + + + + + - App\HandlerCollection: - # use getIndex() instead of getDefaultIndexName() - arguments: [!tagged_iterator { tag: 'app.handler', default_index_method: 'getIndex' }] + .. code-block:: php - .. code-block:: xml + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - - + use App\HandlerCollection; + use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; - - - - - - - - - + return function (ContainerConfigurator $container) { + $services = $container->services(); - .. code-block:: php + // ... - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; + $services->set(HandlerCollection::class) + ->args([ + tagged_iterator('app.handler', null, 'getIndex'), + ]) + ; + }; - use App\HandlerCollection; - use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; +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. - return function (ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->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: - // use getIndex() instead of getDefaultIndexName() - $services->set(HandlerCollection::class) - ->args([ - tagged_iterator('app.handler', null, 'getIndex'), - ]) - ; - }; +#. 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: @@ -1034,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 1124eea36a8..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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -517,6 +551,8 @@ a Symfony service for the connection to the Redis server: .. configuration-block:: + .. code-block:: yaml + # config/services.yaml services: # ... @@ -530,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)%' @@ -665,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) @@ -723,8 +770,8 @@ To use it, first register a new handler service with your database credentials: use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; - return static function (ContainerConfigurator $containerConfiguratorConfigurator) { - $services = $containerConfigurator->services(); + return static function (ContainerConfigurator $container) { + $services = $container->services(); $services->set(PdoSessionHandler::class) ->args([ @@ -838,8 +885,8 @@ passed to the ``PdoSessionHandler`` service: use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; - return static function (ContainerConfigurator $containerConfiguratorConfigurator) { - $services = $containerConfigurator->services(); + return static function (ContainerConfigurator $container) { + $services = $container->services(); $services->set(PdoSessionHandler::class) ->args([ @@ -972,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:: @@ -985,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 @@ -1000,6 +1054,10 @@ the MongoDB connection as argument: doctrine_mongodb.odm.default_connection + + %env('MONGODB_DB')% + sessions + @@ -1011,12 +1069,13 @@ the MongoDB connection as argument: use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler; - return static function (ContainerConfigurator $containerConfiguratorConfigurator) { - $services = $containerConfigurator->services(); + return static function (ContainerConfigurator $container) { + $services = $container->services(); $services->set(MongoDbSessionHandler::class) ->args([ service('doctrine_mongodb.odm.default_connection'), + ['database' => '%env("MONGODB_DB")%', 'collection' => 'sessions'] ]) ; }; @@ -1066,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 @@ -1101,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 @@ -1116,6 +1172,8 @@ configure these values with the second argument passed to the doctrine_mongodb.odm.default_connection + %env('MONGODB_DB')% + sessions _guid eol @@ -1130,13 +1188,18 @@ configure these values with the second argument passed to the use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler; - return static function (ContainerConfigurator $containerConfigurator) { - $services = $containerConfigurator->services(); + return static function (ContainerConfigurator $container) { + $services = $container->services(); $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', + ], ]) ; }; @@ -1466,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 ca52f8bfc69..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 @@ -301,11 +303,6 @@ With setup behind you, it's time to :doc:`Create your first page in Symfony ` 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 5941ba2908f..191dd6a4e02 100644 --- a/setup/flex_private_recipes.rst +++ b/setup/flex_private_recipes.rst @@ -8,7 +8,7 @@ private Symfony Flex recipe repositories, and seamlessly integrate them into the This is particularly useful when you have private bundles or packages that must perform their own installation tasks. To do this, you need to complete several steps: -* Create a private GitHub repository; +* Create a private repository; * Create your private recipes; * Create an index to the recipes; * Store your recipes in the private repository; @@ -16,14 +16,26 @@ perform their own installation tasks. To do this, you need to complete several s * Configure your project's ``composer.json`` file; and * Install the recipes in your project. -Create a Private GitHub Repository ----------------------------------- +.. _create-a-private-github-repository: + +Create a Private Repository +--------------------------- + +GitHub +~~~~~~ Log in to your GitHub.com account, click your account icon in the top-right corner, and select **Your Repositories**. Then click the **New** button, fill in the **repository name**, select the **Private** radio button, and click the **Create Repository** button. +Gitlab +~~~~~~ + +Log in to your Gitlab.com account, click the **New project** button, select +**Create blank project**, fill in the **Project name**, select the **Private** +radio button, and click the **Create project** button. + Create Your Private Recipes --------------------------- @@ -124,6 +136,9 @@ Create an Index to the Recipes The next step is to create an ``index.json`` file, which will contain entries for all your private recipes, and other general configuration information. +GitHub +~~~~~~ + The ``index.json`` file has the following format: .. code-block:: json @@ -134,11 +149,11 @@ The ``index.json`` file has the following format: "1.0" ] }, - "branch": "master", + "branch": "main", "is_contrib": true, "_links": { "repository": "github.com/your-github-account-name/your-recipes-repository", - "origin_template": "{package}:{version}@github.com/your-github-account-name/your-recipes-repository:master", + "origin_template": "{package}:{version}@github.com/your-github-account-name/your-recipes-repository:main", "recipe_template": "https://api.github.com/repos/your-github-account-name/your-recipes-repository/contents/{package_dotted}.{version}.json" } } @@ -146,15 +161,44 @@ The ``index.json`` file has the following format: Create an entry in ``"recipes"`` for each of your bundle recipes. Replace ``your-github-account-name`` and ``your-recipes-repository`` with your own details. +Gitlab +~~~~~~ + +The ``index.json`` file has the following format: + +.. code-block:: json + + { + "recipes": { + "acme/private-bundle": [ + "1.0" + ] + }, + "branch": "main", + "is_contrib": true, + "_links": { + "repository": "gitlab.com/your-gitlab-account-name/your-recipes-repository", + "origin_template": "{package}:{version}@gitlab.com/your-gitlab-account-name/your-recipes-repository:main", + "recipe_template": "https://gitlab.com/api/v4/projects/your-gitlab-project-id/repository/files/{package_dotted}.{version}.json/raw?ref=main" + } + } + +Create an entry in ``"recipes"`` for each of your bundle recipes. Replace +``your-gitlab-account-name``, ``your-gitlab-repository`` and ``your-gitlab-project-id`` +with your own details. + Store Your Recipes in the Private Repository -------------------------------------------- Upload the recipe ``.json`` file(s) and the ``index.json`` file into the root -directory of your private GitHub repository. +directory of your private repository. Grant ``composer`` Access to the Private Repository --------------------------------------------------- +GitHub +~~~~~~ + In your GitHub account, click your account icon in the top-right corner, select ``Settings`` and ``Developer Settings``. Then select ``Personal Access Tokens``. @@ -168,9 +212,28 @@ computer, and execute the following command: Replace ``[token]`` with the value of your GitHub personal access token. +Gitlab +~~~~~~ + +In your Gitlab account, click your account icon in the top-right corner, select +``Preferences`` and ``Access Tokens``. + +Generate a new personal access token with ``read_api`` and ``read_repository`` +scopes. Copy the access token value, switch to the terminal of your local +computer, and execute the following command: + +.. code-block:: terminal + + $ 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 ----------------------------------------------- +GitHub +~~~~~~ + Add the following to your project's ``composer.json`` file: .. code-block:: json @@ -199,6 +262,32 @@ Replace ``your-github-account-name`` and ``your-recipes-repository`` with your o The ``endpoint`` URL **must** point to ``https://api.github.com/repos`` and **not** to ``https://www.github.com``. +Gitlab +~~~~~~ + +Add the following to your project's ``composer.json`` file: + +.. code-block:: json + + { + "extra": { + "symfony": { + "endpoint": [ + "https://gitlab.com/api/v4/projects/your-gitlab-project-id/repository/files/index.json/raw?ref=main", + "flex://defaults" + ] + } + } + } + +Replace ``your-gitlab-project-id`` with your own details. + +.. tip:: + + The ``extra.symfony`` key will most probably already exist in your + ``composer.json``. In that case, add the ``"endpoint"`` key to the existing + ``extra.symfony`` entry. + Install the Recipes in Your Project ----------------------------------- @@ -218,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 d6faf5f81c5..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. @@ -54,6 +54,12 @@ And sometimes, the warning may come from a third-party library or bundle that you're using. If that's true, there's a good chance that those deprecations have already been updated. In that case, upgrade the library to fix them. +.. tip:: + + `Rector`_ is a third-party project that automates the upgrading and + refactoring of PHP projects. Rector includes some rules to fix certain + Symfony deprecations automatically. + Once all the deprecation warnings are gone, you can upgrade with a lot more confidence. @@ -146,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", }, @@ -164,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 @@ -314,3 +340,5 @@ Classes in the ``vendor/`` directory are always ignored. Now, you can safely allow ``^6.0`` for the Symfony dependencies. .. _`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/upgrade_minor.rst b/setup/upgrade_minor.rst index bb1cfda62fa..9e8c6943d1f 100644 --- a/setup/upgrade_minor.rst +++ b/setup/upgrade_minor.rst @@ -84,6 +84,12 @@ included in the Symfony directory that describes these changes. If you follow the instructions in the document and update your code accordingly, it should be safe to update in the future. +.. tip:: + + `Rector`_ is a third-party project that automates the upgrading and + refactoring of PHP projects. Rector includes some rules to fix certain + Symfony deprecations automatically. + These documents can also be found in the `Symfony Repository`_. .. _updating-flex-recipes: @@ -92,3 +98,4 @@ These documents can also be found in the `Symfony Repository`_. .. _`Symfony Repository`: https://github.com/symfony/symfony .. _`UPGRADE-5.4.md`: https://github.com/symfony/symfony/blob/5.4/UPGRADE-5.4.md +.. _`Rector`: https://github.com/rectorphp/rector diff --git a/setup/web_server_configuration.rst b/setup/web_server_configuration.rst index f5f259413b5..e5a0c9e7fd9 100644 --- a/setup/web_server_configuration.rst +++ b/setup/web_server_configuration.rst @@ -5,17 +5,12 @@ The preferred way to develop your Symfony application is to use :doc:`Symfony Local Web Server `. However, when running the application in the production environment, you'll need -to use a fully-featured web server. This article describes several ways to use -Symfony with Apache or Nginx. +to use a fully-featured web server. This article describes how to use Symfony +with Apache, Nginx or Caddy. -When using Apache, you can configure PHP as an -:ref:`Apache module ` or with FastCGI using -:ref:`PHP FPM `. FastCGI also is the preferred way -to use PHP :ref:`with Nginx `. +.. sidebar:: The public directory -.. sidebar:: The ``public/`` directory - - The ``public/`` directory is the home of all of your application's public and + The public directory is the home of all of your application's public and static files, including images, stylesheets and JavaScript files. It is also where the front controller (``index.php``) lives. @@ -27,7 +22,86 @@ to use PHP :ref:`with Nginx `. another location (e.g. ``public_html/``) make sure you :ref:`override the location of the public/ directory `. -.. _web-server-nginx: +Configuring PHP-FPM +------------------- + +All configuration examples below use the PHP FastCGI process manager +(PHP-FPM). Ensure that you have installed PHP-FPM (for example, on a Debian +based system you have to install the ``php-fpm`` package). + +PHP-FPM uses so-called *pools* to handle incoming FastCGI requests. You can +configure an arbitrary number of pools in the FPM configuration. In a pool +you configure either a TCP socket (IP and port) or a Unix domain socket to +listen on. Each pool can also be run under a different UID and GID: + +.. code-block:: ini + + ; /etc/php/8.3/fpm/pool.d/www.conf + + ; a pool called www + [www] + user = www-data + group = www-data + + ; use a unix domain socket + listen = /var/run/php/php8.3-fpm.sock + + ; or listen on a TCP connection + ; listen = 127.0.0.1:9000 + +Apache +------ + +If you are running Apache 2.4+, you can use ``mod_proxy_fcgi`` to pass +incoming requests to PHP-FPM. Install the Apache2 FastCGI mod +(``libapache2-mod-fastcgi`` on Debian), enable ``mod_proxy`` and +``mod_proxy_fcgi`` in your Apache configuration, and use the ``SetHandler`` +directive to pass requests for PHP files to PHP FPM: + +.. code-block:: apache + + # /etc/apache2/conf.d/example.com.conf + + ServerName example.com + ServerAlias www.example.com + + # Uncomment the following line to force Apache to pass the Authorization + # header to PHP: required for "basic_auth" under PHP-FPM and FastCGI + # + # SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=$1 + + + # when using PHP-FPM as a unix socket + 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 + + + DocumentRoot /var/www/project/public + + AllowOverride None + Require all granted + FallbackResource /index.php + + + # uncomment the following lines if you install assets as symlinks + # or run into problems when compiling LESS/Sass/CoffeeScript assets + # + # Options FollowSymlinks + # + + ErrorLog /var/log/apache2/project_error.log + 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 ----- @@ -36,8 +110,9 @@ The **minimum configuration** to get your application running under Nginx is: .. code-block:: nginx + # /etc/nginx/conf.d/example.com.conf server { - server_name domain.tld www.domain.tld; + server_name example.com www.example.com; root /var/www/project/public; location / { @@ -53,7 +128,12 @@ The **minimum configuration** to get your application running under Nginx is: # } location ~ ^/index\.php(/|$) { - fastcgi_pass unix:/var/run/php/php-fpm.sock; + # when using PHP-FPM as a unix socket + 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; + fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; @@ -75,7 +155,7 @@ The **minimum configuration** to get your application running under Nginx is: fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; # Prevents URIs that include the front controller. This will 404: - # http://domain.tld/index.php/some-path + # http://example.com/index.php/some-path # Remove the internal directive to allow URIs like this internal; } @@ -95,11 +175,6 @@ The **minimum configuration** to get your application running under Nginx is: If you use NGINX Unit, check out the official article about `How to run Symfony applications using NGINX Unit`_. -.. note:: - - Depending on your PHP-FPM config, the ``fastcgi_pass`` can also be - ``fastcgi_pass 127.0.0.1:9000``. - .. tip:: This executes **only** ``index.php`` in the public directory. All other files @@ -115,269 +190,46 @@ The **minimum configuration** to get your application running under Nginx is: For advanced Nginx configuration options, read the official `Nginx documentation`_. -.. _web-server-apache-mod-php: - -Adding Rewrite Rules for Apache -------------------------------- - -The easiest way is to install the ``apache`` :ref:`Symfony pack ` -by executing the following command: - -.. code-block:: terminal - - $ composer require symfony/apache-pack - -This pack installs a ``.htaccess`` file in the ``public/`` directory that contains -the rewrite rules needed to serve the Symfony application. - -In production servers, you should move the ``.htaccess`` rules into the main -Apache configuration file to improve performance. To do so, copy the -``.htaccess`` contents inside the ```` configuration associated to -the Symfony application ``public/`` directory (and replace ``AllowOverride All`` -by ``AllowOverride None``): - -.. code-block:: apache - - - # ... - DocumentRoot /var/www/project/public - - - AllowOverride None - - # Copy .htaccess contents here - - - -Apache with mod_php/PHP-CGI ---------------------------- - -The **minimum configuration** to get your application running under Apache is: - -.. code-block:: apache - - - ServerName domain.tld - ServerAlias www.domain.tld - - DocumentRoot /var/www/project/public - - AllowOverride All - Order Allow,Deny - Allow from All - - - # uncomment the following lines if you install assets as symlinks - # or run into problems when compiling LESS/Sass/CoffeeScript assets - # - # Options FollowSymlinks - # - - ErrorLog /var/log/apache2/project_error.log - CustomLog /var/log/apache2/project_access.log combined - - -.. tip:: - - If your system supports the ``APACHE_LOG_DIR`` variable, you may want - to use ``${APACHE_LOG_DIR}/`` instead of hardcoding ``/var/log/apache2/``. - -Use the following **optimized configuration** to disable ``.htaccess`` support -and increase web server performance: - -.. code-block:: apache - - - ServerName domain.tld - ServerAlias www.domain.tld - - DocumentRoot /var/www/project/public - DirectoryIndex /index.php - - - AllowOverride None - Order Allow,Deny - Allow from All - - FallbackResource /index.php - - - # uncomment the following lines if you install assets as symlinks - # or run into problems when compiling LESS/Sass/CoffeeScript assets - # - # Options FollowSymlinks - # - - # optionally disable the fallback resource for the asset directories - # which will allow Apache to return a 404 error when files are - # not found instead of passing the request to Symfony - - DirectoryIndex disabled - FallbackResource disabled - - ErrorLog /var/log/apache2/project_error.log - CustomLog /var/log/apache2/project_access.log combined - - # optionally set the value of the environment variables used in the application - #SetEnv APP_ENV prod - #SetEnv APP_SECRET - #SetEnv DATABASE_URL "mysql://db_user:db_pass@host:3306/db_name" - - -.. caution:: - - Use ``FallbackResource`` on Apache 2.4.25 or higher, due to a bug which was - fixed on that release causing the root ``/`` to hang. - -.. tip:: - - If you are using **php-cgi**, Apache does not pass HTTP basic username and - password to PHP by default. To work around this limitation, you should use - the following configuration snippet: - - .. code-block:: apache - - RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] - -Using mod_php/PHP-CGI with Apache 2.4 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In Apache 2.4, ``Order Allow,Deny`` has been replaced by ``Require all granted``. -Hence, you need to modify your ``Directory`` permission settings as follows: - -.. code-block:: apache - - - Require all granted - # ... - - -For advanced Apache configuration options, read the official `Apache documentation`_. - -.. _web-server-apache-fpm: - -Apache with PHP-FPM -------------------- - -To make use of PHP-FPM with Apache, you first have to ensure that you have -the FastCGI process manager ``php-fpm`` binary and Apache's FastCGI module -installed (for example, on a Debian based system you have to install the -``libapache2-mod-fastcgi`` and ``php-fpm`` packages). - -PHP-FPM uses so-called *pools* to handle incoming FastCGI requests. You can -configure an arbitrary number of pools in the FPM configuration. In a pool -you configure either a TCP socket (IP and port) or a Unix domain socket to -listen on. Each pool can also be run under a different UID and GID: - -.. code-block:: ini - - ; a pool called www - [www] - user = www-data - group = www-data - - ; use a unix domain socket - listen = /var/run/php/php-fpm.sock - - ; or listen on a TCP socket - listen = 127.0.0.1:9000 - -Using mod_proxy_fcgi with Apache 2.4 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you are running Apache 2.4, you can use ``mod_proxy_fcgi`` to pass incoming -requests to PHP-FPM. Configure PHP-FPM to listen on a TCP or Unix socket, enable -``mod_proxy`` and ``mod_proxy_fcgi`` in your Apache configuration, and use the -``SetHandler`` directive to pass requests for PHP files to PHP FPM: - -.. code-block:: apache - - - ServerName domain.tld - ServerAlias www.domain.tld - - # Uncomment the following line to force Apache to pass the Authorization - # header to PHP: required for "basic_auth" under PHP-FPM and FastCGI - # - # SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=$1 - - # For Apache 2.4.9 or higher - # Using SetHandler avoids issues with using ProxyPassMatch in combination - # with mod_rewrite or mod_autoindex - - SetHandler proxy:fcgi://127.0.0.1:9000 - # for Unix sockets, Apache 2.4.10 or higher - # SetHandler proxy:unix:/path/to/fpm.sock|fcgi://dummy - - - # If you use Apache version below 2.4.9 you must consider update or use this instead - # ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9000/var/www/project/public/$1 - - # If you run your Symfony application on a subpath of your document root, the - # regular expression must be changed accordingly: - # ProxyPassMatch ^/path-to-app/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9000/var/www/project/public/$1 - - DocumentRoot /var/www/project/public - - # enable the .htaccess rewrites - AllowOverride All - Require all granted - - - # uncomment the following lines if you install assets as symlinks - # or run into problems when compiling LESS/Sass/CoffeeScript assets - # - # Options FollowSymlinks - # - - ErrorLog /var/log/apache2/project_error.log - CustomLog /var/log/apache2/project_access.log combined - - -PHP-FPM with Apache 2.2 -~~~~~~~~~~~~~~~~~~~~~~~ - -On Apache 2.2 or lower, you cannot use ``mod_proxy_fcgi``. You have to use -the `FastCgiExternalServer`_ directive instead. Therefore, your Apache configuration -should look something like this: - -.. code-block:: apache - - - ServerName domain.tld - ServerAlias www.domain.tld +Caddy +----- - AddHandler php7-fcgi .php - Action php7-fcgi /php7-fcgi - Alias /php7-fcgi /usr/lib/cgi-bin/php7-fcgi - FastCgiExternalServer /usr/lib/cgi-bin/php7-fcgi -host 127.0.0.1:9000 -pass-header Authorization +When using Caddy on the server, you can use a configuration like this: - DocumentRoot /var/www/project/public - - # enable the .htaccess rewrites - AllowOverride All - Order Allow,Deny - Allow from all - +.. code-block:: text - # uncomment the following lines if you install assets as symlinks - # or run into problems when compiling LESS/Sass/CoffeeScript assets - # - # Options FollowSymlinks - # + # /etc/caddy/Caddyfile + example.com, www.example.com { + root * /var/www/project/public - ErrorLog /var/log/apache2/project_error.log - CustomLog /var/log/apache2/project_access.log combined - + # serve files directly if they can be found (e.g. CSS or JS files in public/) + encode zstd gzip + file_server -If you prefer to use a Unix socket, you have to use the ``-socket`` option -instead: + # otherwise, use PHP-FPM (replace "unix//var/..." with "127.0.0.1:9000" when using TCP) + 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 "" + # env DATABASE_URL "mysql://db_user:db_pass@host:3306/db_name" + + # Configure the FastCGI to resolve any symlinks in the root path. + # This ensures that OpCache is using the destination filenames, + # instead of the symlinks, to cache opcodes and php files see + # https://caddy.community/t/root-symlink-folder-updates-and-caddy-reload-not-working/10557 + resolve_root_symlink + } -.. code-block:: apache + # return 404 for all other php files not matching the front controller + # this prevents access to other php files you don't want to be accessible. + @phpFile { + path *.php* + } + error @phpFile "Not found" 404 + } - FastCgiExternalServer /usr/lib/cgi-bin/php7-fcgi -socket /var/run/php/php-fpm.sock -pass-header Authorization +See the `official Caddy documentation`_ for more examples, such as using +Caddy in a container infrastructure. -.. _`Apache documentation`: https://httpd.apache.org/docs/ -.. _`FastCgiExternalServer`: https://docs.oracle.com/cd/B31017_01/web.1013/q20204/mod_fastcgi.html#FastCgiExternalServer .. _`Nginx documentation`: https://www.nginx.com/resources/wiki/start/topics/recipes/symfony/ .. _`How to run Symfony applications using NGINX Unit`: https://unit.nginx.org/howto/symfony/ +.. _`official Caddy documentation`: https://caddyserver.com/docs/ 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 c7885328242..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): @@ -357,8 +358,8 @@ the ``test`` environment as follows: use App\Contracts\Repository\NewsRepositoryInterface; use App\Repository\NewsRepository; - return static function (ContainerConfigurator $containerConfigurator) { - $containerConfigurator->services() + return static function (ContainerConfigurator $container) { + $container->services() // redefine the alias as it should be while making it public ->alias(NewsRepositoryInterface::class, NewsRepository::class) ->public() @@ -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 @@ -566,11 +572,10 @@ In the above example, the test validates that the HTTP response was successful and the request body contains a ``

`` tag with ``"Hello world"``. The ``request()`` method also returns a crawler, which you can use to -create more complex assertions in your tests:: +create more complex assertions in your tests (e.g. to count the number of page +elements that match a given CSS selector):: $crawler = $client->request('GET', '/post/hello-world'); - - // for instance, count the number of ``.comment`` elements on the page $this->assertCount(4, $crawler->filter('.comment')); You can learn more about the crawler in :doc:`/testing/dom_crawler`. @@ -596,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 @@ -615,14 +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. - 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. +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'); + + // 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,18 +1012,18 @@ 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 = '')`` - Asserts the given header is (not) available on the response. + Asserts the given header is (not) available on the response, e.g. ``assertResponseHasHeader('content-type');``. ``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. -``assertResponseHasCookie(string $name, string $path = '/', string $domain = null, string $message = '')``/``assertResponseNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = '')`` + 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 = '')`` 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 @@ -1116,13 +1161,13 @@ Learn more /components/css_selector .. _`PHPUnit`: https://phpunit.de/ -.. _`documentation`: https://phpunit.readthedocs.io/ -.. _`Writing Tests for PHPUnit`: https://phpunit.readthedocs.io/en/9.5/writing-tests-for-phpunit.html -.. _`PHPUnit documentation`: https://phpunit.readthedocs.io/en/9.5/configuration.html +.. _`documentation`: https://docs.phpunit.de/ +.. _`Writing Tests for PHPUnit`: https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html +.. _`PHPUnit documentation`: https://docs.phpunit.de/en/9.6/configuration.html .. _`unit test`: https://en.wikipedia.org/wiki/Unit_testing .. _`DAMADoctrineTestBundle`: https://github.com/dmaicher/doctrine-test-bundle .. _`Doctrine data fixtures`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html .. _`DoctrineFixturesBundle documentation`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html .. _`SymfonyMakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html -.. _`PHPUnit Assertion`: https://phpunit.readthedocs.io/en/9.5/assertions.html +.. _`PHPUnit Assertion`: https://docs.phpunit.de/en/9.6/assertions.html .. _`section 4.1.18 of RFC 3875`: https://tools.ietf.org/html/rfc3875#section-4.1.18 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 e36fb9ff321..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 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 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. +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. - 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. +Custom Translation Resources +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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 @@ -828,7 +854,7 @@ A better policy is to include the locale in the URL using the { } } - + .. code-block:: php-attributes // src/Controller/ContactController.php @@ -943,6 +969,9 @@ the framework: $framework->defaultLocale('en'); }; +This ``default_locale`` is also relevant for the translator, as shown in the +next section. + .. _translation-fallback: Fallback Translation Locales @@ -963,7 +992,8 @@ checks translation resources for several locales: (Spanish) translation resource (e.g. ``messages.es.yaml``); #. If the translation still isn't found, Symfony uses the ``fallbacks`` option, - which can be configured as follows: + which can be configured as follows. When this option is not defined, it + defaults to the ``default_locale`` setting mentioned in the previous section. .. configuration-block:: @@ -1033,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 @@ -1045,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:: @@ -1285,6 +1316,141 @@ adapted to the format required by GitHub, but you can force that format too: The ``yaml-lint`` binary was introduced in Symfony 5.1. +Pseudo-localization translator +------------------------------ + +.. versionadded:: 5.2 + + The pseudolocalization translator was introduced in Symfony 5.2. + +.. note:: + + The pseudolocalization translator is meant to be used for development only. + +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 +only check if the application works when using basic accented letters, instead +of checking for more complex characters such as the ones found in Polish, +Czech, etc. + +These problems can be solved with `pseudolocalization`_, a software testing method +used for testing internationalization. In this method, instead of translating +the text of the software into a foreign language, the textual elements of an +application are replaced with an altered version of the original language. + +For example, ``Account Settings`` is *translated* as ``[!!! Àççôûñţ +Šéţţîñĝš !!!]``. First, the original text is expanded in length with characters +like ``[!!! !!!]`` to test the application when using languages more verbose +than the original one. This solves the first problem. + +In addition, the original characters are replaced by similar but accented +characters. This makes the text highly readable, while allowing to test the +application with all kinds of accented and special characters. This solves the +second problem. + +Full support for pseudolocalization was added to help you debug +internationalization issues in your applications. You can enable and configure +it in the translator configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/translation.yaml + framework: + translator: + pseudo_localization: + # replace characters by their accented version + accents: true + # wrap strings with brackets + brackets: true + # controls how many extra characters are added to make text longer + expansion_factor: 1.4 + # maintain the original HTML tags of the translated contents + parse_html: true + # also translate the contents of these HTML attributes + localizable_html_attributes: ['title'] + + .. code-block:: xml + + + + + + + + + + + + + + title + + + + + + .. code-block:: php + + // config/packages/translation.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + // ... + $framework + ->translator() + ->pseudoLocalization() + // replace characters by their accented version + ->accents(true) + // wrap strings with brackets + ->brackets(true) + // controls how many extra characters are added to make text longer + ->expansionFactor(1.4) + // maintain the original HTML tags of the translated contents + ->parseHtml(true) + // also translate the contents of these HTML attributes + ->localizableHtmlAttributes(['title']) + ; + }; + +That's all. The application will now start displaying those strange, but +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 ------- @@ -1318,3 +1484,8 @@ Learn more .. _`Translatable Behavior`: https://github.com/KnpLabs/DoctrineBehaviors .. _`Custom Language Name setting`: https://docs.lokalise.com/en/articles/1400492-uploading-files#custom-language-codes .. _`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 98b0d7c5798..f3f04b3feea 100644 --- a/workflow.rst +++ b/workflow.rst @@ -33,13 +33,14 @@ 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 instance of a :class:`Symfony\\Component\\Workflow\\MarkingStore\\MarkingStoreInterface`.) Consider the following example for a blog post. A post can have these places: -``draft``, ``reviewed``, ``rejected``, ``published``. You could define the workflow as +``draft``, ``reviewed``, ``rejected``, ``published``. You could define the workflow as follows: .. configuration-block:: @@ -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 --------------------------------- @@ -275,28 +316,6 @@ machine type, use ``camelCased workflow name + StateMachine``:: } } -Alternatively, use the registry:: - - use App\Entity\BlogPost; - use Symfony\Component\Workflow\Registry; - - class MyClass - { - private $workflowRegistry; - - public function __construct(Registry $workflowRegistry) - { - $this->workflowRegistry = $workflowRegistry; - } - - public function toReview(BlogPost $post) - { - $blogPublishingWorkflow = $this->workflowRegistry->get($post); - - // ... - } - } - .. tip:: You can find the list of available workflow services with the @@ -829,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 ------------- @@ -1051,7 +1153,7 @@ In a :ref:`flash message ` in your controller:: // $transition = ...; (an instance of Transition) - // $workflow is a Workflow instance retrieved from the Registry or injected directly (see above) + // $workflow is an injected Workflow instance $title = $workflow->getMetadataStore()->getMetadata('title', $transition); $this->addFlash('info', "You have successfully applied the transition with title: '$title'"); 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``