diff --git a/.doctor-rst.yaml b/.doctor-rst.yaml index 0768119d255..dfb85021586 100644 --- a/.doctor-rst.yaml +++ b/.doctor-rst.yaml @@ -8,6 +8,7 @@ rules: correct_code_block_directive_based_on_the_content: ~ deprecated_directive_should_have_version: ~ ensure_bash_prompt_before_composer_command: ~ + ensure_class_constant: ~ ensure_correct_format_for_phpfunction: ~ ensure_exactly_one_space_before_directive_type: ~ ensure_exactly_one_space_between_link_definition_and_link: ~ @@ -41,6 +42,7 @@ rules: no_composer_req: ~ no_directive_after_shorthand: ~ no_duplicate_use_statements: ~ + no_empty_literals: ~ no_explicit_use_of_code_block_php: ~ no_footnotes: ~ no_inheritdoc: ~ @@ -48,6 +50,7 @@ rules: no_namespace_after_use_statements: ~ no_php_open_tag_in_code_block_php_directive: ~ no_space_before_self_xml_closing_tag: ~ + no_typographic_quotes: ~ non_static_phpunit_assertions: ~ only_backslashes_in_namespace_in_php_code_block: ~ only_backslashes_in_use_statements_in_php_code_block: ~ @@ -71,7 +74,6 @@ rules: versionadded_directive_should_have_version: ~ yaml_instead_of_yml_suffix: ~ - # master versionadded_directive_major_version: major_version: 7 @@ -118,3 +120,4 @@ whitelist: - '.. _`a feature to test applications using Mercure`: https://github.com/symfony/panther#creating-isolated-browsers-to-test-apps-using-mercure-or-websocket' - 'End to End Tests (E2E)' - '.. versionadded:: 2.2.0' # Panther + - '* Inline code blocks use double-ticks (````like this````).' diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 061b0bb85b0..42770d55fe3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -26,9 +26,8 @@ jobs: - name: "Set-up PHP" uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.4 coverage: none - tools: "composer:v2" - name: Get composer cache directory id: composercache @@ -73,7 +72,7 @@ jobs: key: ${{ runner.os }}-doctor-rst-${{ steps.extract_base_branch.outputs.branch }} - name: "Run DOCtor-RST" - uses: docker://oskarstark/doctor-rst:1.64.0 + uses: docker://oskarstark/doctor-rst:1.70.0 with: args: --short --error-format=github --cache-file=/github/workspace/.cache/doctor-rst.cache @@ -93,7 +92,7 @@ jobs: - name: Set-up PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.4 coverage: none - name: Fetch branch from where the PR started diff --git a/_build/build.php b/_build/build.php index 5298abe779a..b684700a848 100755 --- a/_build/build.php +++ b/_build/build.php @@ -15,6 +15,13 @@ ->addOption('generate-fjson-files', null, InputOption::VALUE_NONE, 'Use this option to generate docs both in HTML and JSON formats') ->addOption('disable-cache', null, InputOption::VALUE_NONE, 'Use this option to force a full regeneration of all doc contents') ->setCode(function(InputInterface $input, OutputInterface $output) { + // the doc building app doesn't work on Windows + if ('\\' === DIRECTORY_SEPARATOR) { + $output->writeln('ERROR: The application that builds Symfony Docs does not support Windows. You can try using a Linux distribution via WSL (Windows Subsystem for Linux).'); + + return 1; + } + $io = new SymfonyStyle($input, $output); $io->text('Building all Symfony Docs...'); diff --git a/_build/composer.json b/_build/composer.json index e09d79de52f..f77976b10f4 100644 --- a/_build/composer.json +++ b/_build/composer.json @@ -3,7 +3,7 @@ "prefer-stable": true, "config": { "platform": { - "php": "8.1.0" + "php": "8.3" }, "preferred-install": { "*": "dist" @@ -14,9 +14,9 @@ } }, "require": { - "php": ">=8.1", + "php": ">=8.3", "symfony/console": "^6.2", "symfony/process": "^6.2", - "symfony-tools/docs-builder": "^0.21" + "symfony-tools/docs-builder": "^0.27" } } diff --git a/_build/composer.lock b/_build/composer.lock index 89a4e7da3c6..b9a4646f8ae 100644 --- a/_build/composer.lock +++ b/_build/composer.lock @@ -4,77 +4,33 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8a771cef10c68c570bff7875e4bdece3", + "content-hash": "e38eca557458275428db96db370d2c74", "packages": [ - { - "name": "doctrine/deprecations", - "version": "v1.0.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/deprecations.git", - "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", - "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", - "shasum": "" - }, - "require": { - "php": "^7.1|^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9", - "phpunit/phpunit": "^7.5|^8.5|^9.5", - "psr/log": "^1|^2|^3" - }, - "suggest": { - "psr/log": "Allows logging deprecations via PSR-3 logger implementation" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", - "homepage": "https://www.doctrine-project.org/", - "support": { - "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/v1.0.0" - }, - "time": "2022-05-02T15:47:09+00:00" - }, { "name": "doctrine/event-manager", - "version": "1.2.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/doctrine/event-manager.git", - "reference": "95aa4cb529f1e96576f3fda9f5705ada4056a520" + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/95aa4cb529f1e96576f3fda9f5705ada4056a520", - "reference": "95aa4cb529f1e96576f3fda9f5705ada4056a520", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", "shasum": "" }, "require": { - "doctrine/deprecations": "^0.5.3 || ^1", - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "conflict": { "doctrine/common": "<2.9" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^10", - "phpstan/phpstan": "~1.4.10 || ^1.8.8", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.24" + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" }, "type": "library", "autoload": { @@ -123,7 +79,7 @@ ], "support": { "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/1.2.0" + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" }, "funding": [ { @@ -139,42 +95,42 @@ "type": "tidelift" } ], - "time": "2022-10-12T20:51:15+00:00" + "time": "2024-05-22T20:47:39+00:00" }, { "name": "doctrine/rst-parser", - "version": "0.5.3", + "version": "0.5.6", "source": { "type": "git", "url": "https://github.com/doctrine/rst-parser.git", - "reference": "0b1d413d6bb27699ccec1151da6f617554d02c13" + "reference": "ca7f5f31f9ea58fde5aeffe0f7b8eb569e71a104" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/rst-parser/zipball/0b1d413d6bb27699ccec1151da6f617554d02c13", - "reference": "0b1d413d6bb27699ccec1151da6f617554d02c13", + "url": "https://api.github.com/repos/doctrine/rst-parser/zipball/ca7f5f31f9ea58fde5aeffe0f7b8eb569e71a104", + "reference": "ca7f5f31f9ea58fde5aeffe0f7b8eb569e71a104", "shasum": "" }, "require": { - "doctrine/event-manager": "^1.0", + "doctrine/event-manager": "^1.0 || ^2.0", "php": "^7.2 || ^8.0", - "symfony/filesystem": "^4.1 || ^5.0 || ^6.0", - "symfony/finder": "^4.1 || ^5.0 || ^6.0", + "symfony/filesystem": "^4.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/finder": "^4.1 || ^5.0 || ^6.0 || ^7.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/string": "^5.3 || ^6.0", - "symfony/translation-contracts": "^1.1 || ^2.0", + "symfony/string": "^5.3 || ^6.0 || ^7.0", + "symfony/translation-contracts": "^1.1 || ^2.0 || ^3.0", "twig/twig": "^2.9 || ^3.3" }, "require-dev": { - "doctrine/coding-standard": "^10.0", + "doctrine/coding-standard": "^11.0", "gajus/dindent": "^2.0.2", "phpstan/phpstan": "^1.9", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.2", "phpstan/phpstan-strict-rules": "^1.4", "phpunit/phpunit": "^7.5 || ^8.0 || ^9.0", - "symfony/css-selector": "4.4 || ^5.2 || ^6.0", - "symfony/dom-crawler": "4.4 || ^5.2 || ^6.0" + "symfony/css-selector": "4.4 || ^5.2 || ^6.0 || ^7.0", + "symfony/dom-crawler": "4.4 || ^5.2 || ^6.0 || ^7.0" }, "type": "library", "autoload": { @@ -210,32 +166,30 @@ ], "support": { "issues": "https://github.com/doctrine/rst-parser/issues", - "source": "https://github.com/doctrine/rst-parser/tree/0.5.3" + "source": "https://github.com/doctrine/rst-parser/tree/0.5.6" }, - "time": "2022-12-29T16:24:52+00:00" + "time": "2024-01-14T11:02:23+00:00" }, { "name": "masterminds/html5", - "version": "2.7.6", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "897eb517a343a2281f11bc5556d6548db7d93947" + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/897eb517a343a2281f11bc5556d6548db7d93947", - "reference": "897eb517a343a2281f11bc5556d6548db7d93947", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-dom": "*", - "ext-libxml": "*", "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7" + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" }, "type": "library", "extra": { @@ -279,9 +233,9 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.7.6" + "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" }, - "time": "2022-08-18T16:18:26+00:00" + "time": "2024-03-31T07:05:07+00:00" }, { "name": "psr/container", @@ -338,16 +292,16 @@ }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { @@ -382,9 +336,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "name": "scrivo/highlight.php", @@ -466,37 +420,37 @@ }, { "name": "symfony-tools/docs-builder", - "version": "v0.21.0", + "version": "0.27.0", "source": { "type": "git", "url": "https://github.com/symfony-tools/docs-builder.git", - "reference": "7ab92db15e9be7d6af51b86db87c7e41a14ba18b" + "reference": "720b52b2805122a4c08376496bd9661944c2624a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony-tools/docs-builder/zipball/7ab92db15e9be7d6af51b86db87c7e41a14ba18b", - "reference": "7ab92db15e9be7d6af51b86db87c7e41a14ba18b", + "url": "https://api.github.com/repos/symfony-tools/docs-builder/zipball/720b52b2805122a4c08376496bd9661944c2624a", + "reference": "720b52b2805122a4c08376496bd9661944c2624a", "shasum": "" }, "require": { "doctrine/rst-parser": "^0.5", "ext-curl": "*", "ext-json": "*", - "php": ">=7.4", - "scrivo/highlight.php": "^9.12.0", - "symfony/console": "^5.2 || ^6.0", - "symfony/css-selector": "^5.2 || ^6.0", - "symfony/dom-crawler": "^5.2 || ^6.0", - "symfony/filesystem": "^5.2 || ^6.0", - "symfony/finder": "^5.2 || ^6.0", - "symfony/http-client": "^5.2 || ^6.0", + "php": ">=8.3", + "scrivo/highlight.php": "^9.18.1", + "symfony/console": "^5.2 || ^6.0 || ^7.0", + "symfony/css-selector": "^5.2 || ^6.0 || ^7.0", + "symfony/dom-crawler": "^5.2 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.2 || ^6.0 || ^7.0", + "symfony/finder": "^5.2 || ^6.0 || ^7.0", + "symfony/http-client": "^5.2 || ^6.0 || ^7.0", "twig/twig": "^2.14 || ^3.3" }, "require-dev": { "gajus/dindent": "^2.0", "masterminds/html5": "^2.7", - "symfony/phpunit-bridge": "^5.2 || ^6.0", - "symfony/process": "^5.2 || ^6.0" + "symfony/phpunit-bridge": "^5.2 || ^6.0 || ^7.0", + "symfony/process": "^5.2 || ^6.0 || ^7.0" }, "bin": [ "bin/docs-builder" @@ -514,30 +468,30 @@ "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.21.0" + "source": "https://github.com/symfony-tools/docs-builder/tree/0.27.0" }, - "time": "2023-07-11T15:21:07+00:00" + "time": "2025-03-21T09:48:45+00:00" }, { "name": "symfony/console", - "version": "v6.2.8", + "version": "v6.4.17", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "3582d68a64a86ec25240aaa521ec8bc2342b369b" + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/3582d68a64a86ec25240aaa521ec8bc2342b369b", - "reference": "3582d68a64a86ec25240aaa521ec8bc2342b369b", + "url": "https://api.github.com/repos/symfony/console/zipball/799445db3f15768ecc382ac5699e6da0520a0a04", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/deprecation-contracts": "^2.1|^3", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/string": "^5.4|^6.0" + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" }, "conflict": { "symfony/dependency-injection": "<5.4", @@ -551,18 +505,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/lock": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0", - "symfony/var-dumper": "^5.4|^6.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -596,7 +548,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.2.8" + "source": "https://github.com/symfony/console/tree/v6.4.17" }, "funding": [ { @@ -612,24 +564,24 @@ "type": "tidelift" } ], - "time": "2023-03-29T21:42:15+00:00" + "time": "2024-12-07T12:07:30+00:00" }, { "name": "symfony/css-selector", - "version": "v6.2.7", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "aedf3cb0f5b929ec255d96bbb4909e9932c769e0" + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/aedf3cb0f5b929ec255d96bbb4909e9932c769e0", - "reference": "aedf3cb0f5b929ec255d96bbb4909e9932c769e0", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -661,7 +613,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v6.2.7" + "source": "https://github.com/symfony/css-selector/tree/v7.2.0" }, "funding": [ { @@ -677,20 +629,20 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:44:56+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.2.1", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e" + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", - "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", "shasum": "" }, "require": { @@ -698,12 +650,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.3-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { @@ -728,7 +680,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" }, "funding": [ { @@ -744,33 +696,30 @@ "type": "tidelift" } ], - "time": "2023-03-01T10:25:55+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/dom-crawler", - "version": "v6.2.8", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "0e0d0f709997ad1224ef22bb0a28287c44b7840f" + "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/0e0d0f709997ad1224ef22bb0a28287c44b7840f", - "reference": "0e0d0f709997ad1224ef22bb0a28287c44b7840f", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7", + "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7", "shasum": "" }, "require": { "masterminds/html5": "^2.6", - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { - "symfony/css-selector": "^5.4|^6.0" - }, - "suggest": { - "symfony/css-selector": "" + "symfony/css-selector": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -798,7 +747,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v6.2.8" + "source": "https://github.com/symfony/dom-crawler/tree/v7.2.4" }, "funding": [ { @@ -814,27 +763,30 @@ "type": "tidelift" } ], - "time": "2023-03-09T16:20:02+00:00" + "time": "2025-02-17T15:53:07+00:00" }, { "name": "symfony/filesystem", - "version": "v6.2.7", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "82b6c62b959f642d000456f08c6d219d749215b3" + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/82b6c62b959f642d000456f08c6d219d749215b3", - "reference": "82b6c62b959f642d000456f08c6d219d749215b3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, "type": "library", "autoload": { "psr-4": { @@ -861,7 +813,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.2.7" + "source": "https://github.com/symfony/filesystem/tree/v7.2.0" }, "funding": [ { @@ -877,27 +829,27 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:44:56+00:00" + "time": "2024-10-25T15:15:23+00:00" }, { "name": "symfony/finder", - "version": "v6.2.7", + "version": "v7.2.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "20808dc6631aecafbe67c186af5dcb370be3a0eb" + "reference": "87a71856f2f56e4100373e92529eed3171695cfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/20808dc6631aecafbe67c186af5dcb370be3a0eb", - "reference": "20808dc6631aecafbe67c186af5dcb370be3a0eb", + "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.0" + "symfony/filesystem": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -925,7 +877,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.2.7" + "source": "https://github.com/symfony/finder/tree/v7.2.2" }, "funding": [ { @@ -941,28 +893,33 @@ "type": "tidelift" } ], - "time": "2023-02-16T09:57:23+00:00" + "time": "2024-12-30T19:00:17+00:00" }, { "name": "symfony/http-client", - "version": "v6.2.8", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "66391ba3a8862c560e1d9134c96d9bd2a619b477" + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/66391ba3a8862c560e1d9134c96d9bd2a619b477", - "reference": "66391ba3a8862c560e1d9134c96d9bd2a619b477", + "url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/http-client-contracts": "^3", - "symfony/service-contracts": "^1.0|^2|^3" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" }, "provide": { "php-http/async-client-implementation": "*", @@ -971,18 +928,20 @@ "symfony/http-client-implementation": "3.0" }, "require-dev": { - "amphp/amp": "^2.5", - "amphp/http-client": "^4.2.1", - "amphp/http-tunnel": "^1.0", + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", "amphp/socket": "^1.1", - "guzzlehttp/promises": "^1.4", + "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0", - "symfony/stopwatch": "^5.4|^6.0" + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -1013,7 +972,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.2.8" + "source": "https://github.com/symfony/http-client/tree/v7.2.4" }, "funding": [ { @@ -1029,36 +988,33 @@ "type": "tidelift" } ], - "time": "2023-03-31T09:14:44+00:00" + "time": "2025-02-13T10:27:23+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.2.1", + "version": "v3.5.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "df2ecd6cb70e73c1080e6478aea85f5f4da2c48b" + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/df2ecd6cb70e73c1080e6478aea85f5f4da2c48b", - "reference": "df2ecd6cb70e73c1080e6478aea85f5f4da2c48b", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", "shasum": "" }, "require": { "php": ">=8.1" }, - "suggest": { - "symfony/http-client-implementation": "" - }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.3-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { @@ -1094,7 +1050,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.2.1" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" }, "funding": [ { @@ -1110,24 +1066,24 @@ "type": "tidelift" } ], - "time": "2023-03-01T10:32:47+00:00" + "time": "2024-12-07T08:49:48+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.27.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -1137,12 +1093,9 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1176,7 +1129,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -1192,36 +1145,33 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.27.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1257,7 +1207,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -1273,36 +1223,33 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.27.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1341,7 +1288,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -1357,24 +1304,24 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -1384,12 +1331,9 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1424,7 +1368,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -1440,20 +1384,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/process", - "version": "v6.2.8", + "version": "v6.4.19", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "75ed64103df4f6615e15a7fe38b8111099f47416" + "reference": "7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/75ed64103df4f6615e15a7fe38b8111099f47416", - "reference": "75ed64103df4f6615e15a7fe38b8111099f47416", + "url": "https://api.github.com/repos/symfony/process/zipball/7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3", + "reference": "7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3", "shasum": "" }, "require": { @@ -1485,7 +1429,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.2.8" + "source": "https://github.com/symfony/process/tree/v6.4.19" }, "funding": [ { @@ -1501,40 +1445,38 @@ "type": "tidelift" } ], - "time": "2023-03-09T16:20:02+00:00" + "time": "2025-02-04T13:35:48+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.2.1", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "a8c9cedf55f314f3a186041d19537303766df09a" + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a8c9cedf55f314f3a186041d19537303766df09a", - "reference": "a8c9cedf55f314f3a186041d19537303766df09a", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", "shasum": "" }, "require": { "php": ">=8.1", - "psr/container": "^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" }, - "suggest": { - "symfony/service-implementation": "" - }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.3-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { @@ -1570,7 +1512,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.2.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" }, "funding": [ { @@ -1586,38 +1528,39 @@ "type": "tidelift" } ], - "time": "2023-03-01T10:32:47+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/string", - "version": "v6.2.8", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "193e83bbd6617d6b2151c37fff10fa7168ebddef" + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/193e83bbd6617d6b2151c37fff10fa7168ebddef", - "reference": "193e83bbd6617d6b2151c37fff10fa7168ebddef", + "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/translation-contracts": "<2.0" + "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0", - "symfony/http-client": "^5.4|^6.0", - "symfony/intl": "^6.2", - "symfony/translation-contracts": "^2.0|^3.0", - "symfony/var-exporter": "^5.4|^6.0" + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -1656,7 +1599,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.2.8" + "source": "https://github.com/symfony/string/tree/v7.2.0" }, "funding": [ { @@ -1672,42 +1615,42 @@ "type": "tidelift" } ], - "time": "2023-03-20T16:06:02+00:00" + "time": "2024-11-13T13:31:26+00:00" }, { "name": "symfony/translation-contracts", - "version": "v2.5.2", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "136b19dd05cdf0709db6537d058bcab6dd6e2dbe" + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/136b19dd05cdf0709db6537d058bcab6dd6e2dbe", - "reference": "136b19dd05cdf0709db6537d058bcab6dd6e2dbe", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c", + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c", "shasum": "" }, "require": { - "php": ">=7.2.5" - }, - "suggest": { - "symfony/translation-implementation": "" + "php": ">=8.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { "psr-4": { "Symfony\\Contracts\\Translation\\": "" - } + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1734,7 +1677,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1" }, "funding": [ { @@ -1750,38 +1693,41 @@ "type": "tidelift" } ], - "time": "2022-06-27T16:58:25+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "twig/twig", - "version": "v3.5.1", + "version": "v3.20.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "a6e0510cc793912b451fd40ab983a1d28f611c15" + "reference": "3468920399451a384bef53cf7996965f7cd40183" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/a6e0510cc793912b451fd40ab983a1d28f611c15", - "reference": "a6e0510cc793912b451fd40ab983a1d28f611c15", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/3468920399451a384bef53cf7996965f7cd40183", + "reference": "3468920399451a384bef53cf7996965f7cd40183", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { - "psr/container": "^1.0", - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "phpstan/phpstan": "^2.0", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } - }, "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], "psr-4": { "Twig\\": "src/" } @@ -1814,7 +1760,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.5.1" + "source": "https://github.com/twigphp/Twig/tree/v3.20.0" }, "funding": [ { @@ -1826,21 +1772,21 @@ "type": "tidelift" } ], - "time": "2023-02-08T07:49:20+00:00" + "time": "2025-02-13T08:34:43+00:00" } ], "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=8.1" + "php": ">=8.3" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { - "php": "8.1.0" + "php": "8.3" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/_build/redirection_map b/_build/redirection_map index 1701f4a8f70..c30723eac58 100644 --- a/_build/redirection_map +++ b/_build/redirection_map @@ -430,10 +430,12 @@ /email/spool /mailer /email/testing /mailer /contributing/community/other /contributing/community +/contributing/code/core_team /contributing/core_team /profiler/storage /profiler /setup/composer /setup /security/security_checker /setup /setup/built_in_web_server /setup/symfony_server +/setup/symfony_server /setup/symfony_cli /service_container/parameters /configuration /routing/generate_url_javascript /routing /routing/slash_in_parameter /routing @@ -573,3 +575,7 @@ /doctrine/reverse_engineering /doctrine#doctrine-adding-mapping /components/serializer /serializer /serializer/custom_encoder /serializer/encoders#serializer-custom-encoder +/components/string /string +/form/button_based_validation /form/validation_groups +/form/data_based_validation /form/validation_groups +/form/validation_group_service_resolver /form/validation_groups diff --git a/_images/sources/README.md b/_images/sources/README.md index 467d4024010..84810a9783d 100644 --- a/_images/sources/README.md +++ b/_images/sources/README.md @@ -96,7 +96,7 @@ only the asciicast file). [1]: http://dia-installer.de/ [2]: https://fonts.google.com/specimen/PT+Sans+Narrow -[3]: https://symfony.com/doc/current/contributing/code/core_team.html +[3]: https://symfony.com/doc/current/contributing/core_team.html [4]: https://github.com/asciinema/asciinema [5]: https://github.com/asciinema/agg [6]: https://www.jetbrains.com/lp/mono/ diff --git a/best_practices.rst b/best_practices.rst index 2c393cae9c6..6211d042f0b 100644 --- a/best_practices.rst +++ b/best_practices.rst @@ -95,7 +95,7 @@ Use Secrets for Sensitive Information ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When your application has sensitive configuration, like an API key, you should -store those securely via :doc:`Symfony’s secrets management system `. +store those securely via :doc:`Symfony's secrets management system `. Use Parameters for Application Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -362,10 +362,6 @@ Unless you have two legitimately different authentication systems and users (e.g. form login for the main site and a token system for your API only), it's recommended to have only one firewall to keep things simple. -Additionally, you should use the ``anonymous`` key under your firewall. If you -require users to be logged in for different sections of your site, use the -:doc:`access_control ` option. - Use the ``auto`` Password Hasher ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/bundles/best_practices.rst b/bundles/best_practices.rst index 376984388db..8049ebb9a1c 100644 --- a/bundles/best_practices.rst +++ b/bundles/best_practices.rst @@ -195,25 +195,24 @@ Continuous Integration Testing bundle code continuously, including all its commits and pull requests, is a good practice called Continuous Integration. There are several services -providing this feature for free for open source projects, like `GitHub Actions`_ -and `Travis CI`_. +providing this feature for free for open source projects, like `GitHub Actions`_. A bundle should at least test: * The lower bound of their dependencies (by running ``composer update --prefer-lowest``); * The supported PHP versions; -* All supported major Symfony versions (e.g. both ``4.x`` and ``5.x`` if +* All supported major Symfony versions (e.g. both ``6.4`` and ``7.x`` if support is claimed for both). -Thus, a bundle supporting PHP 7.3, 7.4 and 8.0, and Symfony 4.4 and 5.x should +Thus, a bundle supporting PHP 7.4, 8.3 and 8.4, and Symfony 6.4 and 7.x should have at least this test matrix: =========== =============== =================== PHP version Symfony version Composer flags =========== =============== =================== -7.3 ``4.*`` ``--prefer-lowest`` -7.4 ``5.*`` -8.0 ``5.*`` +7.4 ``6.4`` ``--prefer-lowest`` +8.3 ``7.*`` +8.4 ``7.*`` =========== =============== =================== .. tip:: @@ -233,10 +232,10 @@ with Symfony Flex to install a specific Symfony version: .. code-block:: bash - # this requires Symfony 5.x for all Symfony packages - export SYMFONY_REQUIRE=5.* + # this requires Symfony 7.x for all Symfony packages + export SYMFONY_REQUIRE=7.* # alternatively you can run this command to update composer.json config - # composer config extra.symfony.require "5.*" + # composer config extra.symfony.require "7.*" # install Symfony Flex in the CI environment composer global config --no-plugins allow-plugins.symfony/flex true @@ -397,10 +396,14 @@ Translation Files ----------------- If a bundle provides message translations, they must be defined in the XLIFF -format; the domain should be named after the bundle name (``acme_blog``). +format; the domain should be named after the bundle name (``AcmeBlog``). A bundle must not override existing messages from another bundle. +The translation domain must match the translation file names. For example, +if the translation domain is ``AcmeBlog``, the English translation file name +should be ``AcmeBlog.en.xlf``. + Configuration ------------- @@ -556,6 +559,7 @@ Learn more * :doc:`/bundles/extension` * :doc:`/bundles/configuration` +* :doc:`/frontend/create_ux_bundle` .. _`PSR-4`: https://www.php-fig.org/psr/psr-4/ .. _`Symfony Flex recipe`: https://github.com/symfony/recipes @@ -564,4 +568,3 @@ Learn more .. _`choose any license`: https://choosealicense.com/ .. _`valid license identifier`: https://spdx.org/licenses/ .. _`GitHub Actions`: https://docs.github.com/en/free-pro-team@latest/actions -.. _`Travis CI`: https://docs.travis-ci.com/ diff --git a/bundles/extension.rst b/bundles/extension.rst index 0537eb00c3e..d2792efc477 100644 --- a/bundles/extension.rst +++ b/bundles/extension.rst @@ -25,6 +25,7 @@ class, you can define the :method:`Symfony\\Component\\HttpKernel\\Bundle\\Abstr method to load service definitions from configuration files:: // ... + use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; diff --git a/cache.rst b/cache.rst index 833e4d77007..5687d3544b6 100644 --- a/cache.rst +++ b/cache.rst @@ -539,6 +539,8 @@ Symfony stores the item automatically in all the missing pools. ; }; +.. _cache-using-cache-tags: + Using Cache Tags ---------------- @@ -590,6 +592,7 @@ to enable this feature. This could be added by using the following configuration pools: my_cache_pool: adapter: cache.adapter.redis_tag_aware + tags: true .. code-block:: xml @@ -606,7 +609,7 @@ to enable this feature. This could be added by using the following configuration @@ -622,7 +625,7 @@ to enable this feature. This could be added by using the following configuration $framework->cache() ->pool('my_cache_pool') ->tags(true) - ->adapters(['cache.adapter.redis']) + ->adapters(['cache.adapter.redis_tag_aware']) ; }; @@ -949,9 +952,9 @@ a message bus to compute values in a worker: .. 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; + use function Symfony\Component\DependencyInjection\Loader\Configurator\env; return static function (FrameworkConfig $framework): void { $framework->cache() diff --git a/components/browser_kit.rst b/components/browser_kit.rst index bcb8f7b3c8e..8cf0772298c 100644 --- a/components/browser_kit.rst +++ b/components/browser_kit.rst @@ -143,7 +143,7 @@ field values, etc.) before submitting it:: $crawler = $client->request('GET', 'https://github.com/login'); // find the form with the 'Log in' button and submit it - // 'Log in' can be the text content, id, value or name of a @@ -229,7 +237,7 @@ generate a CSRF token in the template and store it as a hidden form field: Then, get the value of the CSRF token in the controller action and use the :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::isCsrfTokenValid` -method to check its validity:: +method to check its validity, passing the same token ID used in the template:: use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -273,6 +281,20 @@ Suppose you want a CSRF token per item, so in the template you have something li +This attribute can also be applied to a controller class. When used this way, +the CSRF token validation will be applied to **all actions** defined in that +controller:: + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid; + // ... + + #[IsCsrfTokenValid('the token ID')] + final class SomeController extends AbstractController + { + // ... + } + The :class:`Symfony\\Component\\Security\\Http\\Attribute\\IsCsrfTokenValid` attribute also accepts an :class:`Symfony\\Component\\ExpressionLanguage\\Expression` object evaluated to the id:: @@ -288,11 +310,26 @@ object evaluated to the id:: // ... do something, like deleting an object } +By default, the ``IsCsrfTokenValid`` attribute performs the CSRF token check for +all HTTP methods. You can restrict this validation to specific methods using the +``methods`` parameter. If the request uses a method not listed in the ``methods`` +array, the attribute is ignored for that request, and no CSRF validation occurs:: + + #[IsCsrfTokenValid('delete-item', tokenKey: 'token', methods: ['DELETE'])] + public function delete(Post $post): Response + { + // ... delete the object + } + .. versionadded:: 7.1 The :class:`Symfony\\Component\\Security\\Http\\Attribute\\IsCsrfTokenValid` attribute was introduced in Symfony 7.1. +.. versionadded:: 7.3 + + The ``methods`` parameter was introduced in Symfony 7.3. + CSRF Tokens and Compression Side-Channel Attacks ------------------------------------------------ @@ -302,6 +339,171 @@ targeted parts of the plaintext. To mitigate these attacks, and prevent an attacker from guessing the CSRF tokens, a random mask is prepended to the token and used to scramble it. +.. _csrf-stateless-tokens: + +Stateless CSRF Tokens +--------------------- + +.. versionadded:: 7.2 + + Stateless anti-CSRF protection was introduced in Symfony 7.2. + +Traditionally, CSRF tokens are stateful, meaning they're stored in the session. +However, some token IDs can be declared as stateless using the +``stateless_token_ids`` option. Stateless CSRF tokens are enabled by default +in applications using :ref:`Symfony Flex `. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/csrf.yaml + framework: + # ... + csrf_protection: + stateless_token_ids: ['submit', 'authenticate', 'logout'] + + .. code-block:: xml + + + + + + + + submit + authenticate + logout + + + + + .. code-block:: php + + // config/packages/csrf.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->csrfProtection() + ->statelessTokenIds(['submit', 'authenticate', 'logout']) + ; + }; + +Stateless CSRF tokens provide protection without relying on the session. This +allows you to fully cache pages while still protecting against CSRF attacks. + +When validating a stateless CSRF token, Symfony checks the ``Origin`` and +``Referer`` headers of the incoming HTTP request. If either header matches the +application's target origin (i.e. its domain), the token is considered valid. + +This mechanism relies on the application being able to determine its own origin. +If you're behind a reverse proxy, make sure it's properly configured. See +:doc:`/deployment/proxies`. + +Using a Default Token ID +~~~~~~~~~~~~~~~~~~~~~~~~ + +Stateful CSRF tokens are typically scoped per form or action, while stateless +tokens don't require many identifiers. + +In the example above, the ``authenticate`` and ``logout`` identifiers are listed +because they are used by default in the Symfony Security component. The ``submit`` +identifier is included so that form types defined by the application can also use +CSRF protection by default. + +The following configuration applies only to form types registered via +:ref:`autoconfiguration ` (which is the default for your +own services), and it sets ``submit`` as their default token identifier: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/csrf.yaml + framework: + form: + csrf_protection: + token_id: 'submit' + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/csrf.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->form() + ->csrfProtection() + ->tokenId('submit') + ; + }; + +Forms configured with a token identifier listed in the above ``stateless_token_ids`` +option will use the stateless CSRF protection. + +Generating CSRF Token Using Javascript +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to the ``Origin`` and ``Referer`` HTTP headers, stateless CSRF protection +can also validate tokens using a cookie and a header (named ``csrf-token`` by +default; see the :ref:`CSRF configuration reference `). + +These additional checks are part of the **defense-in-depth** strategy provided by +stateless CSRF protection. They are optional and require `some JavaScript`_ to +be enabled. This JavaScript generates a cryptographically secure random token +when a form is submitted. It then inserts the token into the form's hidden CSRF +field and sends it in both a cookie and a request header. + +On the server side, CSRF token validation compares the values in the cookie and +the header. This "double-submit" protection relies on the browser's same-origin +policy and is further hardened by: + +* generating a new token for each submission (to prevent cookie fixation); +* using ``samesite=strict`` and ``__Host-`` cookie attributes (to enforce HTTPS + and limit the cookie to the current domain). + +By default, the Symfony JavaScript snippet expects the hidden CSRF field to be +named ``_csrf_token`` or to include the ``data-controller="csrf-protection"`` +attribute. You can adapt this logic to your needs as long as the same protocol +is followed. + +To prevent validation from being downgraded, an extra behavioral check is performed: +if (and only if) a session already exists, successful "double-submit" is remembered +and becomes required for future requests. This ensures that once the optional cookie/header +validation has been proven effective, it remains enforced for that session. + +.. note:: + + Enforcing "double-submit" validation on all requests is not recommended, + as it may lead to a broken user experience. The opportunistic approach + described above is preferred, allowing the application to gracefully + fall back to ``Origin`` / ``Referer`` checks when JavaScript is unavailable. + .. _`Cross-site request forgery`: https://en.wikipedia.org/wiki/Cross-site_request_forgery .. _`BREACH`: https://en.wikipedia.org/wiki/BREACH .. _`CRIME`: https://en.wikipedia.org/wiki/CRIME +.. _`some JavaScript`: https://github.com/symfony/recipes/blob/main/symfony/stimulus-bundle/2.20/assets/controllers/csrf_protection_controller.js diff --git a/security/custom_authenticator.rst b/security/custom_authenticator.rst index 8b2ec9d7f34..462ec21521c 100644 --- a/security/custom_authenticator.rst +++ b/security/custom_authenticator.rst @@ -1,18 +1,28 @@ How to Write a Custom Authenticator =================================== -Symfony comes with :ref:`many authenticators ` and -third party bundles also implement more complex cases like JWT and oAuth -2.0. However, sometimes you need to implement a custom authentication -mechanism that doesn't exist yet or you need to customize one. In such -cases, you must create and use your own authenticator. +Symfony comes with :ref:`many authenticators `, and +third-party bundles also implement more complex cases like JWT and OAuth 2.0. +However, sometimes you need to implement a custom authentication mechanism +that doesn't exist yet, or you need to customize an existing one. -Authenticators should implement the -:class:`Symfony\\Component\\Security\\Http\\Authenticator\\AuthenticatorInterface`. -You can also extend -:class:`Symfony\\Component\\Security\\Http\\Authenticator\\AbstractAuthenticator`, -which has a default implementation for the ``createToken()`` -method that fits most use-cases:: +To save time, you can install `Symfony Maker`_ and let Symfony generate a new +authenticator by running the following command: + +.. code-block:: terminal + + $ php bin/console make:security:custom + + What is the class name of the authenticator (e.g. CustomAuthenticator): + > ApiKeyAuthenticator + + updated: config/packages/security.yaml + created: src/Security/ApiKeyAuthenticator.php + + Success! + +Open the ``src/Security/ApiKeyAuthenticator.php`` file created by this command, +and you'll find something like the following:: // src/Security/ApiKeyAuthenticator.php namespace App\Security; @@ -77,13 +87,23 @@ method that fits most use-cases:: } } +Authenticators must implement the +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\AuthenticatorInterface`. +You can also extend +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\AbstractAuthenticator`, +which provides a default implementation of the ``createToken()`` method suitable +for most use cases. + .. tip:: - If your custom authenticator is a login form, you can extend from the + If your custom authenticator is a login form, consider extending :class:`Symfony\\Component\\Security\\Http\\Authenticator\\AbstractLoginFormAuthenticator` - class instead to make your job easier. + to simplify your implementation. -The authenticator can be enabled using the ``custom_authenticators`` setting: +Custom authenticators must be explicitly enabled in the security configuration +using the ``custom_authenticators`` setting of your firewall(s). If you used the +``make:security:custom`` command, this configuration is already updated, but you +should review it: .. configuration-block:: @@ -209,13 +229,20 @@ requires a user and some sort of "credentials" (e.g. a password). Use the :class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\UserBadge` to attach the user to the passport. The ``UserBadge`` requires a user -identifier (e.g. the username or email), which is used to load the user -using :ref:`the user provider `:: +identifier (e.g. the username or email):: use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; // ... - $passport = new Passport(new UserBadge($email), $credentials); + $passport = new Passport(new UserBadge($userIdentifier), $credentials); + +User Identifier +~~~~~~~~~~~~~~~ + +The user identifier is a unique string that identifies the user. It is often +something like their email address or username, but it can be any unique value +associated with the user. It allows loading the user through the configured +:ref:`user provider `. .. note:: @@ -255,6 +282,69 @@ using :ref:`the user provider `:: } } +Some applications normalize user identifiers before processing them. For example, +lowercasing identifiers helps treat values like "john.doe", "John.Doe", or +"JOHN.DOE" as equivalent in systems where identifiers are case-insensitive. + +If needed, you can pass a normalizer as the third argument to ``UserBadge``. +This callable receives the ``$userIdentifier`` and must return a string. + +.. versionadded:: 7.3 + + Support for user identifier normalizers was introduced in Symfony 7.3. + +The example below uses a normalizer that converts usernames to a normalized, +ASCII-only, lowercase format:: + + // src/Security/NormalizedUserBadge.php + namespace App\Security; + + use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; + use function Symfony\Component\String\u; + + final class NormalizedUserBadge extends UserBadge + { + public function __construct(string $identifier) + { + $callback = static fn (string $identifier): string => u($identifier)->normalize(UnicodeString::NFKC)->ascii()->lower()->toString(); + + parent::__construct($identifier, null, $callback); + } + } + +:: + + // src/Security/PasswordAuthenticator.php + namespace App\Security; + + final class PasswordAuthenticator extends AbstractLoginFormAuthenticator + { + // simplified for brevity + public function authenticate(Request $request): Passport + { + $username = (string) $request->request->get('username', ''); + $password = (string) $request->request->get('password', ''); + + $request->getSession() + ->set(SecurityRequestAttributes::LAST_USERNAME, $username); + + return new Passport( + new NormalizedUserBadge($username), + new PasswordCredentials($password), + [ + // all other useful badges + ] + ); + } + } + +User Credential +~~~~~~~~~~~~~~~ + +The user credential is used to authenticate the user; that is, to verify +the validity of the provided information (such as a password, an API token, +or custom credentials). + The following credential classes are supported by default: :class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Credentials\\PasswordCredentials` @@ -389,4 +479,5 @@ authenticator methods (e.g. ``createToken()``):: } } +.. _`Symfony Maker`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html .. _`session storage flooding`: https://symfony.com/blog/cve-2016-4423-large-username-storage-in-session diff --git a/security/expressions.rst b/security/expressions.rst index 569c7f093bf..a4ec02c7b84 100644 --- a/security/expressions.rst +++ b/security/expressions.rst @@ -201,6 +201,38 @@ Inside the subject's expression, you have access to two variables: ``args`` An array of controller arguments that are passed to the controller. +Additionally to expressions, the ``#[IsGranted]`` attribute also accepts +closures that return a boolean value. The subject can also be a closure that +returns an array of values that will be injected into the closure:: + + // src/Controller/MyController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Security\Http\Attribute\IsGranted; + use Symfony\Component\Security\Http\Attribute\IsGrantedContext; + + class MyController extends AbstractController + { + #[IsGranted(static function (IsGrantedContext $context, mixed $subject) { + return $context->user === $subject['post']->getAuthor(); + }, subject: static function (array $args) { + return [ + 'post' => $args['post'], + ]; + })] + public function index($post): Response + { + // ... + } + } + +.. versionadded:: 7.3 + + The support for closures in the ``#[IsGranted]`` attribute was introduced + in Symfony 7.3 and requires PHP 8.5. + Learn more ---------- diff --git a/security/ldap.rst b/security/ldap.rst index 081be764290..c4c3646122b 100644 --- a/security/ldap.rst +++ b/security/ldap.rst @@ -256,6 +256,24 @@ This is the default role you wish to give to a user fetched from the LDAP server. If you do not configure this key, your users won't have any roles, and will not be considered as authenticated fully. +role_fetcher +............ + +**Type**: ``string`` **Default**: ``null`` + +When your LDAP service provides user roles, this option allows you to define +the service that retrieves these roles. The role fetcher service must implement +the ``Symfony\Component\Ldap\Security\RoleFetcherInterface``. When this option +is set, the ``default_roles`` option is ignored. + +Symfony provides ``Symfony\Component\Ldap\Security\MemberOfRoles``, a concrete +implementation of the interface that fetches roles from the ``ismemberof`` +attribute. + +.. versionadded:: 7.3 + + The ``role_fetcher`` configuration option was introduced in Symfony 7.3. + uid_key ....... diff --git a/security/passwords.rst b/security/passwords.rst index fe20187b3a0..7f05bc3acb9 100644 --- a/security/passwords.rst +++ b/security/passwords.rst @@ -124,75 +124,38 @@ Further in this article, you can find a .. code-block:: yaml - # config/packages/test/security.yaml - security: - # ... - - password_hashers: - # Use your user class name here - App\Entity\User: - algorithm: plaintext # disable hashing (only do this in tests!) - - # or use the lowest possible values - App\Entity\User: - algorithm: auto # This should be the same value as in config/packages/security.yaml - cost: 4 # Lowest possible value for bcrypt - time_cost: 3 # Lowest possible value for argon - memory_cost: 10 # Lowest possible value for argon - - .. code-block:: xml - - - - - - - - - - - - - - - - - - + # config/packages/security.yaml + when@test: + security: + # ... + + password_hashers: + # Use your user class name here + App\Entity\User: + algorithm: auto + cost: 4 # Lowest possible value for bcrypt + time_cost: 3 # Lowest possible value for argon + memory_cost: 10 # Lowest possible value for argon .. code-block:: php - // config/packages/test/security.php + // config/packages/security.php use App\Entity\User; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security): void { + return static function (SecurityConfig $security, ContainerConfigurator $container): void { // ... - // Use your user class name here - $security->passwordHasher(User::class) - ->algorithm('plaintext'); // disable hashing (only do this in tests!) - - // or use the lowest possible values - $security->passwordHasher(User::class) - ->algorithm('auto') // This should be the same value as in config/packages/security.yaml - ->cost(4) // Lowest possible value for bcrypt - ->timeCost(2) // Lowest possible value for argon - ->memoryCost(10) // Lowest possible value for argon - ; + if ('test' === $container->env()) { + // Use your user class name here + $security->passwordHasher(User::class) + ->algorithm('auto') // This should be the same value as in config/packages/security.yaml + ->cost(4) // Lowest possible value for bcrypt + ->timeCost(2) // Lowest possible value for argon + ->memoryCost(10) // Lowest possible value for argon + ; + } }; Hashing the Password @@ -500,13 +463,14 @@ the user provider:: namespace App\Security; // ... + use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; class UserProvider implements UserProviderInterface, PasswordUpgraderInterface { // ... - public function upgradePassword(UserInterface $user, string $newHashedPassword): void + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void { // set the new hashed password on the User object $user->setPassword($newHashedPassword); diff --git a/security/remember_me.rst b/security/remember_me.rst index 8fac6d78849..2fd0f7e8d1e 100644 --- a/security/remember_me.rst +++ b/security/remember_me.rst @@ -19,7 +19,7 @@ the session lasts using a cookie with the ``remember_me`` firewall option: main: # ... remember_me: - secret: '%kernel.secret%' # required + secret: '%kernel.secret%' lifetime: 604800 # 1 week in seconds # by default, the feature is enabled by checking a # checkbox in the login form (see below), uncomment the @@ -44,7 +44,7 @@ the session lasts using a cookie with the ``remember_me`` firewall option: - firewall('main') // ... ->rememberMe() - ->secret('%kernel.secret%') // required + ->secret('%kernel.secret%') ->lifetime(604800) // 1 week in seconds // by default, the feature is enabled by checking a @@ -77,9 +77,11 @@ the session lasts using a cookie with the ``remember_me`` firewall option: ; }; -The ``secret`` option is the only required option and it is used to sign -the remember me cookie. It's common to use the ``kernel.secret`` parameter, -which is defined using the ``APP_SECRET`` environment variable. +.. versionadded:: 7.2 + + The ``secret`` option is no longer required starting from Symfony 7.2. By + default, ``%kernel.secret%`` is used, which is defined using the + ``APP_SECRET`` environment variable. After enabling the ``remember_me`` system in the configuration, there are a couple more things to do before remember me works correctly: @@ -171,7 +173,6 @@ allow users to opt-out. In these cases, you can use the main: # ... remember_me: - secret: '%kernel.secret%' # ... always_remember_me: true @@ -194,7 +195,6 @@ allow users to opt-out. In these cases, you can use the @@ -211,7 +211,6 @@ allow users to opt-out. In these cases, you can use the $security->firewall('main') // ... ->rememberMe() - ->secret('%kernel.secret%') // ... ->alwaysRememberMe(true) ; @@ -335,7 +334,6 @@ are fetched from the user object using the main: # ... remember_me: - secret: '%kernel.secret%' # ... signature_properties: ['password', 'updatedAt'] @@ -357,7 +355,7 @@ are fetched from the user object using the - + password updatedAt @@ -375,7 +373,6 @@ are fetched from the user object using the $security->firewall('main') // ... ->rememberMe() - ->secret('%kernel.secret%') // ... ->signatureProperties(['password', 'updatedAt']) ; @@ -419,7 +416,6 @@ You can enable the doctrine token provider using the ``doctrine`` setting: main: # ... remember_me: - secret: '%kernel.secret%' # ... token_provider: doctrine: true @@ -442,7 +438,7 @@ You can enable the doctrine token provider using the ``doctrine`` setting: - + @@ -459,7 +455,6 @@ You can enable the doctrine token provider using the ``doctrine`` setting: $security->firewall('main') // ... ->rememberMe() - ->secret('%kernel.secret%') // ... ->tokenProvider([ 'doctrine' => true, diff --git a/security/user_checkers.rst b/security/user_checkers.rst index d62cc0bea32..ec8f49da522 100644 --- a/security/user_checkers.rst +++ b/security/user_checkers.rst @@ -21,6 +21,8 @@ displayed to the user:: namespace App\Security; use App\Entity\User as AppUser; + use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\AccountExpiredException; use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException; use Symfony\Component\Security\Core\User\UserCheckerInterface; @@ -40,7 +42,7 @@ displayed to the user:: } } - public function checkPostAuth(UserInterface $user): void + public function checkPostAuth(UserInterface $user, TokenInterface $token): void { if (!$user instanceof AppUser) { return; @@ -50,9 +52,17 @@ displayed to the user:: if ($user->isExpired()) { throw new AccountExpiredException('...'); } + + if (!\in_array('foo', $token->getRoleNames())) { + throw new AccessDeniedException('...'); + } } } +.. versionadded:: 7.2 + + The ``token`` argument for the ``checkPostAuth()`` method was introduced in Symfony 7.2. + Enabling the Custom User Checker -------------------------------- diff --git a/security/user_providers.rst b/security/user_providers.rst index 09d47c270f2..73b723faaaf 100644 --- a/security/user_providers.rst +++ b/security/user_providers.rst @@ -347,23 +347,23 @@ providers until the user is found: return static function (SecurityConfig $security): void { // ... - $backendProvider = $security->provider('backend_users') + $security->provider('backend_users') ->ldap() // ... ; - $legacyProvider = $security->provider('legacy_users') + $security->provider('legacy_users') ->entity() // ... ; - $userProvider = $security->provider('users') + $security->provider('users') ->entity() // ... ; - $allProviders = $security->provider('all_users')->chain() - ->providers([$backendProvider, $legacyProvider, $userProvider]) + $security->provider('all_users')->chain() + ->providers(['backend_users', 'legacy_users', 'users']) ; }; @@ -425,7 +425,7 @@ command will generate a nice skeleton to get you started:: public function refreshUser(UserInterface $user): UserInterface { if (!$user instanceof User) { - throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user))); + throw new UnsupportedUserException(sprintf('Invalid user class "%s".', $user::class)); } // Return a User object after making sure its data is "fresh". diff --git a/security/voters.rst b/security/voters.rst index e7452fadf99..e621263abb4 100644 --- a/security/voters.rst +++ b/security/voters.rst @@ -40,23 +40,21 @@ or extend :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\Vote which makes creating a voter even easier:: use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; abstract class Voter implements VoterInterface { abstract protected function supports(string $attribute, mixed $subject): bool; - abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool; + abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool; } -.. _how-to-use-the-voter-in-a-controller: - -.. tip:: +.. versionadded:: 7.3 + + The ``$vote`` argument of the ``voteOnAttribute()`` method was introduced + in Symfony 7.3. - Checking each voter several times can be time consuming for applications - that perform a lot of permission checks. To improve performance in those cases, - you can make your voters implement the :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\CacheableVoterInterface`. - This allows the access decision manager to remember the attribute and type - of subject supported by the voter, to only call the needed voters each time. +.. _how-to-use-the-voter-in-a-controller: Setup: Checking for Access in a Controller ------------------------------------------ @@ -140,6 +138,7 @@ would look like this:: use App\Entity\Post; use App\Entity\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; class PostVoter extends Voter @@ -163,12 +162,13 @@ would look like this:: return true; } - protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool { $user = $token->getUser(); if (!$user instanceof User) { // the user must be logged in; if not, deny access + $vote?->addReason('The user is not logged in.'); return false; } @@ -178,7 +178,7 @@ would look like this:: return match($attribute) { self::VIEW => $this->canView($post, $user), - self::EDIT => $this->canEdit($post, $user), + self::EDIT => $this->canEdit($post, $user, $vote), default => throw new \LogicException('This code should not be reached!') }; } @@ -194,10 +194,19 @@ would look like this:: return !$post->isPrivate(); } - private function canEdit(Post $post, User $user): bool + private function canEdit(Post $post, User $user, ?Vote $vote): bool { - // this assumes that the Post object has a `getOwner()` method - return $user === $post->getOwner(); + // this assumes that the Post object has a `getAuthor()` method + if ($user === $post->getAuthor()) { + return true; + } + + $vote?->addReason(sprintf( + 'The logged in user (username: %s) is not the author of this post (id: %d).', + $user->getUsername(), $post->getId() + )); + + return false; } } @@ -215,11 +224,12 @@ To recap, here's what's expected from the two abstract methods: return ``true`` if the attribute is ``view`` or ``edit`` and if the object is a ``Post`` instance. -``voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token)`` +``voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null)`` If you return ``true`` from ``supports()``, then this method is called. Your job is to return ``true`` to allow access and ``false`` to deny access. - The ``$token`` can be used to find the current user object (if any). In this - example, all of the complex business logic is included to determine access. + The ``$token`` can be used to find the current user object (if any). + The ``$vote`` argument can be used to provide an explanation for the vote. + This explanation is included in log messages and on exception pages. .. _declaring-the-voter-as-a-service: @@ -256,7 +266,7 @@ with ``ROLE_SUPER_ADMIN``:: ) { } - protected function voteOnAttribute($attribute, mixed $subject, TokenInterface $token): bool + protected function voteOnAttribute($attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool { // ... @@ -292,6 +302,89 @@ If you're using the :ref:`default services.yaml configuration serializer() - ->defaultContext('', [ + ->defaultContext([ 'allow_extra_attributes' => false, ]) ; @@ -516,8 +516,8 @@ You can also specify a context specific to normalization or denormalization: attributes: createdAt: contexts: - - normalizationContext: { datetime_format: 'Y-m-d' } - denormalizationContext: { datetime_format: !php/const \DateTime::RFC3339 } + - normalization_context: { datetime_format: 'Y-m-d' } + denormalization_context: { datetime_format: !php/const \DateTime::RFC3339 } .. code-block:: xml @@ -1239,6 +1239,71 @@ setting the ``name_converter`` setting to ]; $serializer = new Serializer($normalizers, $encoders); +snake_case to CamelCase +~~~~~~~~~~~~~~~~~~~~~~~ + +In Symfony applications, it is common to use camelCase for naming properties. +However some packages may follow a snake_case convention. + +Symfony provides a built-in name converter designed to transform between +CamelCase and snake_case styles during serialization and deserialization +processes. You can use it instead of the metadata-aware name converter by +setting the ``name_converter`` setting to +``serializer.name_converter.snake_case_to_camel_case``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/serializer.yaml + framework: + serializer: + name_converter: 'serializer.name_converter.snake_case_to_camel_case' + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/serializer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->serializer() + ->nameConverter('serializer.name_converter.snake_case_to_camel_case') + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter; + use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + + // ... + $normalizers = [ + new ObjectNormalizer(null, new SnakeCaseToCamelCaseNameConverter()), + ]; + $serializer = new Serializer($normalizers, $encoders); + +.. versionadded:: 7.2 + + The snake_case to CamelCase converter was introduced in Symfony 7.2. + .. _serializer-built-in-normalizers: Serializer Normalizers @@ -1322,6 +1387,14 @@ normalizers (in order of priority): By default, an exception is thrown when data is not a valid backed enumeration. If you want ``null`` instead, you can set the ``BackedEnumNormalizer::ALLOW_INVALID_VALUES`` option. +:class:`Symfony\\Component\\Serializer\\Normalizer\\NumberNormalizer` + This normalizer converts between :phpclass:`BcMath\\Number` or :phpclass:`GMP` objects and + strings or integers. + +.. versionadded:: 7.2 + + The ``NumberNormalizer`` was introduced in Symfony 7.2. + :class:`Symfony\\Component\\Serializer\\Normalizer\\DataUriNormalizer` This normalizer converts between :phpclass:`SplFileInfo` objects and a `data URI`_ string (``data:...``) such that files can be embedded into @@ -1344,6 +1417,28 @@ normalizers (in order of priority): This denormalizer converts an array of arrays to an array of objects (with the given type). See :ref:`Handling Arrays `. + Use :class:`Symfony\\Component\\PropertyInfo\\PropertyInfoExtractor` to provide + hints with annotations like ``@var Person[]``: + + .. configuration-block:: + + .. code-block:: php-standalone + + use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; + use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; + use Symfony\Component\PropertyInfo\PropertyInfoExtractor; + use Symfony\Component\Serializer\Encoder\JsonEncoder; + use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; + use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; + use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; + use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + use Symfony\Component\Serializer\Serializer; + + $propertyInfo = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); + $normalizers = [new ObjectNormalizer(new ClassMetadataFactory(new AttributeLoader()), null, null, $propertyInfo), new ArrayDenormalizer()]; + + $this->serializer = new Serializer($normalizers, [new JsonEncoder()]); + :class:`Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer` This is the most powerful default normalizer and used for any object that could not be normalized by the other normalizers. @@ -1369,7 +1464,7 @@ Built-in Normalizers ~~~~~~~~~~~~~~~~~~~~ Besides the normalizers registered by default (see previous section), the -serializer component also provides some extra normalizers.You can register +serializer component also provides some extra normalizers. You can register these by defining a service and tag it with :ref:`serializer.normalizer `. For instance, to use the ``CustomNormalizer`` you have to define a service like: @@ -1472,6 +1567,254 @@ like: PropertyNormalizer::NORMALIZE_VISIBILITY => PropertyNormalizer::NORMALIZE_PUBLIC | PropertyNormalizer::NORMALIZE_PROTECTED, ]); +Named Serializers +----------------- + +.. versionadded:: 7.2 + + Named serializers were introduced in Symfony 7.2. + +Sometimes, you may need multiple configurations for the serializer, such as +different default contexts, name converters, or sets of normalizers and encoders, +depending on the use case. For example, when your application communicates with +multiple APIs, each of which follows its own set of serialization rules. + +You can achieve this by configuring multiple serializer instances using +the ``named_serializers`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/serializer.yaml + framework: + serializer: + named_serializers: + api_client1: + name_converter: 'serializer.name_converter.camel_case_to_snake_case' + default_context: + enable_max_depth: true + api_client2: + default_context: + enable_max_depth: false + + .. code-block:: xml + + + + + + + + + + + true + + + + + + false + + + + + + + + .. code-block:: php + + // config/packages/serializer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->serializer() + ->namedSerializer('api_client1') + ->nameConverter('serializer.name_converter.camel_case_to_snake_case') + ->defaultContext([ + 'enable_max_depth' => true, + ]) + ; + $framework->serializer() + ->namedSerializer('api_client2') + ->defaultContext([ + 'enable_max_depth' => false, + ]) + ; + }; + +You can inject these different serializer instances +using :ref:`named aliases `:: + + namespace App\Controller; + + // ... + use Symfony\Component\DependencyInjection\Attribute\Target; + + class PersonController extends AbstractController + { + public function index( + SerializerInterface $serializer, // default serializer + SerializerInterface $apiClient1Serializer, // api_client1 serializer + #[Target('apiClient2.serializer')] // api_client2 serializer + SerializerInterface $customName, + ) { + // ... + } + } + +By default, named serializers use the built-in set of normalizers and encoders, +just like the main serializer service. However, you can customize them by +registering additional normalizers or encoders for a specific named serializer. +To do that, add a ``serializer`` attribute to +the :ref:`serializer.normalizer ` +or :ref:`serializer.encoder ` tags: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + Symfony\Component\Serializer\Normalizer\CustomNormalizer: + # prevent this normalizer from being automatically added to the default serializer + autoconfigure: false + tags: + # add this normalizer only to a specific named serializer + - serializer.normalizer: { serializer: 'api_client1' } + # add this normalizer to several named serializers + - serializer.normalizer: { serializer: [ 'api_client1', 'api_client2' ] } + # add this normalizer to all serializers, including the default one + - serializer.normalizer: { serializer: '*' } + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\Serializer\Normalizer\CustomNormalizer; + + return function(ContainerConfigurator $container) { + // ... + + $services->set(CustomNormalizer::class) + // prevent this normalizer from being automatically added to the default serializer + ->autoconfigure(false) + + // add this normalizer only to a specific named serializer + ->tag('serializer.normalizer', ['serializer' => 'api_client1']) + // add this normalizer to several named serializers + ->tag('serializer.normalizer', ['serializer' => ['api_client1', 'api_client2']]) + // add this normalizer to all serializers, including the default one + ->tag('serializer.normalizer', ['serializer' => '*']) + ; + }; + +When the ``serializer`` attribute is not set, the service is registered only with +the default serializer. + +Each normalizer or encoder used in a named serializer is tagged with a +``serializer.normalizer.`` or ``serializer.encoder.`` tag. +You can inspect their priorities using the following command: + +.. code-block:: terminal + + $ php bin/console debug:container --tag serializer.. + +Additionally, you can exclude the default set of normalizers and encoders from a +named serializer by setting the ``include_built_in_normalizers`` and +``include_built_in_encoders`` options to ``false``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/serializer.yaml + framework: + serializer: + named_serializers: + api_client1: + include_built_in_normalizers: false + include_built_in_encoders: true + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/serializer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->serializer() + ->namedSerializer('api_client1') + ->includeBuiltInNormalizers(false) + ->includeBuiltInEncoders(true) + ; + }; + Debugging the Serializer ------------------------ @@ -1534,6 +1877,14 @@ to ``true``:: ]); // $jsonContent contains {"name":"Jane Doe"} +Preserving Empty Objects +~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the Serializer transforms an empty array to ``[]``. You can change +this behavior by setting the ``AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS`` +context option to ``true``. When the value is an instance of ``\ArrayObject()``, +the serialized data will be ``{}``. + Handling Uninitialized Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -2033,6 +2384,70 @@ correct class for properties typed as ``InvoiceItemInterface``:: $invoiceLine = $serializer->deserialize($jsonString, InvoiceLine::class, 'json'); // $invoiceLine contains new InvoiceLine(new Product(...)) +You can add a default type to avoid the need to add the type property +when deserializing: + +.. configuration-block:: + + .. code-block:: php-attributes + + namespace App\Model; + + use Symfony\Component\Serializer\Attribute\DiscriminatorMap; + + #[DiscriminatorMap( + typeProperty: 'type', + mapping: [ + 'product' => Product::class, + 'shipping' => Shipping::class, + ], + defaultType: 'product', + )] + interface InvoiceItemInterface + { + // ... + } + + .. code-block:: yaml + + App\Model\InvoiceItemInterface: + discriminator_map: + type_property: type + mapping: + product: 'App\Model\Product' + shipping: 'App\Model\Shipping' + default_type: product + + .. code-block:: xml + + + + + + + + + + + +Now it deserializes like this: + +.. configuration-block:: + + .. code-block:: php + + // $jsonString does NOT contain "type" in "invoiceItem" + $invoiceLine = $serializer->deserialize('{"invoiceItem":{...},...}', InvoiceLine::class, 'json'); + // $invoiceLine contains new InvoiceLine(new Product(...)) + +.. versionadded:: 7.3 + + The ``defaultType`` parameter was added in Symfony 7.3. + .. _serializer-unwrapping-denormalizer: Deserializing Input Partially (Unwrapping) diff --git a/serializer/custom_normalizer.rst b/serializer/custom_normalizer.rst index 7435474c05c..4e78d9d394e 100644 --- a/serializer/custom_normalizer.rst +++ b/serializer/custom_normalizer.rst @@ -36,16 +36,16 @@ normalization process:: ) { } - public function normalize($topic, ?string $format = null, array $context = []): array + public function normalize(mixed $data, ?string $format = null, array $context = []): array { - $data = $this->normalizer->normalize($topic, $format, $context); + $normalizedData = $this->normalizer->normalize($data, $format, $context); // Here, add, edit, or delete some data: - $data['href']['self'] = $this->router->generate('topic_show', [ - 'id' => $topic->getId(), + $normalizedData['href']['self'] = $this->router->generate('topic_show', [ + 'id' => $data->getId(), ], UrlGeneratorInterface::ABSOLUTE_URL); - return $data; + return $normalizedData; } public function supportsNormalization($data, ?string $format = null, array $context = []): bool @@ -126,41 +126,32 @@ If you're not using ``autoconfigure``, you have to tag the service with ; }; -Performance of Normalizers/Denormalizers -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Improving Performance of Normalizers/Denormalizers +-------------------------------------------------- -To figure which normalizer (or denormalizer) must be used to handle an object, -the :class:`Symfony\\Component\\Serializer\\Serializer` class will call the -:method:`Symfony\\Component\\Serializer\\Normalizer\\NormalizerInterface::supportsNormalization` -(or :method:`Symfony\\Component\\Serializer\\Normalizer\\DenormalizerInterface::supportsDenormalization`) -of all registered normalizers (or denormalizers) in a loop. +Both :class:Symfony\\Component\\Serializer\\Normalizer\\NormalizerInterface +and :class:Symfony\\Component\\Serializer\\Normalizer\\DenormalizerInterface +define a ``getSupportedTypes()`` method to declare which types they support and +whether their ``supports*()`` result can be cached. -Additionally, both -:class:`Symfony\\Component\\Serializer\\Normalizer\\NormalizerInterface` -and :class:`Symfony\\Component\\Serializer\\Normalizer\\DenormalizerInterface` -contain the ``getSupportedTypes()`` method. This method allows normalizers or -denormalizers to declare the type of objects they can handle, and whether they -are cacheable. With this info, even if the ``supports*()`` call is not cacheable, -the Serializer can skip a ton of method calls to ``supports*()`` improving -performance substantially in some cases. +This **does not** cache the actual normalization or denormalization result. It +only **caches the decision** of whether a normalizer supports a given type, allowing +the Serializer to skip unnecessary ``supports*()`` calls and improve performance. The ``getSupportedTypes()`` method should return an array where the keys -represent the supported types, and the values indicate whether the result of -the ``supports*()`` method call can be cached or not. The format of the -returned array is as follows: +represent the supported types, and the values indicate whether the result of the +corresponding ``supports*()`` call can be cached. The array format is as follows: #. The special key ``object`` can be used to indicate that the normalizer or denormalizer supports any classes or interfaces. #. The special key ``*`` can be used to indicate that the normalizer or - denormalizer might support any types. -#. The other keys in the array should correspond to specific types that the - normalizer or denormalizer supports. -#. The values associated with each type should be a boolean indicating if the - result of the ``supports*()`` method call for that type can be cached or not. - A value of ``true`` means that the result is cacheable, while ``false`` means - that the result is not cacheable. -#. A ``null`` value for a type means that the normalizer or denormalizer does - not support that type. + denormalizer might support any type. +#. Other keys should correspond to specific types that the normalizer or + denormalizer supports. +#. The values should be booleans indicating whether the result of the + ``supports*()`` call for that type is cacheable. Use ``true`` if the result + can be cached, ``false`` if it cannot. +#. A ``null`` value means the normalizer or denormalizer does not support that type. Here is an example of how to use the ``getSupportedTypes()`` method:: @@ -173,9 +164,9 @@ Here is an example of how to use the ``getSupportedTypes()`` method:: public function getSupportedTypes(?string $format): array { return [ - 'object' => null, // Doesn't support any classes or interfaces - '*' => false, // Supports any other types, but the result is not cacheable - MyCustomClass::class => true, // Supports MyCustomClass and result is cacheable + 'object' => null, // doesn't support any classes or interfaces + '*' => false, // supports any other types, but the decision is not cacheable + MyCustomClass::class => true, // supports MyCustomClass and decision is cacheable ]; } } diff --git a/serializer/encoders.rst b/serializer/encoders.rst index 3cf1ff912a4..8238d4d057d 100644 --- a/serializer/encoders.rst +++ b/serializer/encoders.rst @@ -65,6 +65,11 @@ are available to customize the behavior of the encoder: ``csv_end_of_line`` (default: ``\n``) Sets the character(s) used to mark the end of each line in the CSV file. ``csv_escape_char`` (default: empty string) + + .. deprecated:: 7.2 + + The ``csv_escape_char`` option was deprecated in Symfony 7.2. + Sets the escape character (at most one character). ``csv_key_separator`` (default: ``.``) Sets the separator for array's keys during its flattening @@ -197,14 +202,19 @@ These are the options available on the :ref:`serializer context ``, ``&``) in `a CDATA section`_ like following: ````. -``cdata_wrapping_pattern`` (default: ````/[<>&]/````) +``cdata_wrapping_pattern`` (default: ``/[<>&]/``) A regular expression pattern to determine if a value should be wrapped in a CDATA section. +``ignore_empty_attributes`` (default: ``false``) + If set to true, ignores all attributes with empty values in the generated XML .. versionadded:: 7.1 The ``cdata_wrapping_pattern`` option was introduced in Symfony 7.1. +.. versionadded:: 7.3 + + The ``ignore_empty_attributes`` option was introduced in Symfony 7.3. Example with a custom ``context``:: @@ -261,7 +271,7 @@ Creating a Custom Encoder Imagine you want to serialize and deserialize `NEON`_. For that you'll have to create your own encoder:: - // src/Serializer/YamlEncoder.php + // src/Serializer/NeonEncoder.php namespace App\Serializer; use Nette\Neon\Neon; diff --git a/service_container.rst b/service_container.rst index eb2e7fb60ae..6086ae1d946 100644 --- a/service_container.rst +++ b/service_container.rst @@ -162,10 +162,6 @@ each time you ask for it. # this creates a service per class whose id is the fully-qualified class name App\: resource: '../src/' - exclude: - - '../src/DependencyInjection/' - - '../src/Entity/' - - '../src/Kernel.php' # order is important in this file because service definitions # always *replace* previous ones; add your own service configuration below @@ -187,7 +183,7 @@ each time you ask for it. - + @@ -212,8 +208,7 @@ each time you ask for it. // makes classes in src/ available to be used as services // this creates a service per class whose id is the fully-qualified class name - $services->load('App\\', '../src/') - ->exclude('../src/{DependencyInjection,Entity,Kernel.php}'); + $services->load('App\\', '../src/'); // order is important in this file because service definitions // always *replace* previous ones; add your own service configuration below @@ -221,15 +216,57 @@ each time you ask for it. .. tip:: - The value of the ``resource`` and ``exclude`` options can be any valid - `glob pattern`_. The value of the ``exclude`` option can also be an - array of glob patterns. + The value of the ``resource`` option can be any valid `glob pattern`_. 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 how to :ref:`import many services at once ` with resource. + If some files or directories in your project should not become services, you + can exclude them using the ``exclude`` option: + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + App\: + resource: '../src/' + exclude: + - '../src/SomeDirectory/' + - '../src/AnotherDirectory/' + - '../src/SomeFile.php' + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $container): void { + // ... + + $services->load('App\\', '../src/') + ->exclude('../src/{SomeDirectory,AnotherDirectory,Kernel.php}'); + }; + If you'd prefer to manually wire your service, you can :ref:`use explicit configuration `. @@ -260,6 +297,32 @@ as a service in some environments:: // ... } +If you want to exclude a service from being registered in a specific +environment, you can use the ``#[WhenNot]`` attribute:: + + use Symfony\Component\DependencyInjection\Attribute\WhenNot; + + // SomeClass is registered in all environments except "dev" + + #[WhenNot(env: 'dev')] + class SomeClass + { + // ... + } + + // you can apply more than one WhenNot attribute to the same class + + #[WhenNot(env: 'dev')] + #[WhenNot(env: 'test')] + class AnotherClass + { + // ... + } + +.. versionadded:: 7.2 + + The ``#[WhenNot]`` attribute was introduced in Symfony 7.2. + .. _services-constructor-injection: Injecting Services/Config into a Service @@ -1042,6 +1105,14 @@ application to production (e.g. in your continuous integration server): $ php bin/console lint:container + # optionally, you can force the resolution of environment variables; + # the command will fail if any of those environment variables are missing + $ php bin/console lint:container --resolve-env-vars + +.. versionadded:: 7.2 + + The ``--resolve-env-vars`` option was introduced in Symfony 7.2. + Performing those checks whenever the container is compiled can hurt performance. That's why they are implemented in :doc:`compiler passes ` called ``CheckTypeDeclarationsPass`` and ``CheckAliasValidityPass``, which are diff --git a/service_container/alias_private.rst b/service_container/alias_private.rst index f99f7cb5f3e..22bf649d861 100644 --- a/service_container/alias_private.rst +++ b/service_container/alias_private.rst @@ -181,6 +181,32 @@ This means that when using the container directly, you can access the # ... app.mailer: '@App\Mail\PhpMailer' +The ``#[AsAlias]`` attribute can also be limited to one or more specific +:ref:`config environments ` using the ``when`` argument:: + + // src/Mail/PhpMailer.php + namespace App\Mail; + + // ... + use Symfony\Component\DependencyInjection\Attribute\AsAlias; + + #[AsAlias(id: 'app.mailer', when: 'dev')] + class PhpMailer + { + // ... + } + + // pass an array to apply it in multiple config environments + #[AsAlias(id: 'app.mailer', when: ['dev', 'test'])] + class PhpMailer + { + // ... + } + +.. versionadded:: 7.3 + + The ``when`` argument of the ``#[AsAlias]`` attribute was introduced in Symfony 7.3. + .. tip:: When using ``#[AsAlias]`` attribute, you may omit passing ``id`` argument diff --git a/service_container/autowiring.rst b/service_container/autowiring.rst index 48bb40985b8..ea1bf1b12ff 100644 --- a/service_container/autowiring.rst +++ b/service_container/autowiring.rst @@ -408,8 +408,8 @@ Suppose you create a second class - ``UppercaseTransformer`` that implements If you register this as a service, you now have *two* services that implement the ``App\Util\TransformerInterface`` type. Autowiring subsystem can not decide which one to use. Remember, autowiring isn't magic; it looks for a service -whose id matches the type-hint. So you need to choose one by creating an alias -from the type to the correct service id (see :ref:`autowiring-interface-alias`). +whose id matches the type-hint. So you need to choose one by :ref:`creating an alias +` from the type to the correct service id. 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. @@ -862,6 +862,40 @@ typed properties: } } +Autowiring Anonymous Services Inline +------------------------------------ + +.. versionadded:: 7.1 + + The ``#[AutowireInline]`` attribute was added in Symfony 7.1. + +Similar to how anonymous services can be defined inline in configuration files, +the :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireInline` +attribute allows you to declare anonymous services inline, directly next to their +corresponding arguments:: + + public function __construct( + #[AutowireInline( + factory: [ScopingHttpClient::class, 'forBaseUri'], + arguments: [ + '$baseUri' => 'https://api.example.com', + '$defaultOptions' => [ + 'auth_bearer' => '%env(EXAMPLE_TOKEN)%', + ], + ] + )] + private HttpClientInterface $client, + ) { + } + +This example tells Symfony to inject an object created by calling the +``ScopingHttpClient::forBaseUri()`` factory with the specified base URI and +default options. This is just one example: you can use the ``#[AutowireInline]`` +attribute to define any kind of anonymous service. + +While this approach is convenient for simple service definitions, consider moving +complex or heavily configured services to a configuration file to ease maintenance. + Autowiring Controller Action Methods ------------------------------------ diff --git a/service_container/compiler_passes.rst b/service_container/compiler_passes.rst index 11458a4e8e3..096c60c2642 100644 --- a/service_container/compiler_passes.rst +++ b/service_container/compiler_passes.rst @@ -77,10 +77,11 @@ bundle class:: namespace App\MyBundle; use App\DependencyInjection\Compiler\CustomPass; + use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; - class MyBundle extends AbstractBundle + class MyBundle extends AbstractBundle implements CompilerPassInterface { public function process(ContainerBuilder $container): void { diff --git a/service_container/debug.rst b/service_container/debug.rst index c09413e7213..9e3e28a5343 100644 --- a/service_container/debug.rst +++ b/service_container/debug.rst @@ -52,5 +52,8 @@ its id: $ php bin/console debug:container App\Service\Mailer - # to show the service arguments: - $ php bin/console debug:container App\Service\Mailer --show-arguments +.. deprecated:: 7.3 + + Starting in Symfony 7.3, this command displays the service arguments by default. + In earlier Symfony versions, you needed to use the ``--show-arguments`` option, + which is now deprecated. diff --git a/service_container/factories.rst b/service_container/factories.rst index 0c6a4724609..9864287d57a 100644 --- a/service_container/factories.rst +++ b/service_container/factories.rst @@ -389,7 +389,7 @@ e.g. change the service based on a parameter: # you can use the arg() function to retrieve an argument from the definition App\Email\NewsletterManagerInterface: - factory: "@=arg(0).createNewsletterManager() ?: service("default_newsletter_manager")" + factory: '@=arg(0).createNewsletterManager() ?: service("default_newsletter_manager")' arguments: - '@App\Email\NewsletterManagerFactory' @@ -410,7 +410,7 @@ e.g. change the service based on a parameter: - + diff --git a/service_container/import.rst b/service_container/import.rst index d5056032115..47af34d3a34 100644 --- a/service_container/import.rst +++ b/service_container/import.rst @@ -82,7 +82,6 @@ a relative or absolute path to the imported file: App\: resource: '../src/*' - exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' # ... @@ -104,8 +103,98 @@ a relative or absolute path to the imported file: - + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $container): void { + $container->import('services/mailer.php'); + // If you want to import a whole directory: + $container->import('services/'); + + $services = $container->services() + ->defaults() + ->autowire() + ->autoconfigure() + ; + + $services->load('App\\', '../src/*'); + }; + +When loading a configuration file, Symfony first processes all imported files in +the order they are listed under the ``imports`` key. After all imports are processed, +it then processes the parameters and services defined directly in the current file. +In practice, this means that **later definitions override earlier ones**. + +For example, if you use the :ref:`default services.yaml configuration ` +as in the above example, your main ``config/services.yaml`` file uses the ``App\`` +namespace to auto-discover services and loads them after all imported files. +If an imported file (e.g. ``config/services/mailer.yaml``) defines a service that +is also auto-discovered, the definition from ``services.yaml`` will take precedence. + +To make sure your specific service definitions are not overridden by auto-discovery, +consider one of the following strategies: + +#. :ref:`Exclude services from auto-discovery ` +#. :ref:`Override services in the same file ` +#. :ref:`Control import order ` + +.. _import-exclude-services-from-auto-discovery: + +**Exclude services from auto-discovery** + +Adjust the ``App\`` definition to use the ``exclude`` option. This prevents Symfony +from auto-registering classes that are defined manually elsewhere: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + imports: + - { resource: services/mailer.yaml } + # ... other imports + + services: + _defaults: + autowire: true + autoconfigure: true + + App\: + resource: '../src/*' + exclude: + - '../src/Mailer/' + - '../src/SpecificClass.php' + + .. code-block:: xml + + + + + + + + + + + + + + + + ../src/Mailer/ + ../src/SpecificClass.php + @@ -128,20 +217,209 @@ a relative or absolute path to the imported file: ; $services->load('App\\', '../src/*') - ->exclude('../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'); + ->exclude([ + '../src/Mailer/', + '../src/SpecificClass.php', + ]); }; -When loading a configuration file, Symfony loads first the imported files and -then it processes the parameters and services defined in the file. If you use the -:ref:`default services.yaml configuration ` -as in the above example, the ``App\`` definition creates services for classes -found in ``../src/*``. If your imported file defines services for those classes -too, they will be overridden. - -A possible solution for this is to add the classes and/or directories of the -imported files in the ``exclude`` option of the ``App\`` definition. Another -solution is to not use imports and add the service definitions in the same file, -but after the ``App\`` definition to override it. +.. _import-override-services-in-the-same-file: + +**Override services in the same file** + +You can define specific services after the ``App\`` auto-discovery block in the +same file. These later definitions will override the auto-registered ones: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + _defaults: + autowire: true + autoconfigure: true + + App\: + resource: '../src/*' + + App\Mailer\MyMailer: + arguments: ['%env(MAILER_DSN)%'] + + .. code-block:: xml + + + + + + + + + + + + + + + + + + %env(MAILER_DSN)% + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $container): void { + $services = $container->services() + ->defaults() + ->autowire() + ->autoconfigure(); + + $services->load('App\\', '../src/*'); + + $services->set(App\Mailer\MyMailer::class) + ->arg(0, '%env(MAILER_DSN)%'); + }; + +.. _import-control-import-order: + +**Control import order** + +Move the ``App\`` auto-discovery config to a separate file and import it +before more specific service files. This way, specific service definitions +can override the auto-discovered ones. + +.. configuration-block:: + + .. code-block:: yaml + + # config/services/autodiscovery.yaml + services: + _defaults: + autowire: true + autoconfigure: true + + App\: + resource: '../../src/*' + exclude: + - '../../src/Mailer/' + + # config/services/mailer.yaml + services: + App\Mailer\SpecificMailer: + # ... custom configuration + + # config/services.yaml + imports: + - { resource: services/autodiscovery.yaml } + - { resource: services/mailer.yaml } + - { resource: services/ } + + services: + # definitions here override anything from the imports above + # consider keeping most definitions inside imported files + + .. code-block:: xml + + + + + + + + + + ../../src/Mailer/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services/autodiscovery.php + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + return function (ContainerConfigurator $container): void { + $services = $container->services() + ->defaults() + ->autowire() + ->autoconfigure(); + + $services->load('App\\', '../../src/*') + ->exclude([ + '../../src/Mailer/', + ]); + }; + + // config/services/mailer.php + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + return function (ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set(App\Mailer\SpecificMailer::class); + // Add any custom configuration here if needed + }; + + // config/services.php + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + return function (ContainerConfigurator $container): void { + $container->import('services/autodiscovery.php'); + $container->import('services/mailer.php'); + $container->import('services/'); + + $services = $container->services(); + + // definitions here override anything from the imports above + // consider keeping most definitions inside imported files + }; .. include:: /components/dependency_injection/_imports-parameters-note.rst.inc diff --git a/service_container/lazy_services.rst b/service_container/lazy_services.rst index 23d76a4cfbf..abb3c2cca7f 100644 --- a/service_container/lazy_services.rst +++ b/service_container/lazy_services.rst @@ -26,9 +26,6 @@ until you interact with the proxy in some way. 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 - default values for built-in PHP classes (e.g. ``PDO``). - .. _lazy-services_configuration: Configuration @@ -78,11 +75,6 @@ same signature of the class representing the service should be injected. A lazy itself when being accessed for the first time). The same happens when calling ``Container::get()`` directly. -To check if your lazy service works you can check the interface of the received object:: - - dump(class_implements($service)); - // the output should include "Symfony\Component\VarExporter\LazyObjectInterface" - You can also configure your service's laziness thanks to the :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Autoconfigure` attribute. For example, to define your service as lazy use the following:: diff --git a/service_container/service_closures.rst b/service_container/service_closures.rst index cedbaaa2bf9..88b0ab64002 100644 --- a/service_container/service_closures.rst +++ b/service_container/service_closures.rst @@ -52,6 +52,13 @@ argument of type ``service_closure``: # In case the dependency is optional # arguments: [!service_closure '@?mailer'] + # you can also use the special '@>' syntax as a shortcut of '!service_closure' + App\Service\AnotherService: + arguments: ['@>mailer'] + + # the shortcut also works for optional dependencies + # arguments: ['@>?mailer'] + .. code-block:: xml @@ -90,6 +97,10 @@ argument of type ``service_closure``: // ->args([service_closure('mailer')->ignoreOnInvalid()]); }; +.. versionadded:: 7.3 + + The ``@>`` shortcut syntax for YAML was introduced in Symfony 7.3. + .. seealso:: Service closures can be injected :ref:`by using autowiring ` diff --git a/service_container/service_subscribers_locators.rst b/service_container/service_subscribers_locators.rst index 9c6451931d1..9026478cf33 100644 --- a/service_container/service_subscribers_locators.rst +++ b/service_container/service_subscribers_locators.rst @@ -36,7 +36,7 @@ to handle their respective command when it is asked for:: public function handle(Command $command): mixed { - $commandClass = get_class($command); + $commandClass = $command::class; if (!$handler = $this->handlerMap[$commandClass] ?? null) { return; @@ -94,7 +94,7 @@ in the service subscriber:: public function handle(Command $command): mixed { - $commandClass = get_class($command); + $commandClass = $command::class; if ($this->locator->has($commandClass)) { $handler = $this->locator->get($commandClass); @@ -270,8 +270,8 @@ the following dependency injection attributes in the ``getSubscribedServices()`` method directly: * :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Autowire` -* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedIterator` -* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedLocator` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireIterator` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireLocator` * :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Target` * :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireDecorated` @@ -282,8 +282,8 @@ This is done by having ``getSubscribedServices()`` return an array of use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; - use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; - use Symfony\Component\DependencyInjection\Attribute\TaggedLocator; + use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; + use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Contracts\Service\Attribute\SubscribedService; @@ -299,11 +299,11 @@ This is done by having ``getSubscribedServices()`` return an array of // Target new SubscribedService('event.logger', LoggerInterface::class, attributes: new Target('eventLogger')), - // TaggedIterator - new SubscribedService('loggers', 'iterable', attributes: new TaggedIterator('logger.tag')), + // AutowireIterator + new SubscribedService('loggers', 'iterable', attributes: new AutowireIterator('logger.tag')), - // TaggedLocator - new SubscribedService('handlers', ContainerInterface::class, attributes: new TaggedLocator('handler.tag')), + // AutowireLocator + new SubscribedService('handlers', ContainerInterface::class, attributes: new AutowireLocator('handler.tag')), ]; } @@ -320,10 +320,10 @@ This is done by having ``getSubscribedServices()`` return an array of The above example requires using ``3.2`` version or newer of ``symfony/service-contracts``. .. _service-locator_autowire-locator: -.. _service-locator_autowire-iterator: +.. _the-autowirelocator-and-autowireiterator-attributes: -The AutowireLocator and AutowireIterator Attributes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The AutowireLocator Attribute +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Another way to define a service locator is to use the :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireLocator` @@ -350,7 +350,7 @@ attribute:: public function handle(Command $command): mixed { - $commandClass = get_class($command); + $commandClass = $command::class; if ($this->handlers->has($commandClass)) { $handler = $this->handlers->get($commandClass); @@ -397,13 +397,43 @@ attribute:: } } -.. note:: +.. _service-locator_autowire-iterator: - To receive an iterable instead of a service locator, you can switch the - :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireLocator` - attribute to - :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireIterator` - attribute. +The AutowireIterator Attribute +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A variant of ``AutowireLocator`` that injects an iterable of services tagged +with a specific :doc:`tag `. This is useful to loop +over a set of tagged services instead of retrieving them individually. + +For example, to collect all handlers for different command types, use the +``AutowireIterator`` attribute and pass the tag used by those services:: + + // src/CommandBus.php + namespace App; + + use App\CommandHandler\BarHandler; + use App\CommandHandler\FooHandler; + use Psr\Container\ContainerInterface; + use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; + + class CommandBus + { + public function __construct( + #[AutowireIterator('command_handler')] + private iterable $handlers, // collects all services tagged with 'command_handler' + ) { + } + + public function handle(Command $command): mixed + { + foreach ($this->handlers as $handler) { + if ($handler->supports($command)) { + return $handler->handle($command); + } + } + } + } .. _service-subscribers-locators_defining-service-locator: @@ -975,8 +1005,8 @@ You can use the ``attributes`` argument of ``SubscribedService`` to add any of the following dependency injection attributes: * :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Autowire` -* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedIterator` -* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedLocator` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireIterator` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireLocator` * :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Target` * :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireDecorated` diff --git a/service_container/tags.rst b/service_container/tags.rst index 270d6702f5a..3a547042de7 100644 --- a/service_container/tags.rst +++ b/service_container/tags.rst @@ -155,22 +155,30 @@ In a Symfony application, call this method in your kernel class:: } } -In a Symfony bundle, call this method in the ``load()`` method of the -:doc:`bundle extension class `:: +In bundles extending the :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` +class, call this method in the ``loadExtension()`` method of the main bundle class:: - // src/DependencyInjection/MyBundleExtension.php - class MyBundleExtension extends Extension - { - // ... + // ... + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; - public function load(array $configs, ContainerBuilder $container): void + class MyBundle extends AbstractBundle + { + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void { - $container->registerForAutoconfiguration(CustomInterface::class) + $builder + ->registerForAutoconfiguration(CustomInterface::class) ->addTag('app.custom_tag') ; } } +.. note:: + + For bundles not extending the ``AbstractBundle`` class, call this method in + the ``load()`` method of the :doc:`bundle extension class `. + Autoconfiguration registering is not limited to interfaces. It is possible to use PHP attributes to autoconfigure services by using the :method:`Symfony\\Component\\DependencyInjection\\ContainerBuilder::registerAttributeForAutoconfiguration` @@ -750,7 +758,7 @@ directly via PHP attributes: .. note:: - Some IDEs will show an error when using ``#[TaggedIterator]`` together + Some IDEs will show an error when using ``#[AutowireIterator]`` 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 @@ -1281,4 +1289,19 @@ be used directly on the class of the service you want to configure:: // ... } +You can apply the ``#[AsTaggedItem]`` attribute multiple times to register the +same service under different indexes:: + + #[AsTaggedItem(index: 'handler_one', priority: 5)] + #[AsTaggedItem(index: 'handler_two', priority: 20)] + class SomeService + { + // ... + } + +.. versionadded:: 7.3 + + The feature to apply the ``#[AsTaggedItem]`` attribute multiple times was + introduced in Symfony 7.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 9ddf3eb028d..8cb3462d920 100644 --- a/session.rst +++ b/session.rst @@ -115,7 +115,7 @@ sessions for anonymous users, you must *completely* avoid accessing the session. .. note:: Sessions will also be started when using features that rely on them internally, - such as the :ref:`CSRF protection in forms `. + such as the :ref:`stateful CSRF protection in forms `. .. _flash-messages: @@ -425,6 +425,11 @@ 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``. +.. deprecated:: 7.2 + + The ``sid_length`` and ``sid_bits_per_character`` options were deprecated + in Symfony 7.2 and will be ignored in Symfony 8.0. + 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. @@ -487,12 +492,11 @@ the ``php.ini`` directive ``session.gc_maxlifetime``. The meaning in this contex that any stored session that was saved more than ``gc_maxlifetime`` ago should be deleted. This allows one to expire records based on idle time. -However, some operating systems (e.g. Debian) do their own session handling and set -the ``session.gc_probability`` variable to ``0`` to stop PHP doing garbage -collection. That's why Symfony now overwrites this value to ``1``. - -If you wish to use the original value set in your ``php.ini``, add the following -configuration: +However, some operating systems (e.g. Debian) manage session handling differently +and set the ``session.gc_probability`` variable to ``0`` to prevent PHP from performing +garbage collection. By default, Symfony uses the value of the ``gc_probability`` +directive set in the ``php.ini`` file. If you can't modify this PHP setting, you +can configure it directly in Symfony: .. code-block:: yaml @@ -500,14 +504,19 @@ configuration: framework: session: # ... - gc_probability: null + gc_probability: 1 -You can configure these settings by passing ``gc_probability``, ``gc_divisor`` -and ``gc_maxlifetime`` in an array to the constructor of +Alternatively, you can configure these settings by passing ``gc_probability``, +``gc_divisor`` and ``gc_maxlifetime`` in an array to the constructor of :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage` or to the :method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage::setOptions` method. +.. versionadded:: 7.2 + + Using the ``php.ini`` directive as the default value for ``gc_probability`` + was introduced in Symfony 7.2. + .. _session-database: Store Sessions in a Database @@ -1843,8 +1852,8 @@ the example below: https://symfony.com/schema/dic/services/services-1.0.xsd"> - @@ -1857,7 +1866,7 @@ the example below: return static function (FrameworkConfig $framework): void { $framework->session() ->storageFactoryId('session.storage.factory.php_bridge') - ->handlerId('session.storage.native_file') + ->handlerId('session.handler.native_file') ; }; diff --git a/setup.rst b/setup.rst index c8654296986..20d71d112eb 100644 --- a/setup.rst +++ b/setup.rst @@ -48,10 +48,10 @@ application: .. code-block:: terminal # run this if you are building a traditional web application - $ symfony new my_project_directory --version="7.1.*" --webapp + $ symfony new my_project_directory --version="7.3.x-dev" --webapp # run this if you are building a microservice, console application or API - $ symfony new my_project_directory --version="7.1.*" + $ symfony new my_project_directory --version="7.3.x-dev" The only difference between these two commands is the number of packages installed by default. The ``--webapp`` option installs extra packages to give @@ -63,12 +63,12 @@ Symfony application using Composer: .. code-block:: terminal # run this if you are building a traditional web application - $ composer create-project symfony/skeleton:"7.1.*" my_project_directory + $ composer create-project symfony/skeleton:"7.3.x-dev" my_project_directory $ cd my_project_directory $ composer require webapp # run this if you are building a microservice, console application or API - $ composer create-project symfony/skeleton:"7.1.*" my_project_directory + $ composer create-project symfony/skeleton:"7.3.x-dev" my_project_directory No matter which command you run to create the Symfony application. All of them will create a new ``my_project_directory/`` directory, download some dependencies @@ -121,8 +121,8 @@ development. .. _symfony-binary-web-server: However for local development, the most convenient way of running Symfony is by -using the :doc:`local web server ` provided by the -``symfony`` binary. This local server provides among other things support for +using the :ref:`local web server ` provided by the +Symfony CLI tool. This local server provides among other things support for HTTP/2, concurrent requests, TLS/SSL and automatic generation of security certificates. @@ -249,9 +249,9 @@ workflows to make them fail when there are vulnerabilities. .. tip:: In continuous integration services you can check security vulnerabilities - using a different stand-alone project called `Local PHP Security Checker`_. - This is the same project used internally by ``check:security`` but much - smaller in size than the entire Symfony CLI. + by running the ``composer audit`` command. This uses the same data internally + as ``check:security`` but does not require installing the entire Symfony CLI + during CI or on CI workers. Symfony LTS Versions -------------------- @@ -318,7 +318,6 @@ Learn More .. _`The Symfony Demo Application`: https://github.com/symfony/demo .. _`Symfony Flex`: https://github.com/symfony/flex .. _`PHP security advisories database`: https://github.com/FriendsOfPHP/security-advisories -.. _`Local PHP Security Checker`: https://github.com/fabpot/local-php-security-checker .. _`Symfony releases`: https://symfony.com/releases .. _`Main recipe repository`: https://github.com/symfony/recipes .. _`Contrib recipe repository`: https://github.com/symfony/recipes-contrib diff --git a/setup/_update_dep_errors.rst.inc b/setup/_update_dep_errors.rst.inc index 49ae97067e4..5dc7e6745bc 100644 --- a/setup/_update_dep_errors.rst.inc +++ b/setup/_update_dep_errors.rst.inc @@ -24,7 +24,7 @@ versions of other libraries. Check your error message to debug. Another issue that may happen is that the project dependencies can be installed on your local computer but not on the remote server. This usually happens when the PHP versions are different on each machine. The solution is to add the -`platform`_ config option to your `composer.json` file to define the highest +`platform`_ config option to your ``composer.json`` file to define the highest PHP version allowed for the dependencies (set it to the server's PHP version). .. _`platform`: https://getcomposer.org/doc/06-config.md#platform diff --git a/setup/symfony_cli.rst b/setup/symfony_cli.rst new file mode 100644 index 00000000000..ccd79e4b423 --- /dev/null +++ b/setup/symfony_cli.rst @@ -0,0 +1,653 @@ +.. _symfony-server: +.. _symfony-local-web-server: + +Symfony CLI +=========== + +The **Symfony CLI** is a free and `open source`_ developer tool to help you build, +run, and manage your Symfony applications directly from your terminal. It's designed +to boost your productivity with smart features like: + +* **Web server** optimized for development, with **HTTPS support** +* **Docker** integration and automatic environment variable management +* Management of muktiple **PHP versions** +* Support for background **workers** +* Seamless integration with **Symfony Cloud** + +Installation +------------ + +The Symfony CLI is available as a standalone executable that supports Linux, +macOS, and Windows. Download and install it following the instructions on +`symfony.com/download`_. + +.. _symfony-cli-autocompletion: + +Shell Autocompletion +~~~~~~~~~~~~~~~~~~~~ + +The Symfony CLI supports autocompletion for Bash, Zsh, and Fish shells. This +helps you type commands faster and discover available options: + +.. code-block:: terminal + + # install autocompletion (do this only once) + $ symfony completion bash | sudo tee /etc/bash_completion.d/symfony + + # for Zsh users + $ symfony completion zsh > ~/.symfony_completion && echo "source ~/.symfony_completion" >> ~/.zshrc + + # for Fish users + $ symfony completion fish | source + +After installation, restart your terminal to enable autocompletion. The CLI will +also provide autocompletion for ``composer`` and ``console`` commands when it +detects a Symfony project. + +Creating New Symfony Applications +--------------------------------- + +The Symfony CLI includes a project creation command that helps you start new +projects quickly: + +.. code-block:: terminal + + # create a new Symfony project based on the latest stable version + $ symfony new my_project + + # create a project with the latest LTS (Long Term Support) version + $ symfony new my_project --version=lts + + # create a project based on a specific Symfony version + $ symfony new my_project --version=6.4 + + # create a project using the development version + $ symfony new my_project --version=next + + # all the previous commands create minimal projects with the least + # amount of dependencies possible; if you are building a website or + # web application, add this option to install all the common dependencies + $ symfony new my_project --webapp + + # Create a project based on the Symfony Demo application + $ symfony new my_project --demo + +.. tip:: + + Pass the ``--cloud`` option to initialize a Symfony Cloud project at the same + time the Symfony project is created. + +.. _symfony-cli-server: + +Running the Local Web Server +---------------------------- + +The Symfony CLI includes a **local web server** designed for development. It's +not intended for production use, but it provides features that improve the +developer experience: + +* HTTPS support with automatic certificate generation +* HTTP/2 support +* Automatic PHP version selection +* Integration with Docker services +* Built-in proxy for custom domain names + +.. _getting-started: + +Serving Your Application +~~~~~~~~~~~~~~~~~~~~~~~~ + +To serve a Symfony project with the local server: + +.. code-block:: terminal + + $ cd my-project/ + $ symfony server:start + + [OK] Web server listening on http://127.0.0.1:8000 + ... + +Now browse the given URL or run the following command to open it in the browser: + +.. code-block:: terminal + + $ symfony open:local + +.. tip:: + + If you work on more than one project, you can run multiple instances of the + Symfony server on your development machine. Each instance will find a different + available port. + +The ``server:start`` command blocks the current terminal to output the server +logs. To run the server in the background: + +.. code-block:: terminal + + $ symfony server:start -d + +Now you can continue working in the terminal and run other commands: + +.. code-block:: terminal + + # view the latest log messages + $ symfony server:log + + # stop the background server + $ symfony server:stop + +.. tip:: + + 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 applications that are not listed in the + firewall list. The solution is to run this command to sign the Symfony CLI: + + .. code-block:: terminal + + $ sudo codesign --force --deep --sign - $(whereis -q symfony) + +Enabling PHP-FPM +~~~~~~~~~~~~~~~~ + +.. note:: + + PHP-FPM must be installed locally for the Symfony server to utilize. + +When the server starts, it checks for ``web/index_dev.php``, ``web/index.php``, +``public/app_dev.php``, ``public/app.php`` in that order. If one is found, the +server will automatically start with PHP-FPM enabled. Otherwise the server will +start without PHP-FPM and will show a ``Page not found`` page when trying to +access a ``.php`` file in the browser. + +.. tip:: + + When an ``index.html`` and a front controller (e.g. ``index.php``) are both + present, the server will still start with PHP-FPM enabled, but the + ``index.html`` will take precedence. This means that if an ``index.html`` + file is present in ``public/`` or ``web/``, it will be displayed instead of + the ``index.php``, which would otherwise show, for example, the Symfony + application. + +Enabling HTTPS/TLS +~~~~~~~~~~~~~~~~~~ + +Running your application over HTTPS locally helps detect mixed content issues +early and allows using features that require secure connections. Traditionally, +this has been painful and complicated to set up, but the Symfony server automates +everything for you: + +.. code-block:: terminal + + # install the certificate authority (run this only once on your machine) + $ symfony server:ca:install + + # now start (or restart) your server; it will use HTTPS automatically + $ symfony server:start + +.. tip:: + + For WSL (Windows Subsystem for Linux), the newly created local certificate + authority needs to be imported manually: + + .. 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``. + +PHP Management +-------------- + +The Symfony CLI provides PHP management features, allowing you to use different +PHP versions and/or settings for different projects. + +Selecting PHP Version +~~~~~~~~~~~~~~~~~~~~~ + +If you have multiple PHP versions installed on your computer, you can tell +Symfony which one to use creating a file called ``.php-version`` at the project +root directory: + +.. code-block:: terminal + + $ cd my-project/ + + # use a specific PHP version + $ echo 8.2 > .php-version + + # use any PHP 8.x version available + $ echo 8 > .php-version + +To see all available PHP versions: + +.. code-block:: terminal + + $ symfony local:php:list + +.. tip:: + + You can create a ``.php-version`` file in a parent directory to set the same + PHP version for multiple projects. + +Custom PHP Configuration +~~~~~~~~~~~~~~~~~~~~~~~~ + +Override PHP settings per project by creating a ``php.ini`` file at the project +root: + +.. code-block:: ini + + ; php.ini + [Date] + date.timezone = Asia/Tokyo + + [PHP] + memory_limit = 256M + +Using PHP Commands +~~~~~~~~~~~~~~~~~~ + +Use ``symfony php`` to ensure commands run with the correct PHP version: + +.. code-block:: terminal + + # runs with the system's default PHP + $ php -v + + # runs with the project's PHP version + $ symfony php -v + + # this also works for Composer + $ symfony composer install + +Local Domain Names +------------------ + +By default, projects are accessible at a random port on the ``127.0.0.1`` +local IP. However, sometimes it is preferable to associate a domain name +(e.g. ``my-app.wip``) with them: + +* it's more convenient when working continuously on the same project because + port numbers can change but domains don't; +* the behavior of some applications depends on their domains/subdomains; +* to have stable endpoints, such as the local redirection URL for OAuth2. + +Setting up the Local Proxy +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Symfony CLI includes a proxy that allows using custom local domains. The +first time you use it, you must configure it as follows: + +#. Open the **proxy settings** of your operating system: + + * `Proxy settings in Windows`_; + * `Proxy settings in macOS`_; + * `Proxy settings in Ubuntu`_. + +#. Set the following URL as the value of the **Automatic Proxy Configuration**: + + ``http://127.0.0.1:7080/proxy.pac`` + +Now run this command to start the proxy: + +.. code-block:: terminal + + $ symfony proxy:start + +If the proxy doesn't work as explained in the following sections, check the following: + +* Some browsers (e.g. Chrome) require reapplying proxy settings (clicking on + ``Re-apply settings`` button on the ``chrome://net-internals/#proxy`` page) + or a full restart after starting the proxy. Otherwise, you'll see a + *"This webpage is not available"* error (``ERR_NAME_NOT_RESOLVED``); +* Some Operating Systems (e.g. macOS) don't apply proxy settings to local hosts + and domains by default. You may need to remove ``*.local`` and/or other + IP addresses from that list. +* Windows **requires** using ``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 +~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, Symfony uses ``.wip`` (for *Work in Progress*) as the local TLD for +custom domains. You can define a local domain for your project as follows: + +.. code-block:: terminal + + $ cd my-project/ + $ symfony proxy:domain:attach my-app + +Your application is now available at ``https://my-app.wip`` + +.. tip:: + + View all local domains and their configuration at http://127.0.0.1:7080 + +You can also use wildcards: + +.. code-block:: terminal + + $ symfony proxy:domain:attach "*.my-app" + +This allows accessing subdomains like ``https://api.my-app.wip`` or +``https://admin.my-app.wip``. + +When running console commands, set the ``https_proxy`` environment variable +to make custom domains work: + +.. code-block:: terminal + + # example with cURL + $ https_proxy=$(symfony proxy:url) curl https://my-domain.wip + + # example with Blackfire and cURL + $ https_proxy=$(symfony proxy:url) blackfire curl https://my-domain.wip + + # example with Cypress + $ https_proxy=$(symfony proxy:url) ./node_modules/bin/cypress open + +.. warning:: + + Although environment variable names are typically uppercase, the ``https_proxy`` + variable `is treated differently`_ and must be written in lowercase. + +.. tip:: + + If you prefer to use a different TLD, edit the ``~/.symfony5/proxy.json`` + file (where ``~`` means the path to your user directory) and change the + value of the ``tld`` option from ``wip`` to any other TLD. + +.. _symfony-server-docker: + +Docker Integration +------------------ + +The Symfony CLI provides full `Docker`_ integration for projects that +use it. To learn more about Docker and Symfony, see :doc:`docker`. +The local server automatically detects Docker services and exposes their +connection information as environment variables. + +Automatic Service Detection +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +With this ``compose.yaml``: + +.. code-block:: yaml + + services: + database: + image: mysql:8 + ports: [3306] + +The web server detects that a service exposing port ``3306`` is running for the +project. It understands that this is a MySQL service and creates environment +variables accordingly, using the service name (``database``) as a prefix: + +* ``DATABASE_URL`` +* ``DATABASE_HOST`` +* ``DATABASE_PORT`` + +Here is a list of supported services with their ports and default Symfony prefixes: + +============= ========= ====================== +Service Port Symfony default prefix +============= ========= ====================== +MySQL 3306 ``DATABASE_`` +PostgreSQL 5432 ``DATABASE_`` +Redis 6379 ``REDIS_`` +Memcached 11211 ``MEMCACHED_`` +RabbitMQ 5672 ``RABBITMQ_`` (set user and pass via Docker ``RABBITMQ_DEFAULT_USER`` and ``RABBITMQ_DEFAULT_PASS`` env var) +Elasticsearch 9200 ``ELASTICSEARCH_`` +MongoDB 27017 ``MONGODB_`` (set the database via a Docker ``MONGO_DATABASE`` env var) +Kafka 9092 ``KAFKA_`` +MailCatcher 1025/1080 ``MAILER_`` + or 25/80 +Blackfire 8707 ``BLACKFIRE_`` +Mercure 80 Always exposes ``MERCURE_PUBLIC_URL`` and ``MERCURE_URL`` (only works with the ``dunglas/mercure`` Docker image) +============= ========= ====================== + +If the service is not supported, generic environment variables are set: +``PORT``, ``IP``, and ``HOST``. + +You can open web management interfaces for the services that expose them +by clicking on the links in the "Server" section of the web debug toolbar +or by running these commands: + +.. code-block:: bash + + $ symfony open:local:webmail + $ symfony open:local:rabbitmq + +.. tip:: + + To debug and list all exported environment variables, run: + ``symfony var:export --debug``. + +.. tip:: + + For some services, the local web server also exposes environment variables + understood by CLI tools related to the service. For instance, running + ``symfony run psql`` will connect you automatically to the PostgreSQL server + running in a container without having to specify the username, password, or + database name. + +When Docker services are running, browse a page of your Symfony application and +check the "Symfony Server" section in the web debug toolbar. You'll see that +"Docker Compose" is marked as "Up". + +.. note:: + + If you don't want environment variables to be exposed for a service, set + the ``com.symfony.server.service-ignore`` label to ``true``: + + .. code-block:: yaml + + # compose.yaml + services: + db: + ports: [3306] + labels: + com.symfony.server.service-ignore: true + +If your Docker Compose file is not at the root of the project, use the +``COMPOSE_FILE`` and ``COMPOSE_PROJECT_NAME`` environment variables to define +its location, same as for ``docker-compose``: + +.. code-block:: bash + + # start your containers: + COMPOSE_FILE=docker/compose.yaml COMPOSE_PROJECT_NAME=project_name docker-compose up -d + + # run any Symfony CLI command: + COMPOSE_FILE=docker/compose.yaml COMPOSE_PROJECT_NAME=project_name symfony var:export + +.. note:: + + If you have more than one Docker Compose file, you can provide them all, + separated by ``:``, as explained in the `Docker Compose CLI env var reference`_. + +.. warning:: + + When using the Symfony CLI with ``php bin/console`` (``symfony console ...``), + it will **always** use environment variables detected via Docker, ignoring + any local environment variables. For example, if you set up a different database + name in your ``.env.test`` file (``DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/test``) + and run ``symfony console doctrine:database:drop --force --env=test``, + the command will drop the database defined in your Docker configuration and not the "test" one. + +.. warning:: + + Similar to other web servers, this tool automatically exposes all environment + variables available in the CLI context. Ensure that this local server is not + accessible on your local network without your explicit consent, to avoid + potential security issues. + +Service Naming +~~~~~~~~~~~~~~ + +If your service names don't match Symfony conventions, use labels: + +.. code-block:: yaml + + services: + db: + image: postgres:15 + ports: [5432] + labels: + com.symfony.server.service-prefix: 'DATABASE' + +In this example, the service is named ``db``, so environment variables would be +prefixed with ``DB_``, but as the ``com.symfony.server.service-prefix`` is set +to ``DATABASE``, the web server creates environment variables starting with +``DATABASE_`` instead as expected by the default Symfony configuration. + +Managing Long-Running Processes +------------------------------- + +Use the ``run`` command provided by the Symfony CLI to manage long-running +processes like Webpack watchers: + +.. code-block:: terminal + + # start webpack watcher in the background to not block the terminal + $ symfony run -d npx encore dev --watch + + # continue working and running other commands... + + # view logs + $ symfony server:log + + # check status + $ symfony server:status + +.. _symfony-server_configuring-workers: + +Configuring Workers +~~~~~~~~~~~~~~~~~~~ + +Define processes that should start automatically with the server in +``.symfony.local.yaml``: + +.. code-block:: yaml + + # .symfony.local.yaml + workers: + # Built-in Encore integration + npm_encore_watch: ~ + + # Messenger consumer with file watching + messenger_consume_async: + cmd: ['symfony', 'console', 'messenger:consume', 'async'] + watch: ['config', 'src', 'templates', 'vendor'] + + # Custom commands + build_spa: + cmd: ['npm', 'run', 'watch'] + + # Auto-start Docker Compose + docker_compose: ~ + +Advanced Configuration +---------------------- + +The ``.symfony.local.yaml`` file provides advanced configuration options: + +.. code-block:: yaml + + # sets app.wip and admin.app.wip for the current project + proxy: + domains: + - app + - admin.app + + # HTTP server settings + http: + document_root: public/ + passthru: index.php + # forces the port that will be used to run the server + port: 8000 + # sets the HTTP port you prefer for this project [default: 8000] + # (only will be used if it's available; otherwise a random port is chosen) + preferred_port: 8001 + # used to disable the default auto-redirection from HTTP to HTTPS + allow_http: true + # force the use of HTTP instead of HTTPS + no_tls: false + # path to the file containing the TLS certificate to use in p12 format + p12: path/to/custom-cert.p12 + # toggle GZIP compression + use_gzip: true + # run the server in the background + daemon: true + +.. warning:: + + Setting domains in this configuration file will override any domains you set + using the ``proxy:domain:attach`` command for the current project when you start + the server. + +.. _platform-sh-integration: + +Symfony Cloud Integration +------------------------- + +The Symfony CLI provides seamless integration with `Symfony Cloud`_ (powered by +`Platform.sh`_): + +.. code-block:: terminal + + # open Platform.sh web UI + $ symfony cloud:web + + # deploy your project to production + $ symfony cloud:deploy + + # create a new environment + $ symfony cloud:env:create feature-xyz + +For more features, see the `Symfony Cloud documentation`_. + +Troubleshooting +--------------- + +**Server doesn't start**: Check if the port is already in use: + +.. code-block:: terminal + + $ symfony server:status + $ symfony server:stop # If a server is already running + +**HTTPS not working**: Ensure the CA is installed: + +.. code-block:: terminal + + $ symfony server:ca:install + +**Docker services not detected**: Check that Docker is running and environment +variables are properly exposed: + +.. code-block:: terminal + + $ docker compose ps + $ symfony var:export --debug + +**Proxy domains not working**: + +* Clear your browser cache +* Check proxy settings in your system +* For Chrome, visit ``chrome://net-internals/#proxy`` and click "Re-apply settings" + +.. _`open source`: https://github.com/symfony-cli/symfony-cli +.. _`symfony.com/download`: https://symfony.com/download +.. _`Docker`: https://en.wikipedia.org/wiki/Docker_(software) +.. _`Symfony Cloud`: https://symfony.com/cloud/ +.. _`Platform.sh`: https://platform.sh/ +.. _`Symfony Cloud documentation`: https://docs.platform.sh/frameworks/symfony.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://superuser.com/a/1799209 +.. _`Docker Compose CLI env var reference`: https://docs.docker.com/compose/reference/envvars/ diff --git a/setup/symfony_server.rst b/setup/symfony_server.rst deleted file mode 100644 index 2ea4da543fe..00000000000 --- a/setup/symfony_server.rst +++ /dev/null @@ -1,563 +0,0 @@ -Symfony Local Web Server -======================== - -You can run Symfony applications with any web server (Apache, nginx, the -internal PHP web server, etc.). However, Symfony provides its own web server to -make you more productive while developing your applications. - -Although this server is not intended for production use, it supports HTTP/2, -TLS/SSL, automatic generation of security certificates, local domains, and many -other features that sooner or later you'll need when developing web projects. -Moreover, the server is not tied to Symfony and you can also use it with any -PHP application and even with HTML or single page applications. - -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 - `symfony-cli/symfony-cli GitHub repository`_. - -Getting Started ---------------- - -The Symfony server is started once per project, so you may end up with several -instances (each of them listening to a different port). This is the common -workflow to serve a Symfony project: - -.. code-block:: terminal - - $ cd my-project/ - $ symfony server:start - - [OK] Web server listening on http://127.0.0.1:.... - ... - - # Now, browse the given URL, or run this command: - $ symfony open:local - -Running the server this way makes it display the log messages in the console, so -you won't be able to run other commands at the same time. If you prefer, you can -run the Symfony server in the background: - -.. code-block:: terminal - - $ cd my-project/ - - # start the server in the background - $ symfony server:start -d - - # continue working and running other commands... - - # show the latest log messages - $ symfony server:log - -.. tip:: - - 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 applications that are not listed in the - firewall list. The solution is to run this command that signs the Symfony binary: - - .. code-block:: terminal - - $ sudo codesign --force --deep --sign - $(whereis -q symfony) - -Enabling PHP-FPM ----------------- - -.. note:: - - PHP-FPM must be installed locally for the Symfony server to utilize. - -When the server starts, it checks for ``web/index_dev.php``, ``web/index.php``, -``public/app_dev.php``, ``public/app.php`` in that order. If one is found, the -server will automatically start with PHP-FPM enabled. Otherwise the server will -start without PHP-FPM and will show a ``Page not found`` page when trying to -access a ``.php`` file in the browser. - -.. tip:: - - When an ``index.html`` and a front controller like e.g. ``index.php`` are - both present the server will still start with PHP-FPM enabled but the - ``index.html`` will take precedence over the front controller. This means - when an ``index.html`` file is present in ``public`` or ``web``, it will be - displayed instead of the ``index.php`` which would show e.g. the Symfony - application. - -Enabling TLS ------------- - -Browsing the secure version of your applications locally is important to detect -problems with mixed content early, and to run libraries that only run in HTTPS. -Traditionally this has been painful and complicated to set up, but the Symfony -server automates everything. First, run this command: - -.. code-block:: terminal - - $ symfony server:ca:install - -This command creates a local certificate authority, registers it in your system -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. - -Different PHP Settings Per Project ----------------------------------- - -Selecting a Different PHP Version -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you have multiple PHP versions installed on your computer, you can tell -Symfony which one to use creating a file called ``.php-version`` at the project -root directory: - -.. code-block:: terminal - - $ cd my-project/ - - # use a specific PHP version - $ echo 7.4 > .php-version - - # use any PHP 8.x version available - $ echo 8 > .php-version - -.. tip:: - - The Symfony server traverses the directory structure up to the root - directory, so you can create a ``.php-version`` file in some parent - directory to set the same PHP version for a group of projects under that - directory. - -Run the command below if you don't remember all the PHP versions installed on your -computer: - -.. code-block:: terminal - - $ symfony local:php:list - - # You'll see all supported SAPIs (CGI, FastCGI, etc.) for each version. - # FastCGI (php-fpm) is used when possible; then CGI (which acts as a FastCGI - # server as well), and finally, the server falls back to plain CGI. - -Overriding PHP Config Options Per Project -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can change the value of any PHP runtime config option per project by creating a -file called ``php.ini`` at the project root directory. Add only the options you want -to override: - -.. code-block:: terminal - - $ cd my-project/ - - # this project only overrides the default PHP timezone - $ cat php.ini - [Date] - date.timezone = Asia/Tokyo - -Running Commands with Different PHP Versions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When running different PHP versions, it is useful to use the main ``symfony`` -command as a wrapper for the ``php`` command. This allows you to always select -the most appropriate PHP version according to the project which is running the -commands. It also loads the env vars automatically, which is important when -running non-Symfony commands: - -.. code-block:: terminal - - # runs the command with the default PHP version - $ php -r "..." - - # runs the command with the PHP version selected by the project - # (or the default PHP version if the project didn't select one) - $ symfony php -r "..." - -Local Domain Names ------------------- - -By default, projects are accessible at some random port of the ``127.0.0.1`` -local IP. However, sometimes it is preferable to associate a domain name to them: - -* It's more convenient when you work continuously on the same project because - port numbers can change but domains don't; -* The behavior of some applications depend on their domains/subdomains; -* To have stable endpoints, such as the local redirection URL for OAuth2. - -Setting up the Local Proxy -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Local domains are possible thanks to a local proxy provided by the Symfony server. -If this is the first time you run the proxy, you must configure it as follows: - -#. Open the **proxy settings** of your operating system: - - * `Proxy settings in Windows`_; - * `Proxy settings in macOS`_; - * `Proxy settings in Ubuntu`_. - -#. Set the following URL as the value of the **Automatic Proxy Configuration**: - - ``http://127.0.0.1:7080/proxy.pac`` - -Now run this command to start the proxy: - -.. code-block:: terminal - - $ symfony proxy:start - -If the proxy doesn't work as explained in the following sections, check these: - -* Some browsers (e.g. Chrome) require to re-apply proxy settings (clicking on - ``Re-apply settings`` button on the ``chrome://net-internals/#proxy`` page) - or a full restart after starting the proxy. Otherwise, you'll see a - *"This webpage is not available"* error (``ERR_NAME_NOT_RESOLVED``); -* 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 -~~~~~~~~~~~~~~~~~~~~~~~~~ - -By default, Symfony proposes ``.wip`` (for *Work in Progress*) for the local -domains. You can define a local domain for your project as follows: - -.. code-block:: terminal - - $ cd my-project/ - $ symfony proxy:domain:attach my-domain - -If you have installed the local proxy as explained in the previous section, you -can now browse ``https://my-domain.wip`` to access your local project with the -new custom domain. - -.. tip:: - - Browse the http://127.0.0.1:7080 URL to get the full list of local project - directories, their custom domains, and port numbers. - -You can also add a wildcard domain: - -.. code-block:: terminal - - $ symfony proxy:domain:attach "*.my-domain" - -So it will match all subdomains like ``https://admin.my-domain.wip``, ``https://other.my-domain.wip``... - -When running console commands, add the ``https_proxy`` env var to make custom -domains work: - -.. code-block:: terminal - - # Example with curl - $ https_proxy=$(symfony proxy:url) curl https://my-domain.wip - - # Example with Blackfire and curl - $ https_proxy=$(symfony proxy:url) blackfire curl https://my-domain.wip - - # Example with Cypress - $ https_proxy=$(symfony proxy:url) ./node_modules/bin/cypress open - -.. warning:: - - 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 - spelled in lowercase. - -.. tip:: - - If you prefer to use a different TLD, edit the ``~/.symfony5/proxy.json`` - file (where ``~`` means the path to your user directory) and change the - value of the ``tld`` option from ``wip`` to any other TLD. - -Long-Running Commands ---------------------- - -Long-running commands, such as the ones that compile front-end web assets, block -the terminal and you can't run other commands at the same time. The Symfony -server provides a ``run`` command to wrap them as follows: - -.. code-block:: terminal - - # compile Webpack assets using Symfony Encore ... but do that in the - # background to not block the terminal - $ symfony run -d npx encore dev --watch - - # continue working and running other commands... - - # from time to time, check the command logs if you want - $ symfony server:log - - # and you can also check if the command is still running - $ symfony server:status - Web server listening on ... - Command "npx ..." running with PID ... - - # stop the web server (and all the associated commands) when you are finished - $ symfony server:stop - -Configuration file ------------------- - -There are several options that you can set using a ``.symfony.local.yaml`` config file: - -.. code-block:: yaml - - # Sets domain1.wip and domain2.wip for the current project - proxy: - domains: - - domain1 - - domain2 - - http: - document_root: public/ # Path to the project document root - passthru: index.php # Project passthru index - port: 8000 # Force the port that will be used to run the server - preferred_port: 8001 # Preferred HTTP port [default: 8000] - p12: path/to/p12_cert # Name of the file containing the TLS certificate to use in p12 format - allow_http: true # Prevent auto-redirection from HTTP to HTTPS - 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 - -.. warning:: - - Setting domains in this configuration file will override any domains you set - using the ``proxy:domain:attach`` command for the current project when you start - the server. - -.. _symfony-server_configuring-workers: - -Configuring Workers -~~~~~~~~~~~~~~~~~~~ - -If you like some processes to start automatically, along with the webserver -(``symfony server:start``), you can set them in the YAML configuration file: - -.. code-block:: yaml - - # .symfony.local.yaml - workers: - # built-in command that builds and watches front-end assets - # npm_encore_watch: - # cmd: ['npx', 'encore', 'dev', '--watch'] - npm_encore_watch: ~ - - # built-in command that starts messenger consumer - # messenger_consume_async: - # cmd: ['symfony', 'console', 'messenger:consume', 'async'] - # watch: ['config', 'src', 'templates', 'vendor'] - messenger_consume_async: ~ - - # you can also add your own custom commands - build_spa: - 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: - -Docker Integration ------------------- - -The local Symfony server provides full `Docker`_ integration for projects that -use it. To learn more about Docker & Symfony, see :doc:`docker`. - -When the web server detects that Docker Compose is running for the project, it -automatically exposes some environment variables. - -Via the ``docker-compose`` API, it looks for exposed ports used for common -services. When it detects one it knows about, it uses the service name to -expose environment variables. - -Consider the following configuration: - -.. code-block:: yaml - - # compose.yaml - services: - database: - ports: [3306] - -The web server detects that a service exposing port ``3306`` is running for the -project. It understands that this is a MySQL service and creates environment -variables accordingly with the service name (``database``) as a prefix: -``DATABASE_URL``, ``DATABASE_HOST``, ... - -If the service is not in the supported list below, generic environment -variables are set: ``PORT``, ``IP``, and ``HOST``. - -If the ``compose.yaml`` names do not match Symfony's conventions, add a -label to override the environment variables prefix: - -.. code-block:: yaml - - # compose.yaml - services: - db: - ports: [3306] - labels: - com.symfony.server.service-prefix: 'DATABASE' - -In this example, the service is named ``db``, so environment variables would be -prefixed with ``DB_``, but as the ``com.symfony.server.service-prefix`` is set -to ``DATABASE``, the web server creates environment variables starting with -``DATABASE_`` instead as expected by the default Symfony configuration. - -Here is the list of supported services with their ports and default Symfony -prefixes: - -============= ========= ====================== -Service Port Symfony default prefix -============= ========= ====================== -MySQL 3306 ``DATABASE_`` -PostgreSQL 5432 ``DATABASE_`` -Redis 6379 ``REDIS_`` -Memcached 11211 ``MEMCACHED_`` -RabbitMQ 5672 ``RABBITMQ_`` (set user and pass via Docker ``RABBITMQ_DEFAULT_USER`` and ``RABBITMQ_DEFAULT_PASS`` env var) -Elasticsearch 9200 ``ELASTICSEARCH_`` -MongoDB 27017 ``MONGODB_`` (set the database via a Docker ``MONGO_DATABASE`` env var) -Kafka 9092 ``KAFKA_`` -MailCatcher 1025/1080 ``MAILER_`` - or 25/80 -Blackfire 8707 ``BLACKFIRE_`` -Mercure 80 Always exposes ``MERCURE_PUBLIC_URL`` and ``MERCURE_URL`` (only works with the ``dunglas/mercure`` Docker image) -============= ========= ====================== - -You can open web management interfaces for the services that expose them: - -.. code-block:: bash - - $ symfony open:local:webmail - $ symfony open:local:rabbitmq - -Or click on the links in the "Server" section of the web debug toolbar. - -.. tip:: - - To debug and list all exported environment variables, run ``symfony - var:export --debug``. - -.. tip:: - - For some services, the web server also exposes environment variables - understood by CLI tools related to the service. For instance, running - ``symfony run psql`` will connect you automatically to the PostgreSQL server - running in a container without having to specify the username, password, or - database name. - -When Docker services are running, browse a page of your Symfony application and -check the "Symfony Server" section in the web debug toolbar; you'll see that -"Docker Compose" is "Up". - -.. note:: - - If you don't want environment variables to be exposed for a service, set - the ``com.symfony.server.service-ignore`` label to ``true``: - - .. code-block:: yaml - - # compose.yaml - services: - db: - ports: [3306] - labels: - com.symfony.server.service-ignore: true - -If your Docker Compose file is not at the root of the project, use the -``COMPOSE_FILE`` and ``COMPOSE_PROJECT_NAME`` environment variables to define -its location, same as for ``docker-compose``: - -.. code-block:: bash - - # start your containers: - COMPOSE_FILE=docker/compose.yaml COMPOSE_PROJECT_NAME=project_name docker-compose up -d - - # run any Symfony CLI command: - COMPOSE_FILE=docker/compose.yaml COMPOSE_PROJECT_NAME=project_name symfony var:export - -.. note:: - - If you have more than one Docker Compose file, you can provide them all - separated by ``:`` as explained in the `Docker compose CLI env var reference`_. - -.. warning:: - - When using the Symfony binary with ``php bin/console`` (``symfony console ...``), - the binary will **always** use environment variables detected via Docker and will - ignore local environment variables. - For example if you set up a different database name in your ``.env.test`` file - (``DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/test``) and if you run - ``symfony console doctrine:database:drop --force --env=test``, the command will drop the database - defined in your Docker configuration and not the "test" one. - -.. warning:: - - Similar to other web servers, this tool automatically exposes all environment - variables available in the CLI context. Ensure that this local server is not - accessible on your local network without consent to avoid security issues. - -Platform.sh Integration ------------------------ - -The local Symfony server provides full, but optional, integration with -`Platform.sh`_, a service optimized to run your Symfony applications on the -cloud. It provides features such as creating environments, backups/snapshots, -and even access to a copy of the production data from your local machine to -help debug any issues. - -`Read Platform.sh for Symfony technical docs`_. - -.. _`install Symfony`: https://symfony.com/download -.. _`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/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://superuser.com/a/1799209 -.. _`Docker compose CLI env var reference`: https://docs.docker.com/compose/reference/envvars/ diff --git a/setup/web_server_configuration.rst b/setup/web_server_configuration.rst index 58935bf5352..4b562d4f79e 100644 --- a/setup/web_server_configuration.rst +++ b/setup/web_server_configuration.rst @@ -2,7 +2,7 @@ Configuring a Web Server ======================== The preferred way to develop your Symfony application is to use -:doc:`Symfony Local Web Server `. +:ref:`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 how to use Symfony @@ -178,24 +178,35 @@ directive to pass requests for PHP files to PHP FPM: # 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 .. 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. + If you're running some quick tests with Apache, you can run + ``composer require symfony/apache-pack`` to create an ``.htaccess`` file in + the ``public/`` directory with the rewrite rules needed to serve the Symfony + application. Make sure Apache's ``AllowOverride`` setting is set to ``All`` + for that directory; otherwise, the ``.htaccess`` file will be ignored. + + In production, however, it's recommended to move these rules to the main + Apache configuration file (as shown above) to improve performance. Caddy ----- When using Caddy on the server, you can use a configuration like this: -.. code-block:: text +.. code-block:: nginx # /etc/caddy/Caddyfile example.com, www.example.com { diff --git a/string.rst b/string.rst index 667dcd06010..e51e7d1b502 100644 --- a/string.rst +++ b/string.rst @@ -232,14 +232,26 @@ Methods to Change Case u('Foo: Bar-baz.')->camel(); // 'fooBarBaz' // changes all graphemes/code points to snake_case u('Foo: Bar-baz.')->snake(); // 'foo_bar_baz' - // other cases can be achieved by chaining methods. E.g. PascalCase: - u('Foo: Bar-baz.')->camel()->title(); // 'FooBarBaz' + // changes all graphemes/code points to kebab-case + u('Foo: Bar-baz.')->kebab(); // 'foo-bar-baz' + // changes all graphemes/code points to PascalCase + u('Foo: Bar-baz.')->pascal(); // 'FooBarBaz' + // other cases can be achieved by chaining methods, e.g. : + u('Foo: Bar-baz.')->camel()->upper(); // 'FOOBARBAZ' .. versionadded:: 7.1 The ``localeLower()``, ``localeUpper()`` and ``localeTitle()`` methods were introduced in Symfony 7.1. +.. versionadded:: 7.2 + + The ``kebab()`` method was introduced in Symfony 7.2. + +.. versionadded:: 7.3 + + The ``pascal()`` method was introduced in Symfony 7.3. + The methods of all string classes are case-sensitive by default. You can perform case-insensitive operations with the ``ignoreCase()`` method:: @@ -395,10 +407,19 @@ Methods to Join, Split, Truncate and Reverse u('Lorem Ipsum')->truncate(80); // 'Lorem Ipsum' // the second argument is the character(s) added when a string is cut // (the total length includes the length of this character(s)) - u('Lorem Ipsum')->truncate(8, '…'); // 'Lorem I…' - // if the third argument is false, the last word before the cut is kept - // even if that generates a string longer than the desired length - u('Lorem Ipsum')->truncate(8, '…', cut: false); // 'Lorem Ipsum' + // (note that '…' is a single character that includes three dots; it's not '...') + u('Lorem Ipsum')->truncate(8, '…'); // 'Lorem I…' + // the third optional argument defines how to cut words when the length is exceeded + // the default value is TruncateMode::Char which cuts the string at the exact given length + u('Lorem ipsum dolor sit amet')->truncate(8, cut: TruncateMode::Char); // 'Lorem ip' + // returns up to the last complete word that fits in the given length without surpassing it + u('Lorem ipsum dolor sit amet')->truncate(8, cut: TruncateMode::WordBefore); // 'Lorem' + // returns up to the last complete word that fits in the given length, surpassing it if needed + u('Lorem ipsum dolor sit amet')->truncate(8, cut: TruncateMode::WordAfter); // 'Lorem ipsum' + +.. versionadded:: 7.2 + + The ``TruncateMode`` parameter for truncate function was introduced in Symfony 7.2. :: @@ -645,11 +666,28 @@ class to convert English words from/to singular/plural with confidence:: The value returned by both methods is always an array because sometimes it's not possible to determine a unique singular/plural form for the given word. +Symfony also provides inflectors for other languages:: + + use Symfony\Component\String\Inflector\FrenchInflector; + + $inflector = new FrenchInflector(); + $result = $inflector->singularize('souris'); // ['souris'] + $result = $inflector->pluralize('hôpital'); // ['hôpitaux'] + + use Symfony\Component\String\Inflector\SpanishInflector; + + $inflector = new SpanishInflector(); + $result = $inflector->singularize('aviones'); // ['avión'] + $result = $inflector->pluralize('miércoles'); // ['miércoles'] + +.. versionadded:: 7.2 + + The ``SpanishInflector`` class was introduced in Symfony 7.2. + .. note:: - Symfony also provides a :class:`Symfony\\Component\\String\\Inflector\\FrenchInflector` - and an :class:`Symfony\\Component\\String\\Inflector\\InflectorInterface` if - you need to implement your own inflector. + Symfony provides an :class:`Symfony\\Component\\String\\Inflector\\InflectorInterface` + in case you need to implement your own inflector. .. _`ASCII`: https://en.wikipedia.org/wiki/ASCII .. _`Unicode`: https://en.wikipedia.org/wiki/Unicode diff --git a/templates.rst b/templates.rst index 5815dbb56c4..fc353384202 100644 --- a/templates.rst +++ b/templates.rst @@ -304,35 +304,33 @@ You can now use the ``asset()`` function: .. code-block:: html+twig {# the image lives at "public/images/logo.png" #} - Symfony! + Symfony! {# the CSS file lives at "public/css/blog.css" #} - + {# the JS file lives at "public/bundles/acme/js/loader.js" #} -The ``asset()`` function's main purpose is to make your application more portable. -If your application lives at the root of your host (e.g. ``https://example.com``), -then the rendered path should be ``/images/logo.png``. But if your application -lives in a subdirectory (e.g. ``https://example.com/my_app``), each asset path -should render with the subdirectory (e.g. ``/my_app/images/logo.png``). The -``asset()`` function takes care of this by determining how your application is -being used and generating the correct paths accordingly. +Using the ``asset()`` function is recommended for these reasons: -.. tip:: +* **Asset versioning**: ``asset()`` appends a version hash to asset URLs for + cache busting. This works both via :doc:`AssetMapper ` and the + :doc:`Asset component ` (see also the + :ref:`assets configuration options `, such as ``version`` + and ``version_format``). - The ``asset()`` function supports various cache busting techniques via the - :ref:`version `, - :ref:`version_format `, and - :ref:`json_manifest_path ` configuration options. +* **Application portability**: whether your app is hosted at the root + (e.g. ``https://example.com``) or in a subdirectory (e.g. ``https://example.com/my_app``), + ``asset()`` generates the correct path (e.g. ``/images/logo.png`` vs ``/my_app/images/logo.png``) + automatically based on your app's base URL. If you need absolute URLs for assets, use the ``absolute_url()`` Twig function as follows: .. code-block:: html+twig - Symfony! + Symfony! @@ -497,8 +495,8 @@ in container parameters `: .. code-block:: php // config/packages/twig.php - use function Symfony\Component\DependencyInjection\Loader\Configurator\service; use Symfony\Config\TwigConfig; + use function Symfony\Component\DependencyInjection\Loader\Configurator\service; return static function (TwigConfig $twig): void { // ... @@ -642,6 +640,31 @@ This might come handy when dealing with blocks in :ref:`templates inheritance ` or when using `Turbo Streams`_. +Similarly, you can use the ``#[Template]`` attribute on the controller to specify +a block to render:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use Symfony\Bridge\Twig\Attribute\Template; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + + class ProductController extends AbstractController + { + #[Template('product.html.twig', block: 'price_block')] + public function price(): array + { + return [ + // ... + ]; + } + } + +.. versionadded:: 7.2 + + The ``#[Template]`` attribute's ``block`` argument was introduced in Symfony 7.2. + Rendering a Template in Services ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -715,6 +738,11 @@ provided by Symfony: site_name: 'ACME' theme: 'dark' + # optionally you can define HTTP headers to add to the response + headers: + Content-Type: 'text/html' + foo: 'bar' + .. code-block:: xml @@ -744,6 +772,11 @@ provided by Symfony: ACME dark + + + + text/html + @@ -774,11 +807,20 @@ provided by Symfony: 'context' => [ 'site_name' => 'ACME', 'theme' => 'dark', + ], + + // optionally you can define HTTP headers to add to the response + 'headers' => [ + 'Content-Type' => 'text/html', ] ]) ; }; +.. versionadded:: 7.2 + + The ``headers`` option was introduced in Symfony 7.2. + Checking if a Template Exists ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -835,6 +877,11 @@ errors. It's useful to run it before deploying your application to production The option to exclude directories was introduced in Symfony 7.1. +.. versionadded:: 7.3 + + Before Symfony 7.3, the ``--show-deprecations`` option only displayed the + first deprecation found, so you had to run the command repeatedly. + When running the linter inside `GitHub Actions`_, the output is automatically adapted to the format required by GitHub, but you can force that format too: @@ -924,7 +971,7 @@ following code to display the user information is repeated in several places: {# ... #} @@ -1209,7 +1256,7 @@ In practice, the ``base.html.twig`` template would look like this: {% block title %}My Application{% endblock %} {% block stylesheets %} - + {% endblock %} @@ -1464,8 +1511,8 @@ Bundle Templates ~~~~~~~~~~~~~~~~ If you :ref:`install packages/bundles ` in your application, they -may include their own Twig templates (in the ``Resources/views/`` directory of -each bundle). To avoid messing with your own templates, Symfony adds bundle +may include their own Twig templates (in the ``templates/`` directory of each +bundle). To avoid messing with your own templates, Symfony adds bundle templates under an automatic namespace created after the bundle name. For example, the templates of a bundle called ``AcmeBlogBundle`` are available @@ -1504,23 +1551,20 @@ as currency: {# pass in the 3 optional arguments #} {{ product.price|price(2, ',', '.') }} -Create a class that extends ``AbstractExtension`` and fill in the logic:: +.. _templates-twig-filter-attribute: + +Create a regular PHP class with a method that contains the filter logic. Then, +add the ``#[AsTwigFilter]`` attribute to define the name and options of +the Twig filter:: // src/Twig/AppExtension.php namespace App\Twig; - use Twig\Extension\AbstractExtension; - use Twig\TwigFilter; + use Twig\Attribute\AsTwigFilter; - class AppExtension extends AbstractExtension + class AppExtension { - public function getFilters(): array - { - return [ - new TwigFilter('price', [$this, 'formatPrice']), - ]; - } - + #[AsTwigFilter('price')] public function formatPrice(float $number, int $decimals = 0, string $decPoint = '.', string $thousandsSep = ','): string { $price = number_format($number, $decimals, $decPoint, $thousandsSep); @@ -1530,24 +1574,19 @@ Create a class that extends ``AbstractExtension`` and fill in the logic:: } } -If you want to create a function instead of a filter, define the -``getFunctions()`` method:: +.. _templates-twig-function-attribute: + +If you want to create a function instead of a filter, use the +``#[AsTwigFunction]`` attribute:: // src/Twig/AppExtension.php namespace App\Twig; - use Twig\Extension\AbstractExtension; - use Twig\TwigFunction; + use Twig\Attribute\AsTwigFunction; - class AppExtension extends AbstractExtension + class AppExtension { - public function getFunctions(): array - { - return [ - new TwigFunction('area', [$this, 'calculateArea']), - ]; - } - + #[AsTwigFunction('area')] public function calculateArea(int $width, int $length): int { return $width * $length; @@ -1559,6 +1598,18 @@ If you want to create a function instead of a filter, define the Along with custom filters and functions, you can also register `global variables`_. +.. versionadded:: 7.3 + + Support for the ``#[AsTwigFilter]``, ``#[AsTwigFunction]`` and ``#[AsTwigTest]`` + attributes was introduced in Symfony 7.3. Previously, you had to extend the + ``AbstractExtension`` class, and override the ``getFilters()`` and ``getFunctions()`` + methods. + +If you're using the :ref:`default services.yaml configuration `, +the :ref:`service autoconfiguration ` feature will enable +this class as a Twig extension. Otherwise, you need to define a service manually +and :doc:`tag it ` with the ``twig.attribute_extension`` tag. + Register an Extension as a Service .................................. @@ -1582,10 +1633,11 @@ this command to confirm that your new filter was successfully registered: Creating Lazy-Loaded Twig Extensions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Including the code of the custom filters/functions in the Twig extension class -is the simplest way to create extensions. However, Twig must initialize all -extensions before rendering any template, even if the template doesn't use an -extension. +When :ref:`using attributes to extend Twig `, +the **Twig extensions are already lazy-loaded** and you don't have to do anything +else. However, if your Twig extensions follow the **legacy approach** of extending +the ``AbstractExtension`` class, Twig initializes all the extensions before +rendering any template, even if they are not used. If extensions don't define dependencies (i.e. if you don't inject services in them) performance is not affected. However, if extensions define lots of complex diff --git a/testing.rst b/testing.rst index 30c0e87ab77..09cddfa55bb 100644 --- a/testing.rst +++ b/testing.rst @@ -5,14 +5,38 @@ Whenever you write a new line of code, you also potentially add new bugs. To build better and more reliable applications, you should test your code using both functional and unit tests. -.. _testing-installation: +Symfony integrates with an independent library called `PHPUnit`_ to give you a +rich testing framework. This article covers the PHPUnit basics you'll need to +write Symfony tests. To learn everything about PHPUnit and its features, read +the `official PHPUnit documentation`_. + +Types of Tests +-------------- + +There are many types of automated tests and precise definitions often +differ from project to project. In Symfony, the following definitions are +used. If you have learned something different, that is not necessarily +wrong, just different from what the Symfony documentation is using. + +`Unit Tests`_ + These tests ensure that *individual* units of source code (e.g. a single + class) behave as intended. + +`Integration Tests`_ + These tests test a combination of classes and commonly interact with + Symfony's service container. These tests do not yet cover the fully + working application, those are called *Application tests*. -The PHPUnit Testing Framework ------------------------------ +`Application Tests`_ + Application tests (also known as functional tests) test the behavior of a + complete application. They make HTTP requests (both real and simulated ones) + and test that the response is as expected. + +.. _testing-installation: +.. _the-phpunit-testing-framework: -Symfony integrates with an independent library called `PHPUnit`_ to give -you a rich testing framework. This article won't cover PHPUnit itself, -which has its own excellent `documentation`_. +Installation +------------ Before creating your first test, install ``symfony/test-pack``, which installs some other packages needed for testing (such as ``phpunit/phpunit``): @@ -44,28 +68,6 @@ your test into multiple "test suites"). missing, you can try running the recipe again using ``composer recipes:install phpunit/phpunit --force -v``. -Types of Tests --------------- - -There are many types of automated tests and precise definitions often -differ from project to project. In Symfony, the following definitions are -used. If you have learned something different, that is not necessarily -wrong, just different from what the Symfony documentation is using. - -`Unit Tests`_ - These tests ensure that *individual* units of source code (e.g. a single - class) behave as intended. - -`Integration Tests`_ - These tests test a combination of classes and commonly interact with - Symfony's service container. These tests do not yet cover the fully - working application, those are called *Application tests*. - -`Application Tests`_ - Application tests test the behavior of a complete application. They - make HTTP requests (both real and simulated ones) and test that the - response is as expected. - Unit Tests ---------- @@ -712,6 +714,29 @@ stores in the session of the test client. If you need to define custom attributes in this token, you can use the ``tokenAttributes`` argument of the :method:`Symfony\\Bundle\\FrameworkBundle\\KernelBrowser::loginUser` method. +You can also use an :ref:`in-memory user ` in your tests +by instantiating :class:`Symfony\\Component\\Security\\Core\\User\\InMemoryUser` directly:: + + // tests/Controller/ProfileControllerTest.php + use Symfony\Component\Security\Core\User\InMemoryUser; + + $client = static::createClient(); + $testUser = new InMemoryUser('admin', 'password', ['ROLE_ADMIN']); + $client->loginUser($testUser); + +Before doing this, you must define the in-memory user in your test environment +configuration to ensure it exists and can be authenticated:: + +.. code-block:: yaml + + # config/packages/security.yaml + when@test: + security: + users_in_memory: + memory: + users: + admin: { password: password, roles: ROLE_ADMIN } + To set a specific firewall (``main`` is set by default):: $client->loginUser($testUser, 'my_firewall'); @@ -862,8 +887,8 @@ Use the ``submitForm()`` method to submit the form that contains the given butto 'comment_form[content]' => '...', ]); -The first argument of ``submitForm()`` is the text content, ``id``, ``value`` or -``name`` of any ``