diff --git a/.doctor-rst.yaml b/.doctor-rst.yaml index c7a17edd06c..2ecd4fc7d2d 100644 --- a/.doctor-rst.yaml +++ b/.doctor-rst.yaml @@ -1,9 +1,5 @@ rules: american_english: ~ - argument_variable_must_match_type: - arguments: - - { type: 'ContainerBuilder', name: 'container' } - - { type: 'ContainerConfigurator', name: 'container' } avoid_repetetive_words: ~ blank_line_after_anchor: ~ blank_line_after_directive: ~ @@ -23,10 +19,12 @@ rules: ensure_order_of_code_blocks_in_configuration_block: ~ ensure_php_reference_syntax: ~ extend_abstract_controller: ~ - extension_xlf_instead_of_xliff: ~ + # extension_xlf_instead_of_xliff: ~ forbidden_directives: directives: - '.. index::' + - directive: '.. caution::' + replacements: ['.. warning::', '.. danger::'] indention: ~ lowercase_as_in_use_statements: ~ max_blank_lines: @@ -43,6 +41,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: ~ @@ -75,35 +74,40 @@ rules: # master versionadded_directive_major_version: - major_version: 5 + major_version: 7 versionadded_directive_min_version: - min_version: '5.0' + min_version: '7.0' deprecated_directive_major_version: - major_version: 5 + major_version: 7 deprecated_directive_min_version: - min_version: '5.0' + min_version: '7.0' exclude_rule_for_file: - path: configuration/multiple_kernels.rst rule_name: replacement + - path: page_creation.rst + rule_name: no_php_open_tag_in_code_block_php_directive + - path: frontend/create_ux_bundle.rst + rule_name: argument_variable_must_match_type # do not report as violation whitelist: regex: - - '/FOSUserBundle(.*)\.yml/' + - '/``.yml``/' - '/(.*)\.orm\.yml/' # currently DoctrineBundle only supports .yml lines: - 'in config files, so the old ``app/config/config_dev.yml`` goes to' - '#. The most important config file is ``app/config/services.yml``, which now is' - 'The bin/console Command' - '.. _`LDAP injection`: http://projects.webappsec.org/w/page/13246947/LDAP%20Injection' + - '.. versionadded:: 2.8.0' # Doctrine - '.. versionadded:: 1.9.0' # Encore - '.. versionadded:: 1.18' # Flex in setup/upgrade_minor.rst - '.. versionadded:: 1.0.0' # Encore - - '.. versionadded:: 5.1' # Private Services + - '.. versionadded:: 2.7.1' # Doctrine - '123,' # assertion for var_dumper - components/var_dumper.rst - '"foo",' # assertion for var_dumper - components/var_dumper.rst - '$var .= "Because of this `\xE9` octet (\\xE9),\n";' @@ -113,4 +117,6 @@ whitelist: - '.. versionadded:: 3.5' # Monolog - '.. versionadded:: 3.0' # Doctrine ORM - '.. _`a feature to test applications using Mercure`: https://github.com/symfony/panther#creating-isolated-browsers-to-test-apps-using-mercure-or-websocket' - - '.. End to End Tests (E2E)' + - '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 4d67a5c084c..497dfd9b430 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.1 + 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.63.0 + uses: docker://oskarstark/doctor-rst:1.67.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.1 + php-version: 8.4 coverage: none - name: Fetch branch from where the PR started diff --git a/README.md b/README.md index ed323a8ee83..5c063058c02 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ We love contributors! For more information on how you can contribute, please rea the [Symfony Docs Contributing Guide](https://symfony.com/doc/current/contributing/documentation/overview.html). > [!IMPORTANT] -> Use `5.4` branch as the base of your pull requests, unless you are documenting a -> feature that was introduced *after* Symfony 5.4 (e.g. in Symfony 7.1). +> Use `6.4` branch as the base of your pull requests, unless you are documenting a +> feature that was introduced *after* Symfony 6.4 (e.g. in Symfony 7.2). Build Documentation Locally --------------------------- diff --git a/_build/build.php b/_build/build.php index be2fb062a77..b684700a848 100755 --- a/_build/build.php +++ b/_build/build.php @@ -15,12 +15,19 @@ ->addOption('generate-fjson-files', null, InputOption::VALUE_NONE, 'Use this option to generate docs both in HTML and JSON formats') ->addOption('disable-cache', null, InputOption::VALUE_NONE, 'Use this option to force a full regeneration of all doc contents') ->setCode(function(InputInterface $input, OutputInterface $output) { + // the doc building app doesn't work on Windows + if ('\\' === DIRECTORY_SEPARATOR) { + $output->writeln('ERROR: The application that builds Symfony Docs does not support Windows. You can try using a Linux distribution via WSL (Windows Subsystem for Linux).'); + + return 1; + } + $io = new SymfonyStyle($input, $output); $io->text('Building all Symfony Docs...'); $outputDir = __DIR__.'/output'; $buildConfig = (new BuildConfig()) - ->setSymfonyVersion('5.4') + ->setSymfonyVersion('7.1') ->setContentDir(__DIR__.'/..') ->setOutputDir($outputDir) ->setImagesDir(__DIR__.'/output/_images') 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/maintainer_guide.rst b/_build/maintainer_guide.rst index fcee70f8f90..9758b4e7397 100644 --- a/_build/maintainer_guide.rst +++ b/_build/maintainer_guide.rst @@ -39,14 +39,14 @@ contributes again, it's OK to mention some of the minor issues to educate them. $ gh merge 11059 - Working on symfony/symfony-docs (branch 5.4) + Working on symfony/symfony-docs (branch 6.2) Merging Pull Request 11059: dmaicher/patch-3 ... # This is important!! Say NO to push the changes now Push the changes now? (Y/n) n - Now, push with: git push gh "5.4" refs/notes/github-comments + Now, push with: git push gh "6.2" refs/notes/github-comments # Now, open your editor and make the needed changes ... @@ -54,7 +54,7 @@ contributes again, it's OK to mention some of the minor issues to educate them. # Use "Minor reword", "Minor tweak", etc. as the commit message # now run the 'push' command shown above by 'gh' (it's different each time) - $ git push gh "5.4" refs/notes/github-comments + $ git push gh "6.2" refs/notes/github-comments Merging Pull Requests --------------------- diff --git a/_build/redirection_map b/_build/redirection_map index 295311d1532..ee14c191025 100644 --- a/_build/redirection_map +++ b/_build/redirection_map @@ -414,6 +414,7 @@ /security/entity_provider /security/user_provider /session/avoid_session_start /session /session/sessions_directory /session +/session/configuring_ttl /session#session-configure-ttl /frontend/encore/legacy-apps /frontend/encore/legacy-applications /configuration/external_parameters /configuration/environment_variables /contributing/code/patches /contributing/code/pull_requests @@ -429,6 +430,7 @@ /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 @@ -524,9 +526,10 @@ /testing/functional_tests_assertions /testing#testing-application-assertions /components https://symfony.com/components /components/index https://symfony.com/components -/serializer/normalizers /components/serializer#normalizers +/serializer/normalizers /serializer#serializer-built-in-normalizers /logging/monolog_regex_based_excludes /logging/monolog_exclude_http_codes /security/named_encoders /security/named_hashers +/components/inflector /string#inflector /security/experimental_authenticators /security /security/user_provider /security/user_providers /security/reset_password /security/passwords#reset-password @@ -555,3 +558,19 @@ /notifier/chatters /notifier#sending-chat-messages /notifier/texters /notifier#sending-sms /notifier/events /notifier#notifier-events +/email /mailer +/frontend/assetic /frontend +/frontend/assetic/index /frontend +/controller/argument_value_resolver /controller/value_resolver +/frontend/ux https://symfony.com/bundles/StimulusBundle/current/index.html +/messenger/handler_results /messenger#messenger-getting-handler-results +/messenger/dispatch_after_current_bus /messenger#messenger-transactional-messages +/messenger/multiple_buses /messenger#messenger-multiple-buses +/frontend/encore/server-data /frontend/server-data +/components/string /string +/testing/http_authentication /testing#testing_logging_in_users +/doctrine/registration_form /security#security-make-registration-form +/form/form_dependencies /form/create_custom_field_type +/doctrine/reverse_engineering /doctrine#doctrine-adding-mapping +/components/serializer /serializer +/serializer/custom_encoder /serializer/encoders#serializer-custom-encoder diff --git a/_build/spelling_word_list.txt b/_build/spelling_word_list.txt deleted file mode 100644 index 70240ceb6d1..00000000000 --- a/_build/spelling_word_list.txt +++ /dev/null @@ -1,345 +0,0 @@ -accessor -Akamai -analytics -Ansi -Ansible -Assetic -async -authenticator -authenticators -autocompleted -autocompletion -autoconfiguration -autoconfigure -autoconfigured -autoconfigures -autoconfiguring -autoload -autoloaded -autoloader -autoloaders -autoloading -autoprefixing -autowire -autowireable -autowired -autowiring -backend -backends -balancer -balancers -bcrypt -benchmarking -Bitbucket -bitmask -bitmasks -bitwise -Blackfire -boolean -booleans -Brasseur -browserslist -buildpack -buildpacks -bundler -cacheable -Caddy -callables -camelCase -casted -changelog -changeset -charset -charsets -checkboxes -classmap -classname -clearers -cloner -cloners -codebase -config -configs -configurator -configurators -contrib -cron -cronjobs -cryptographic -cryptographically -Ctrl -ctype -cURL -customizable -customizations -Cygwin -dataset -datepicker -decrypt -denormalization -denormalize -denormalized -denormalizing -deprecations -deserialization -deserialize -deserialized -deserializing -destructor -dev -dn -DNS -docblock -Dotenv -downloader -Doxygen -DSN -Dunglas -easter -Eberlei -emilie -enctype -entrypoints -enum -env -escaper -escpaer -extensibility -extractable -eZPublish -Fabien -failover -filesystem -filesystems -formatter -formatters -frontend -getter -getters -GitHub -gmail -Gmail -Goutte -grapheme -hardcode -hardcoded -hardcodes -hardcoding -hasser -hassers -headshot -HInclude -hostname -https -iconv -igbinary -incrementing -ini -inlined -inlining -installable -instantiation -interoperable -intl -Intl -invokable -IPv -isser -issers -Jpegoptim -jQuery -js -Karlton -kb -kB -Kévin -Ki -KiB -kibibyte -Kubernetes -Kudu -labelled -latin -Ldap -libketama -licensor -lifecycle -liip -linter -localhost -Loggly -Logplex -lookups -loopback -lorenzo -Luhn -macOS -matcher -matchers -mbstring -mebibyte -memcache -memcached -MiB -michelle -minification -minified -minifier -minifies -minify -minifying -misconfiguration -misconfigured -misgendering -Monolog -mutator -nagle -namespace -namespaced -namespaces -namespacing -natively -nd -netmasks -nginx -normalizer -normalizers -npm -nyholm -OAuth -OPcache -overcomplicate -Packagist -parallelizes -parsers -PHP -PHPUnit -PID -plaintext -polyfill -polyfills -postcss -Potencier -pre -preconfigured -predefines -Predis -preload -preloaded -preloading -prepend -prepended -prepending -prepends -preprocessed -preprocessors -Procfile -profiler -programmatically -prototyped -rebase -reconfiguring -reconnection -redirections -refactorization -regexes -renderer -resolvers -responder -reStructuredText -reusability -runtime -sandboxing -schemas -screencast -semantical -serializable -serializer -sexualized -Silex -sluggable -socio -specificities -SQLite -stacktrace -stacktraces -storages -stringified -stylesheet -stylesheets -subclasses -subdirectories -subdirectory -sublcasses -sublicense -sublincense -subrequests -subtree -superclass -superglobal -superglobals -symfony -Symfony -symlink -symlinks -syntaxes -templating -testability -th -theming -throbber -timestampable -timezones -TLS -tmpfs -tobias -todo -Tomayko -Toolbelt -tooltip -Traversable -triaging -UI -uid -unary -unauthenticate -uncacheable -uncached -uncomment -uncommented -undelete -unhandled -unicode -Unix -unmapped -unminified -unported -unregister -unrendered -unserialize -unserialized -unserializing -unsubmitted -untracked -uploader -URI -validator -validators -variadic -VirtualBox -Vue -webpack -webpacked -webpackJsonp -webserver -whitespace -whitespaces -woh -Wordpress -Xdebug -xkcd -Xliff -XML -XPath -yaml -yay diff --git a/_images/components/messenger/basic_cycle.png b/_images/components/messenger/basic_cycle.png new file mode 100644 index 00000000000..a0558968cbb Binary files /dev/null and b/_images/components/messenger/basic_cycle.png differ diff --git a/_images/components/messenger/overview.svg b/_images/components/messenger/overview.svg index 94737e7a6da..4b82c203756 100644 --- a/_images/components/messenger/overview.svg +++ b/_images/components/messenger/overview.svg @@ -1 +1 @@ - + diff --git a/_images/components/scheduler/generate_consume.png b/_images/components/scheduler/generate_consume.png new file mode 100644 index 00000000000..269281266a5 Binary files /dev/null and b/_images/components/scheduler/generate_consume.png differ diff --git a/_images/components/scheduler/scheduler_cycle.png b/_images/components/scheduler/scheduler_cycle.png new file mode 100644 index 00000000000..18addb37d91 Binary files /dev/null and b/_images/components/scheduler/scheduler_cycle.png differ diff --git a/_images/components/var_dumper/10-uninitialized.png b/_images/components/var_dumper/10-uninitialized.png new file mode 100644 index 00000000000..735731b83b5 Binary files /dev/null and b/_images/components/var_dumper/10-uninitialized.png differ diff --git a/_images/components/workflow/blogpost_metadata.png b/_images/components/workflow/blogpost_metadata.png new file mode 100644 index 00000000000..783f51c6ccf Binary files /dev/null and b/_images/components/workflow/blogpost_metadata.png differ diff --git a/_images/profiler/web-interface.png b/_images/profiler/web-interface.png index 2a1bc8a0650..b107f6427d7 100644 Binary files a/_images/profiler/web-interface.png and b/_images/profiler/web-interface.png differ diff --git a/_images/components/serializer/serializer_workflow.svg b/_images/serializer/serializer_workflow.svg similarity index 100% rename from _images/components/serializer/serializer_workflow.svg rename to _images/serializer/serializer_workflow.svg 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/_images/sources/components/messenger/overview.dia b/_images/sources/components/messenger/overview.dia index 55ee153439e..b0e2edaeab2 100644 Binary files a/_images/sources/components/messenger/overview.dia and b/_images/sources/components/messenger/overview.dia differ diff --git a/_images/sources/components/serializer/serializer_workflow.dia b/_images/sources/serializer/serializer_workflow.dia similarity index 100% rename from _images/sources/components/serializer/serializer_workflow.dia rename to _images/sources/serializer/serializer_workflow.dia diff --git a/_includes/_annotation_loader_tip.rst.inc b/_includes/_annotation_loader_tip.rst.inc deleted file mode 100644 index 0f4267b07f5..00000000000 --- a/_includes/_annotation_loader_tip.rst.inc +++ /dev/null @@ -1,19 +0,0 @@ -.. note:: - - In order to use the annotation loader, you should have installed the - ``doctrine/annotations`` and ``doctrine/cache`` packages with Composer. - -.. tip:: - - Annotation classes aren't loaded automatically, so you must load them - using a class loader like this:: - - use Composer\Autoload\ClassLoader; - use Doctrine\Common\Annotations\AnnotationRegistry; - - /** @var ClassLoader $loader */ - $loader = require __DIR__.'/../vendor/autoload.php'; - - AnnotationRegistry::registerLoader([$loader, 'loadClass']); - - return $loader; diff --git a/best_practices.rst b/best_practices.rst index cc38287365e..2c393cae9c6 100644 --- a/best_practices.rst +++ b/best_practices.rst @@ -214,9 +214,6 @@ Doctrine supports several metadata formats, but it's recommended to use PHP attributes because they are by far the most convenient and agile way of setting up and looking for mapping information. -If your PHP version doesn't support attributes yet, use annotations, which is -similar but requires installing some extra dependencies in your project. - Controllers ----------- @@ -234,43 +231,37 @@ nothing more than a few lines of *glue-code*, so you are not coupling the important parts of your application. .. _best-practice-controller-annotations: +.. _best-practice-controller-attributes: -Use Attributes or Annotations to Configure Routing, Caching, and Security -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Use Attributes to Configure Routing, Caching, and Security +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Using attributes or annotations for routing, caching, and security simplifies +Using attributes for routing, caching, and security simplifies configuration. You don't need to browse several files created with different formats (YAML, XML, PHP): all the configuration is just where you require it, and it only uses one format. -Don't Use Annotations to Configure the Controller Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``@Template`` annotation is useful, but also involves some *magic*. -Moreover, most of the time ``@Template`` is used without any parameters, which -makes it more difficult to know which template is being rendered. It also hides -the fact that a controller should always return a ``Response`` object. - Use Dependency Injection to Get Services ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you extend the base ``AbstractController``, you can only access to the most +If you extend the base ``AbstractController``, you can only get access to the most common services (e.g ``twig``, ``router``, ``doctrine``, etc.), directly from the container via ``$this->container->get()``. Instead, you must use dependency injection to fetch services by :ref:`type-hinting action method arguments ` or constructor arguments. -Use ParamConverters If They Are Convenient -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Use Entity Value Resolvers If They Are Convenient +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you're using :doc:`Doctrine `, then you can *optionally* use the -`ParamConverter`_ to automatically query for an entity and pass it as an argument -to your controller. It will also show a 404 page if no entity can be found. +If you're using :doc:`Doctrine `, then you can *optionally* use +the :ref:`EntityValueResolver ` to +automatically query for an entity and pass it as an argument to your +controller. It will also show a 404 page if no entity can be found. If the logic to get an entity from a route variable is more complex, instead of -configuring the ParamConverter, it's better to make the Doctrine query inside -the controller (e.g. by calling to a :doc:`Doctrine repository method `). +configuring the EntityValueResolver, it's better to make the Doctrine query +inside the controller (e.g. by calling to a :doc:`Doctrine repository method `). Templates --------- @@ -298,7 +289,7 @@ Define your Forms as PHP Classes Creating :ref:`forms in classes ` allows reusing them in different parts of the application. Besides, not creating forms in -controllers simplify the code and maintenance of the controllers. +controllers simplifies the code and maintenance of the controllers. Add Form Buttons in Templates ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -380,30 +371,27 @@ Use the ``auto`` Password Hasher The :ref:`auto password hasher ` automatically selects the best possible encoder/hasher depending on your PHP installation. -Starting from Symfony 5.3, the default auto hasher is ``bcrypt``. +Currently, the default auto hasher is ``bcrypt``. Use Voters to Implement Fine-grained Security Restrictions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If your security logic is complex, you should create custom :doc:`security voters ` instead of defining long expressions -inside the ``#[Security]`` attribute (or in the ``@Security`` annotation if your -PHP version doesn't support attributes yet). +inside the ``#[Security]`` attribute. Web Assets ---------- -Use Webpack Encore to Process Web Assets -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _use-webpack-encore-to-process-web-assets: -Web assets are things like CSS, JavaScript, and image files that make the -frontend of your site look and work great. `Webpack`_ is the leading JavaScript -module bundler that compiles, transforms and packages assets for usage in a browser. +Use AssetMapper to Manage Web Assets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -:doc:`Webpack Encore ` is a JavaScript library that gets rid of most -of Webpack complexity without hiding any of its features or distorting its usage -and philosophy. It was created for Symfony applications, but it works -for any application using any technology. +Web assets are the CSS, JavaScript, and image files that make the frontend of +your site look and work great. :doc:`AssetMapper ` lets +you write modern JavaScript and CSS without the complexity of using a bundler +such as `Webpack`_ (directly or via :doc:`Webpack Encore `). Tests ----- @@ -426,7 +414,7 @@ checks that all application URLs load successfully:: /** * @dataProvider urlProvider */ - public function testPageIsSuccessful($url) + public function testPageIsSuccessful($url): void { $client = self::createClient(); $client->request('GET', $url); @@ -434,7 +422,7 @@ checks that all application URLs load successfully:: $this->assertResponseIsSuccessful(); } - public function urlProvider() + public function urlProvider(): \Generator { yield ['/']; yield ['/posts']; @@ -466,7 +454,6 @@ you must set up a redirection. .. _`Symfony Demo`: https://github.com/symfony/demo .. _`download Symfony`: https://symfony.com/download .. _`Composer`: https://getcomposer.org/ -.. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html .. _`feature toggles`: https://en.wikipedia.org/wiki/Feature_toggle .. _`smoke testing`: https://en.wikipedia.org/wiki/Smoke_testing_(software) .. _`Webpack`: https://webpack.js.org/ diff --git a/bundles.rst b/bundles.rst index 02db1dd5d23..878bee3af4a 100644 --- a/bundles.rst +++ b/bundles.rst @@ -3,7 +3,7 @@ The Bundle System ================= -.. caution:: +.. warning:: In Symfony versions prior to 4.0, it was recommended to organize your own application code using bundles. This is :ref:`no longer recommended ` and bundles @@ -22,13 +22,15 @@ file:: return [ // 'all' means that the bundle is enabled for any Symfony environment Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], - Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], - Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], - Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], - Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], - Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], + // ... + + // this bundle is enabled only in 'dev' + Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], + // ... + // this bundle is enabled only in 'dev' and 'test', so you can't use it in 'prod' Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + // ... ]; .. tip:: @@ -41,28 +43,32 @@ Creating a Bundle ----------------- This section creates and enables a new bundle to show there are only a few steps required. -The new bundle is called AcmeTestBundle, where the ``Acme`` portion is an example +The new bundle is called AcmeBlogBundle, where the ``Acme`` portion is an example name that should be replaced by some "vendor" name that represents you or your -organization (e.g. ABCTestBundle for some company named ``ABC``). +organization (e.g. AbcBlogBundle for some company named ``Abc``). -Start by creating a ``src/Acme/TestBundle/`` directory and adding a new file -called ``AcmeTestBundle.php``:: +Start by creating a new class called ``AcmeBlogBundle``:: - // src/Acme/TestBundle/AcmeTestBundle.php - namespace App\Acme\TestBundle; + // src/AcmeBlogBundle.php + namespace Acme\BlogBundle; - use Symfony\Component\HttpKernel\Bundle\Bundle; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; - class AcmeTestBundle extends Bundle + class AcmeBlogBundle extends AbstractBundle { } +.. warning:: + + If your bundle must be compatible with previous Symfony versions you have to + extend from the :class:`Symfony\\Component\\HttpKernel\\Bundle\\Bundle` instead. + .. tip:: - The name AcmeTestBundle follows the standard + The name AcmeBlogBundle follows the standard :ref:`Bundle naming conventions `. You could - also choose to shorten the name of the bundle to simply TestBundle by naming - this class TestBundle (and naming the file ``TestBundle.php``). + also choose to shorten the name of the bundle to simply BlogBundle by naming + this class BlogBundle (and naming the file ``BlogBundle.php``). This empty class is the only piece you need to create the new bundle. Though commonly empty, this class is powerful and can be used to customize the behavior @@ -71,10 +77,12 @@ of the bundle. Now that you've created the bundle, enable it:: // config/bundles.php return [ // ... - App\Acme\TestBundle\AcmeTestBundle::class => ['all' => true], + Acme\BlogBundle\AcmeBlogBundle::class => ['all' => true], ]; -And while it doesn't do anything yet, AcmeTestBundle is now ready to be used. +And while it doesn't do anything yet, AcmeBlogBundle is now ready to be used. + +.. _bundles-directory-structure: Bundle Directory Structure -------------------------- @@ -83,35 +91,71 @@ The directory structure of a bundle is meant to help to keep code consistent between all Symfony bundles. It follows a set of conventions, but is flexible to be adjusted if needed: -``Controller/`` - the controllers of the bundle (e.g. ``RandomController.php``). - -``DependencyInjection/`` - Holds certain Dependency Injection Extension classes, which may import service - configuration, register compiler passes or more (this directory is not - necessary). - -``Resources/config/`` - Houses configuration, including routing configuration (e.g. ``routing.yaml``). +``assets/`` + Contains the web asset sources like JavaScript and TypeScript files, CSS and + Sass files, but also images and other assets related to the bundle that are + not in ``public/`` (e.g. Stimulus controllers). -``Resources/views/`` - Holds templates organized by controller name (e.g. ``Random/index.html.twig``). +``config/`` + Houses configuration, including routing configuration (e.g. ``routes.php``). -``Resources/public/`` +``public/`` Contains web assets (images, compiled CSS and JavaScript files, etc.) and is copied or symbolically linked into the project ``public/`` directory via the ``assets:install`` console command. -``Tests/`` +``src/`` + Contains all PHP classes related to the bundle logic (e.g. ``Controller/CategoryController.php``). + +``templates/`` + Holds templates organized by controller name (e.g. ``category/show.html.twig``). + +``tests/`` Holds all tests for the bundle. -A bundle can be as small or large as the feature it implements. It contains -only the files you need and nothing else. +``translations/`` + Holds translations organized by domain and locale (e.g. ``AcmeBlogBundle.en.xlf``). + +.. _bundles-legacy-directory-structure: + +.. warning:: + + The recommended bundle structure was changed in Symfony 5, read the + `Symfony 4.4 bundle documentation`_ for information about the old + structure. + + When using the new ``AbstractBundle`` class, the bundle defaults to the + new structure. Override the ``Bundle::getPath()`` method to change to + the old structure:: + + class AcmeBlogBundle extends AbstractBundle + { + public function getPath(): string + { + return __DIR__; + } + } + +.. tip:: -As you move through the guides, you'll learn how to persist objects to a -database, create and validate forms, create translations for your application, -write tests and much more. Each of these has their own place and role within -the bundle. + It's recommended to use the `PSR-4`_ autoload standard: use the namespace as key, + and the location of the bundle's main class (relative to ``composer.json``) + as value. As the main class is located in the ``src/`` directory of the bundle: + + .. code-block:: json + + { + "autoload": { + "psr-4": { + "Acme\\BlogBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Acme\\BlogBundle\\Tests\\": "tests/" + } + } + } Learn more ---------- @@ -123,3 +167,5 @@ Learn more * :doc:`/bundles/prepend_extension` .. _`third-party bundles`: https://github.com/search?q=topic%3Asymfony-bundle&type=Repositories +.. _`Symfony 4.4 bundle documentation`: https://symfony.com/doc/4.4/bundles.html#bundle-directory-structure +.. _`PSR-4`: https://www.php-fig.org/psr/psr-4/ diff --git a/bundles/best_practices.rst b/bundles/best_practices.rst index d2819e42fdb..37dc386b8e4 100644 --- a/bundles/best_practices.rst +++ b/bundles/best_practices.rst @@ -78,16 +78,22 @@ The following is the recommended directory structure of an AcmeBlogBundle: ├── LICENSE └── README.md -This directory structure requires to configure the bundle path to its root -directory as follows:: +.. note:: + + This directory structure is used by default when your bundle class extends + the recommended :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle`. + If your bundle extends the :class:`Symfony\\Component\\HttpKernel\\Bundle\\Bundle` + class, you have to override the ``getPath()`` method as follows:: - class AcmeBlogBundle extends Bundle - { - public function getPath(): string + use Symfony\Component\HttpKernel\Bundle\Bundle; + + class AcmeBlogBundle extends Bundle { - return \dirname(__DIR__); + public function getPath(): string + { + return \dirname(__DIR__); + } } - } **The following files are mandatory**, because they ensure a structure convention that automated tools can rely on: @@ -125,8 +131,8 @@ Configuration (routes, services, etc.) ``config/`` Web Assets (compiled CSS and JS, images) ``public/`` Web Asset sources (``.scss``, ``.ts``, Stimulus) ``assets/`` Translation files ``translations/`` -Validation (when not using annotations) ``config/validation/`` -Serialization (when not using annotations) ``config/serialization/`` +Validation (when not using attributes) ``config/validation/`` +Serialization (when not using attributes) ``config/serialization/`` Templates ``templates/`` Unit and Functional Tests ``tests/`` =================================================== ======================================== @@ -165,13 +171,7 @@ If the bundle includes Doctrine ORM entities and/or ODM documents, it's recommended to define their mapping using XML files stored in ``config/doctrine/``. This allows to override that mapping using the :doc:`standard Symfony mechanism to override bundle parts `. -This is not possible when using annotations/attributes to define the mapping. - -.. caution:: - - The recommended bundle structure was changed in Symfony 5, read the - `Symfony 4.4 bundle documentation`_ for information about the old - structure. +This is not possible when using attributes to define the mapping. Tests ----- @@ -246,7 +246,7 @@ with Symfony Flex to install a specific Symfony version: # recommended to have a better output and faster download time) composer update --prefer-dist --no-progress -.. caution:: +.. warning:: If you want to cache your Composer dependencies, **do not** cache the ``vendor/`` directory as this has side-effects. Instead cache @@ -298,7 +298,7 @@ following standardized instructions in your ``README.md`` file. Open a command console, enter your project directory and execute: ```console - $ composer require + composer require ``` Applications that don't use Symfony Flex @@ -310,7 +310,7 @@ following standardized instructions in your ``README.md`` file. following command to download the latest stable version of this bundle: ```console - $ composer require + composer require ``` ### Step 2: Enable the Bundle @@ -339,9 +339,9 @@ following standardized instructions in your ``README.md`` file. Open a command console, enter your project directory and execute: - .. code-block:: bash + .. code-block:: terminal - $ composer require + composer require Applications that don't use Symfony Flex ---------------------------------------- @@ -354,7 +354,7 @@ following standardized instructions in your ``README.md`` file. .. code-block:: terminal - $ composer require + composer require Step 2: Enable the Bundle ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -397,10 +397,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 ------------- @@ -444,7 +448,7 @@ The end user can provide values in any configuration file: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->parameters() ->set('acme_blog.author.email', 'fabien@example.com') ; @@ -565,4 +569,3 @@ Learn more .. _`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/ -.. _`Symfony 4.4 bundle documentation`: https://symfony.com/doc/4.4/bundles.html#bundle-directory-structure diff --git a/bundles/configuration.rst b/bundles/configuration.rst index a30b6310ec1..dedfada2ea2 100644 --- a/bundles/configuration.rst +++ b/bundles/configuration.rst @@ -42,15 +42,114 @@ as integration of other related components: // config/packages/framework.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->form()->enabled(true); }; +There are two different ways of creating friendly configuration for a bundle: + +#. :ref:`Using the main bundle class `: + this is recommended for new bundles and for bundles following the + :ref:`recommended directory structure `; +#. :ref:`Using the Bundle extension class `: + this was the traditional way of doing it, but nowadays it's only recommended for + bundles following the :ref:`legacy directory structure `. + +.. _using-the-bundle-class: +.. _bundle-friendly-config-bundle-class: + +Using the AbstractBundle Class +------------------------------ + +In bundles extending the :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` +class, you can add all the logic related to processing the configuration in that class:: + + // src/AcmeSocialBundle.php + namespace Acme\SocialBundle; + + use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + + class AcmeSocialBundle extends AbstractBundle + { + public function configure(DefinitionConfigurator $definition): void + { + $definition->rootNode() + ->children() + ->arrayNode('twitter') + ->children() + ->integerNode('client_id')->end() + ->scalarNode('client_secret')->end() + ->end() + ->end() // twitter + ->end() + ; + } + + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + // the "$config" variable is already merged and processed so you can + // use it directly to configure the service container (when defining an + // extension class, you also have to do this merging and processing) + $container->services() + ->get('acme_social.twitter_client') + ->arg(0, $config['twitter']['client_id']) + ->arg(1, $config['twitter']['client_secret']) + ; + } + } + +.. note:: + + The ``configure()`` and ``loadExtension()`` methods are called only at compile time. + +.. tip:: + + The ``AbstractBundle::configure()`` method also allows to import the + configuration definition from one or more files:: + + // src/AcmeSocialBundle.php + namespace Acme\SocialBundle; + + // ... + class AcmeSocialBundle extends AbstractBundle + { + public function configure(DefinitionConfigurator $definition): void + { + $definition->import('../config/definition.php'); + // you can also use glob patterns + //$definition->import('../config/definition/*.php'); + } + + // ... + } + + .. code-block:: php + + // config/definition.php + use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; + + return static function (DefinitionConfigurator $definition): void { + $definition->rootNode() + ->children() + ->scalarNode('foo')->defaultValue('bar')->end() + ->end() + ; + }; + +.. _bundle-friendly-config-extension: + Using the Bundle Extension -------------------------- +This is the traditional way of creating friendly configuration for bundles. For new +bundles it's recommended to :ref:`use the main bundle class `, +but the traditional way of creating an extension class still works. + Imagine you are creating a new bundle - AcmeSocialBundle - which provides -integration with Twitter. To make your bundle configurable to the user, you +integration with X/Twitter. To make your bundle configurable to the user, you can add some configuration that looks like this: .. configuration-block:: @@ -85,7 +184,7 @@ can add some configuration that looks like this: // config/packages/acme_social.php use Symfony\Config\AcmeSocialConfig; - return static function (AcmeSocialConfig $acmeSocial) { + return static function (AcmeSocialConfig $acmeSocial): void { $acmeSocial->twitter() ->clientId(123) ->clientSecret('your_secret'); @@ -110,7 +209,7 @@ load correct services and parameters inside an "Extension" class. If a bundle provides an Extension class, then you should *not* generally override any service container parameters from that bundle. The idea - is that if an Extension class is present, every setting that should be + is that if an extension class is present, every setting that should be configurable should be present in the configuration made available by that class. In other words, the extension class defines all the public configuration settings for which backward compatibility will be maintained. @@ -175,7 +274,7 @@ of your bundle's configuration. The ``Configuration`` class to handle the sample configuration looks like:: - // src/Acme/SocialBundle/DependencyInjection/Configuration.php + // src/DependencyInjection/Configuration.php namespace Acme\SocialBundle\DependencyInjection; use Symfony\Component\Config\Definition\Builder\TreeBuilder; @@ -183,7 +282,7 @@ The ``Configuration`` class to handle the sample configuration looks like:: class Configuration implements ConfigurationInterface { - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('acme_social'); @@ -216,8 +315,8 @@ This class can now be used in your ``load()`` method to merge configurations and force validation (e.g. if an additional option was passed, an exception will be thrown):: - // src/Acme/SocialBundle/DependencyInjection/AcmeSocialExtension.php - public function load(array $configs, ContainerBuilder $container) + // src/DependencyInjection/AcmeSocialExtension.php + public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); @@ -236,7 +335,7 @@ For example, imagine your bundle has the following example config: .. code-block:: xml - + - + @@ -253,13 +352,13 @@ For example, imagine your bundle has the following example config: In your extension, you can load this and dynamically set its arguments:: - // src/Acme/SocialBundle/DependencyInjection/AcmeSocialExtension.php - // ... + // src/DependencyInjection/AcmeSocialExtension.php + namespace Acme\SocialBundle\DependencyInjection; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $loader = new XmlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config')); $loader->load('services.xml'); @@ -267,7 +366,7 @@ In your extension, you can load this and dynamically set its arguments:: $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - $definition = $container->getDefinition('acme.social.twitter_client'); + $definition = $container->getDefinition('acme_social.twitter_client'); $definition->replaceArgument(0, $config['twitter']['client_id']); $definition->replaceArgument(1, $config['twitter']['client_secret']); } @@ -279,7 +378,7 @@ In your extension, you can load this and dynamically set its arguments:: :class:`Symfony\\Component\\HttpKernel\\DependencyInjection\\ConfigurableExtension` to do this automatically for you:: - // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php + // src/DependencyInjection/HelloExtension.php namespace Acme\HelloBundle\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -288,7 +387,7 @@ In your extension, you can load this and dynamically set its arguments:: class AcmeHelloExtension extends ConfigurableExtension { // note that this method is called loadInternal and not load - protected function loadInternal(array $mergedConfig, ContainerBuilder $container) + protected function loadInternal(array $mergedConfig, ContainerBuilder $container): void { // ... } @@ -304,7 +403,7 @@ In your extension, you can load this and dynamically set its arguments:: (e.g. by overriding configurations and using :phpfunction:`isset` to check for the existence of a value). Be aware that it'll be very hard to support XML:: - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $config = []; // let resources override the previous set value @@ -330,7 +429,7 @@ The ``config:dump-reference`` command dumps the default configuration of a bundle in the console using the Yaml format. As long as your bundle's configuration is located in the standard location -(``YourBundle\DependencyInjection\Configuration``) and does not have +(``/src/DependencyInjection/Configuration``) and does not have a constructor, it will work automatically. If you have something different, your ``Extension`` class must override the :method:`Extension::getConfiguration() ` @@ -364,14 +463,15 @@ URL nor does it need to exist). By default, the namespace for a bundle is ``http://example.org/schema/dic/DI_ALIAS``, where ``DI_ALIAS`` is the DI alias of the extension. You might want to change this to a more professional URL:: - // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php + // src/DependencyInjection/AcmeHelloExtension.php + namespace Acme\HelloBundle\DependencyInjection; // ... class AcmeHelloExtension extends Extension { // ... - public function getNamespace() + public function getNamespace(): string { return 'http://acme_company.com/schema/dic/hello'; } @@ -393,19 +493,20 @@ namespace is then replaced with the XSD validation base path returned from method. This namespace is then followed by the rest of the path from the base path to the file itself. -By convention, the XSD file lives in the ``Resources/config/schema/``, but you +By convention, the XSD file lives in ``config/schema/`` directory, but you can place it anywhere you like. You should return this path as the base path:: - // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php + // src/DependencyInjection/AcmeHelloExtension.php + namespace Acme\HelloBundle\DependencyInjection; // ... class AcmeHelloExtension extends Extension { // ... - public function getXsdValidationBasePath() + public function getXsdValidationBasePath(): string { - return __DIR__.'/../Resources/config/schema'; + return __DIR__.'/../config/schema'; } } diff --git a/bundles/extension.rst b/bundles/extension.rst index 74659cd98b6..d2792efc477 100644 --- a/bundles/extension.rst +++ b/bundles/extension.rst @@ -6,12 +6,74 @@ file used by the application but in the bundles themselves. This article explains how to create and load service files using the bundle directory structure. +There are two different ways of doing it: + +#. :ref:`Load your services in the main bundle class `: + this is recommended for new bundles and for bundles following the + :ref:`recommended directory structure `; +#. :ref:`Create an extension class to load the service configuration files `: + this was the traditional way of doing it, but nowadays it's only recommended for + bundles following the :ref:`legacy directory structure `. + +.. _bundle-load-services-bundle-class: + +Loading Services Directly in your Bundle Class +---------------------------------------------- + +In bundles extending the :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` +class, you can define the :method:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle::loadExtension` +method to load service definitions from configuration files:: + + // ... + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + + class AcmeHelloBundle extends AbstractBundle + { + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + // load an XML, PHP or YAML file + $container->import('../config/services.xml'); + + // you can also add or replace parameters and services + $container->parameters() + ->set('acme_hello.phrase', $config['phrase']) + ; + + if ($config['scream']) { + $container->services() + ->get('acme_hello.printer') + ->class(ScreamingPrinter::class) + ; + } + } + } + +This method works similar to the ``Extension::load()`` method explained below, +but it uses a new simpler API to define and import service configuration. + +.. note:: + + Contrary to the ``$configs`` parameter in ``Extension::load()``, the + ``$config`` parameter is already merged and processed by the + ``AbstractBundle``. + +.. note:: + + The ``loadExtension()`` is called only at compile time. + +.. _bundle-load-services-extension: + Creating an Extension Class --------------------------- -In order to load service configuration, you have to create a Dependency -Injection (DI) Extension for your bundle. By default, the Extension class must -follow these conventions (but later you'll learn how to skip them if needed): +This is the traditional way of loading service definitions in bundles. For new +bundles it's recommended to :ref:`load your services in the main bundle class `, +but the traditional way of creating an extension class still works. + +A dependency injection extension is defined as a class that follows these +conventions (later you'll learn how to skip them if needed): * It has to live in the ``DependencyInjection`` namespace of the bundle; @@ -20,13 +82,13 @@ follow these conventions (but later you'll learn how to skip them if needed): :class:`Symfony\\Component\\DependencyInjection\\Extension\\Extension` class; * The name is equal to the bundle name with the ``Bundle`` suffix replaced by - ``Extension`` (e.g. the Extension class of the AcmeBundle would be called + ``Extension`` (e.g. the extension class of the AcmeBundle would be called ``AcmeExtension`` and the one for AcmeHelloBundle would be called ``AcmeHelloExtension``). This is how the extension of an AcmeHelloBundle should look like:: - // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php + // src/DependencyInjection/AcmeHelloExtension.php namespace Acme\HelloBundle\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -34,7 +96,7 @@ This is how the extension of an AcmeHelloBundle should look like:: class AcmeHelloExtension extends Extension { - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { // ... you'll load the files here later } @@ -50,10 +112,11 @@ method to return the instance of the extension:: // ... use Acme\HelloBundle\DependencyInjection\UnconventionalExtensionClass; + use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; class AcmeHelloBundle extends Bundle { - public function getContainerExtension() + public function getContainerExtension(): ?ExtensionInterface { return new UnconventionalExtensionClass(); } @@ -69,7 +132,7 @@ class name to underscores (e.g. ``AcmeHelloExtension``'s DI alias is ``acme_hello``). Using the ``load()`` Method ---------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~ In the ``load()`` method, all services and parameters related to this extension will be loaded. This method doesn't get the actual container instance, but a @@ -83,17 +146,17 @@ but it is more common if you put these definitions in a configuration file (using the YAML, XML or PHP format). For instance, assume you have a file called ``services.xml`` in the -``Resources/config/`` directory of your bundle, your ``load()`` method looks like:: +``config/`` directory of your bundle, your ``load()`` method looks like:: use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; // ... - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $loader = new XmlFileLoader( $container, - new FileLocator(__DIR__.'/../Resources/config') + new FileLocator(__DIR__.'/../../config') ); $loader->load('services.xml'); } @@ -115,15 +178,15 @@ they are compiled when generating the application cache to improve the overall performance. Define the list of annotated classes to compile in the ``addAnnotatedClassesToCompile()`` method:: - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { // ... $this->addAnnotatedClassesToCompile([ // you can define the fully qualified class names... - 'App\\Controller\\DefaultController', + 'Acme\\BlogBundle\\Controller\\AuthorController', // ... but glob patterns are also supported: - '**Bundle\\Controller\\', + 'Acme\\BlogBundle\\Form\\**', // ... ]); @@ -138,7 +201,7 @@ Patterns are transformed into the actual class namespaces using the classmap generated by Composer. Therefore, before using these patterns, you must generate the full classmap executing the ``dump-autoload`` command of Composer. -.. caution:: +.. warning:: This technique can't be used when the classes to compile use the ``__DIR__`` or ``__FILE__`` constants, because their values will change when loading diff --git a/bundles/override.rst b/bundles/override.rst index 1e4926a1c76..f25bd785373 100644 --- a/bundles/override.rst +++ b/bundles/override.rst @@ -12,14 +12,14 @@ Templates Third-party bundle templates can be overridden in the ``/templates/bundles//`` directory. The new templates -must use the same name and path (relative to ``/Resources/views/``) as +must use the same name and path (relative to ``/templates/``) as the original templates. -For example, to override the ``Resources/views/Registration/confirmed.html.twig`` -template from the FOSUserBundle, create this template: -``/templates/bundles/FOSUserBundle/Registration/confirmed.html.twig`` +For example, to override the ``templates/registration/confirmed.html.twig`` +template from the AcmeUserBundle, create this template: +``/templates/bundles/AcmeUserBundle/registration/confirmed.html.twig`` -.. caution:: +.. warning:: If you add a template in a new location, you *may* need to clear your cache (``php bin/console cache:clear``), even if you are in debug mode. @@ -32,9 +32,9 @@ extend from the original template, not from the overridden one: .. code-block:: twig - {# templates/bundles/FOSUserBundle/Registration/confirmed.html.twig #} + {# templates/bundles/AcmeUserBundle/registration/confirmed.html.twig #} {# the special '!' prefix avoids errors when extending from an overridden template #} - {% extends "@!FOSUser/Registration/confirmed.html.twig" %} + {% extends "@!AcmeUser/registration/confirmed.html.twig" %} {% block some_block %} ... @@ -162,7 +162,7 @@ For this reason, you can override any bundle translation file from the main ``translations/`` directory, as long as the new file uses the same domain. For example, to override the translations defined in the -``Resources/translations/FOSUserBundle.es.yml`` file of the FOSUserBundle, -create a ``/translations/FOSUserBundle.es.yml`` file. +``translations/AcmeUserBundle.es.yaml`` file of the AcmeUserBundle, +create a ``/translations/AcmeUserBundle.es.yaml`` file. .. _`the Doctrine documentation`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/inheritance-mapping.html#overrides diff --git a/bundles/prepend_extension.rst b/bundles/prepend_extension.rst index 35c277ec0e6..e4099d9f81a 100644 --- a/bundles/prepend_extension.rst +++ b/bundles/prepend_extension.rst @@ -31,7 +31,7 @@ To give an Extension the power to do this, it needs to implement { // ... - public function prepend(ContainerBuilder $container) + public function prepend(ContainerBuilder $container): void { // ... } @@ -52,7 +52,7 @@ a configuration setting in multiple bundles as well as disable a flag in multipl in case a specific other bundle is not registered:: // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php - public function prepend(ContainerBuilder $container) + public function prepend(ContainerBuilder $container): void { // get all bundles $bundles = $container->getParameter('kernel.bundles'); @@ -61,18 +61,16 @@ in case a specific other bundle is not registered:: // disable AcmeGoodbyeBundle in bundles $config = ['use_acme_goodbye' => false]; foreach ($container->getExtensions() as $name => $extension) { - switch ($name) { - case 'acme_something': - case 'acme_other': - // set use_acme_goodbye to false in the config of - // acme_something and acme_other - // - // note that if the user manually configured - // use_acme_goodbye to true in config/services.yaml - // then the setting would in the end be true and not false - $container->prependExtensionConfig($name, $config); - break; - } + match ($name) { + // set use_acme_goodbye to false in the config of + // acme_something and acme_other + // + // note that if the user manually configured + // use_acme_goodbye to true in config/services.yaml + // then the setting would in the end be true and not false + 'acme_something', 'acme_other' => $container->prependExtensionConfig($name, $config), + default => null + }; } } @@ -141,7 +139,7 @@ registered and the ``entity_manager_name`` setting for ``acme_hello`` is set to // config/packages/acme_something.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->extension('acme_something', [ // ... 'use_acme_goodbye' => false, @@ -153,6 +151,70 @@ registered and the ``entity_manager_name`` setting for ``acme_hello`` is set to ]); }; +Prepending Extension in the Bundle Class +---------------------------------------- + +You can also prepend extension configuration directly in your +Bundle class if you extend from the :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` +class and define the :method:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle::prependExtension` +method:: + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + + class FooBundle extends AbstractBundle + { + public function prependExtension(ContainerConfigurator $containerConfigurator, ContainerBuilder $containerBuilder): void + { + // prepend + $containerBuilder->prependExtensionConfig('framework', [ + 'cache' => ['prefix_seed' => 'foo/bar'], + ]); + + // prepend config from a file + $containerConfigurator->import('../config/packages/cache.php'); + } + } + +.. note:: + + The ``prependExtension()`` method, like ``prepend()``, is called only at compile time. + +.. versionadded:: 7.1 + + Starting from Symfony 7.1, calling the :method:`Symfony\\Component\\DependencyInjection\\Loader\\Configurator\\ContainerConfigurator::import` + method inside ``prependExtension()`` will prepend the given configuration. + In previous Symfony versions, this method appended the configuration. + +Alternatively, you can use the ``prepend`` parameter of the +:method:`Symfony\\Component\\DependencyInjection\\Loader\\Configurator\\ContainerConfigurator::extension` +method:: + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + + class FooBundle extends AbstractBundle + { + public function prependExtension(ContainerConfigurator $containerConfigurator, ContainerBuilder $containerBuilder): void + { + // ... + + $containerConfigurator->extension('framework', [ + 'cache' => ['prefix_seed' => 'foo/bar'], + ], prepend: true); + + // ... + } + } + +.. versionadded:: 7.1 + + The ``prepend`` parameter of the + :method:`Symfony\\Component\\DependencyInjection\\Loader\\Configurator\\ContainerConfigurator::extension` + method was added in Symfony 7.1. + More than one Bundle using PrependExtensionInterface ---------------------------------------------------- diff --git a/cache.rst b/cache.rst index c073a98387f..83bb5b4cedc 100644 --- a/cache.rst +++ b/cache.rst @@ -10,7 +10,7 @@ The following example shows a typical usage of the cache:: use Symfony\Contracts\Cache\ItemInterface; // The callable will only be executed on a cache miss. - $value = $pool->get('my_cache_key', function (ItemInterface $item) { + $value = $pool->get('my_cache_key', function (ItemInterface $item): string { $item->expiresAfter(3600); // ... do some HTTP request or heavy computations @@ -24,14 +24,9 @@ The following example shows a typical usage of the cache:: // ... and to remove the cache key $pool->delete('my_cache_key'); -Symfony supports Cache Contracts, PSR-6/16 and Doctrine Cache interfaces. +Symfony supports Cache Contracts and PSR-6/16 interfaces. You can read more about these at the :doc:`component documentation `. -.. deprecated:: 5.4 - - Support for Doctrine Cache was deprecated in Symfony 5.4 - and it will be removed in Symfony 6.0. - .. _cache-configuration-with-frameworkbundle: Configuring Cache with FrameworkBundle @@ -92,7 +87,7 @@ adapter (template) they use by using the ``app`` and ``system`` key like: // config/packages/cache.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->cache() ->app('cache.adapter.filesystem') ->system('cache.adapter.system') @@ -108,7 +103,6 @@ The Cache component comes with a series of adapters pre-configured: * :doc:`cache.adapter.apcu ` * :doc:`cache.adapter.array ` -* :doc:`cache.adapter.doctrine ` (deprecated) * :doc:`cache.adapter.doctrine_dbal ` * :doc:`cache.adapter.filesystem ` * :doc:`cache.adapter.memcached ` @@ -117,10 +111,6 @@ The Cache component comes with a series of adapters pre-configured: * :doc:`cache.adapter.redis ` * :ref:`cache.adapter.redis_tag_aware ` (Redis adapter optimized to work with tags) -.. versionadded:: 5.2 - - ``cache.adapter.redis_tag_aware`` has been introduced in Symfony 5.2. - .. note:: There's also a special ``cache.adapter.system`` adapter. It's recommended to @@ -143,12 +133,7 @@ Some of these adapters could be configured via shortcuts. default_psr6_provider: 'app.my_psr6_service' default_redis_provider: 'redis://localhost' default_memcached_provider: 'memcached://localhost' - default_pdo_provider: 'app.my_pdo_service' - - services: - app.my_pdo_service: - class: \PDO - arguments: ['pgsql:host=localhost'] + default_pdo_provider: 'pgsql:host=localhost' .. code-block:: xml @@ -169,24 +154,17 @@ Some of these adapters could be configured via shortcuts. default-psr6-provider="app.my_psr6_service" default-redis-provider="redis://localhost" default-memcached-provider="memcached://localhost" - default-pdo-provider="app.my_pdo_service" + default-pdo-provider="pgsql:host=localhost" /> - - - - pgsql:host=localhost - - .. code-block:: php // config/packages/cache.php - use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework, ContainerConfigurator $container) { + return static function (FrameworkConfig $framework): void { $framework->cache() // Only used with cache.adapter.filesystem ->directory('%kernel.cache_dir%/pools') @@ -195,19 +173,13 @@ Some of these adapters could be configured via shortcuts. ->defaultPsr6Provider('app.my_psr6_service') ->defaultRedisProvider('redis://localhost') ->defaultMemcachedProvider('memcached://localhost') - ->defaultPdoProvider('app.my_pdo_service') - ; - - $container->services() - ->set('app.my_pdo_service', \PDO::class) - ->args(['pgsql:host=localhost']) + ->defaultPdoProvider('pgsql:host=localhost') ; }; -.. deprecated:: 5.4 +.. versionadded:: 7.1 - The ``default_doctrine_provider`` option was deprecated in Symfony 5.4 and - it will be removed in Symfony 6.0. + Using a DSN as the provider for the PDO adapter was introduced in Symfony 7.1. .. _cache-create-pools: @@ -295,7 +267,7 @@ You can also create more customized pools: // config/packages/cache.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $cache = $framework->cache(); $cache->defaultMemcachedProvider('memcached://localhost'); @@ -338,15 +310,16 @@ with either :class:`Symfony\\Contracts\\Cache\\CacheInterface` or ``Psr\Cache\CacheItemPoolInterface``:: use Symfony\Contracts\Cache\CacheInterface; + // ... // from a controller method - public function listProducts(CacheInterface $customThingCache) + public function listProducts(CacheInterface $customThingCache): Response { // ... } // in a service - public function __construct(CacheInterface $customThingCache) + public function __construct(private CacheInterface $customThingCache) { // ... } @@ -394,7 +367,7 @@ with either :class:`Symfony\\Contracts\\Cache\\CacheInterface` or // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $container->services() // ... @@ -475,7 +448,7 @@ and use that when configuring the pool. use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Config\FrameworkConfig; - return static function (ContainerBuilder $container, FrameworkConfig $framework) { + return static function (ContainerBuilder $container, FrameworkConfig $framework): void { $framework->cache() ->pool('cache.my_redis') ->adapters(['cache.adapter.redis']) @@ -554,7 +527,7 @@ Symfony stores the item automatically in all the missing pools. // config/packages/cache.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->cache() ->pool('my_cache_pool') ->defaultLifetime(31536000) // One year @@ -579,23 +552,21 @@ the same tag could be invalidated with one function call:: class SomeClass { - private $myCachePool; - // using autowiring to inject the cache pool - public function __construct(TagAwareCacheInterface $myCachePool) - { - $this->myCachePool = $myCachePool; + public function __construct( + private TagAwareCacheInterface $myCachePool, + ) { } - public function someMethod() + public function someMethod(): void { - $value0 = $this->myCachePool->get('item_0', function (ItemInterface $item) { + $value0 = $this->myCachePool->get('item_0', function (ItemInterface $item): string { $item->tag(['foo', 'bar']); return 'debug'; }); - $value1 = $this->myCachePool->get('item_1', function (ItemInterface $item) { + $value1 = $this->myCachePool->get('item_1', function (ItemInterface $item): string { $item->tag('foo'); return 'debug'; @@ -619,6 +590,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 @@ -635,7 +607,7 @@ to enable this feature. This could be added by using the following configuration @@ -647,11 +619,11 @@ to enable this feature. This could be added by using the following configuration // config/packages/cache.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->cache() ->pool('my_cache_pool') ->tags(true) - ->adapters(['cache.adapter.redis']) + ->adapters(['cache.adapter.redis_tag_aware']) ; }; @@ -701,7 +673,7 @@ achieved by specifying the adapter. // config/packages/cache.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->cache() ->pool('my_cache_pool') ->tags('tag_pool') @@ -753,19 +725,42 @@ Clear all custom pools: $ php bin/console cache:pool:clear cache.app_clearer +Clear all cache pools: + +.. code-block:: terminal + + $ php bin/console cache:pool:clear --all + +Clear all cache pools except some: + +.. code-block:: terminal + + $ php bin/console cache:pool:clear --all --exclude=my_cache_pool --exclude=another_cache_pool + Clear all caches everywhere: .. code-block:: terminal $ php bin/console cache:pool:clear cache.global_clearer -Encrypting the Cache --------------------- +Clear cache by tag(s): + +.. code-block:: terminal + + # invalidate tag1 from all taggable pools + $ php bin/console cache:pool:invalidate-tags tag1 + + # invalidate tag1 & tag2 from all taggable pools + $ php bin/console cache:pool:invalidate-tags tag1 tag2 -.. versionadded:: 5.1 + # invalidate tag1 & tag2 from cache.app pool + $ php bin/console cache:pool:invalidate-tags tag1 tag2 --pool=cache.app - The :class:`Symfony\\Component\\Cache\\Marshaller\\SodiumMarshaller` - class was introduced in Symfony 5.1. + # invalidate tag1 & tag2 from cache1 & cache2 pools + $ php bin/console cache:pool:invalidate-tags tag1 tag2 -p cache1 -p cache2 + +Encrypting the Cache +-------------------- To encrypt the cache using ``libsodium``, you can use the :class:`Symfony\\Component\\Cache\\Marshaller\\SodiumMarshaller`. @@ -835,7 +830,7 @@ Then, register the ``SodiumMarshaller`` service using this key: //->addArgument(['env(base64:CACHE_DECRYPTION_KEY)', 'env(base64:OLD_CACHE_DECRYPTION_KEY)']) ->addArgument(new Reference('.inner')); -.. caution:: +.. danger:: This will encrypt the values of the cache items, but not the cache keys. Be careful not to leak sensitive data in the keys. @@ -848,10 +843,6 @@ cache items encrypted with the old key have expired, you can completely remove Computing Cache Values Asynchronously ------------------------------------- -.. versionadded:: 5.2 - - The feature to compute cache values asynchronously was introduced in Symfony 5.2. - The Cache component uses the `probabilistic early expiration`_ algorithm to protect against the :ref:`cache stampede ` problem. This means that some cache items are elected for early-expiration while they are @@ -894,15 +885,13 @@ In the following example, the value is requested from a controller:: use App\Cache\CacheComputation; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; class CacheController extends AbstractController { - /** - * @Route("/cache", name="cache") - */ + #[Route('/cache', name: 'cache')] public function index(CacheInterface $asyncCache): Response { // pass to the cache the service method that refreshes the item diff --git a/components/asset.rst b/components/asset.rst index e515b41395c..d6d3f485859 100644 --- a/components/asset.rst +++ b/components/asset.rst @@ -179,25 +179,17 @@ listed in the manifest:: echo $package->getUrl('not-found.css'); // error: -.. versionadded:: 5.4 - - The ``$strictMode`` option was introduced in Symfony 5.4. - If your JSON file is not on your local filesystem but is accessible over HTTP, -use the :class:`Symfony\\Component\\Asset\\VersionStrategy\\RemoteJsonManifestVersionStrategy` +use the :class:`Symfony\\Component\\Asset\\VersionStrategy\\JsonManifestVersionStrategy` with the :doc:`HttpClient component `:: use Symfony\Component\Asset\Package; - use Symfony\Component\Asset\VersionStrategy\RemoteJsonManifestVersionStrategy; + use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy; use Symfony\Component\HttpClient\HttpClient; $httpClient = HttpClient::create(); $manifestUrl = 'https://cdn.example.com/rev-manifest.json'; - $package = new Package(new RemoteJsonManifestVersionStrategy($manifestUrl, $httpClient)); - -.. versionadded:: 5.1 - - The ``RemoteJsonManifestVersionStrategy`` was introduced in Symfony 5.1. + $package = new Package(new JsonManifestVersionStrategy($manifestUrl, $httpClient)); Custom Version Strategies ......................... diff --git a/components/browser_kit.rst b/components/browser_kit.rst index 12c2a63a7c7..8cf0772298c 100644 --- a/components/browser_kit.rst +++ b/components/browser_kit.rst @@ -4,13 +4,6 @@ The BrowserKit Component The BrowserKit component simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically. -.. note:: - - In Symfony versions prior to 4.3, the BrowserKit component could only make - internal requests to your application. Starting from Symfony 4.3, this - component can also :ref:`make HTTP requests to any public site ` - when using it in combination with the :doc:`HttpClient component `. - Installation ------------ @@ -45,7 +38,7 @@ This method accepts a request and should return a response:: class Client extends AbstractBrowser { - protected function doRequest($request) + protected function doRequest($request): Response { // ... convert request into a response @@ -86,10 +79,6 @@ convert the request parameters into a JSON string and set the needed HTTP header // this encodes parameters as JSON and sets the required CONTENT_TYPE and HTTP_ACCEPT headers $crawler = $client->jsonRequest('GET', '/', ['some_parameter' => 'some_value']); -.. versionadded:: 5.3 - - The ``jsonRequest()`` method was introduced in Symfony 5.3. - The :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::xmlHttpRequest` method, which defines the same arguments as the ``request()`` method, is a shortcut to make AJAX requests:: @@ -123,6 +112,24 @@ provides access to the link properties (e.g. ``$link->getMethod()``, $link = $crawler->selectLink('Go elsewhere...')->link(); $client->click($link); +The :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::click` and +:method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::clickLink` methods +can take an optional ``serverParameters`` argument. This +parameter allows to send additional information like headers when clicking +on a link:: + + use Acme\Client; + + $client = new Client(); + $client->request('GET', '/product/123'); + + // works both with `click()`... + $link = $crawler->selectLink('Go elsewhere...')->link(); + $client->click($link, ['X-Custom-Header' => 'Some data']); + + // ... and `clickLink()` + $crawler = $client->clickLink('Go elsewhere...', ['X-Custom-Header' => 'Some data']); + Submitting Forms ~~~~~~~~~~~~~~~~ @@ -136,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 + + +Then create the controller: + +.. code-block:: javascript + + // assets/controllers/form-collection_controller.js + + import { Controller } from '@hotwired/stimulus'; + + export default class extends Controller { + static targets = ["collectionContainer"] + + static values = { + index : Number, + prototype: String, + } + + addCollectionElement(event) + { + const item = document.createElement('li'); + item.innerHTML = this.prototypeValue.replace(/__name__/g, this.indexValue); + this.collectionContainerTarget.appendChild(item); + this.indexValue++; + } + } + +Handling the new Tags in PHP +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + To make handling these new tags easier, add an "adder" and a "remover" method for the tags in the ``Task`` class:: @@ -381,13 +427,13 @@ That was fine, but forcing the use of the "adder" method makes handling these new ``Tag`` objects easier (especially if you're using Doctrine, which you will learn about next!). -.. caution:: +.. warning:: You have to create **both** ``addTag()`` and ``removeTag()`` methods, otherwise the form will still use ``setTag()`` even if ``by_reference`` is ``false``. You'll learn more about the ``removeTag()`` method later in this article. -.. caution:: +.. warning:: Symfony can only make the plural-to-singular conversion (e.g. from the ``tags`` property to the ``addTag()`` method) for English words. Code @@ -412,16 +458,14 @@ you will learn about next!). .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Task.php // ... - /** - * @ORM\ManyToMany(targetEntity="App\Entity\Tag", cascade={"persist"}) - */ - protected $tags; + #[ORM\ManyToMany(targetEntity: Tag::class, cascade: ['persist'])] + protected Collection $tags; .. code-block:: yaml @@ -535,7 +579,8 @@ on the server. In order for this to work in an HTML form, you must remove the DOM element for the collection item to be removed, before submitting the form. -First, add a "delete this tag" link to each tag form: +In the JavaScript code, add a "delete" button to each existing tag on the page. +Then, append the "add delete button" method in the function that adds the new tags: .. code-block:: javascript @@ -547,7 +592,7 @@ First, add a "delete this tag" link to each tag form: // ... the rest of the block from above - const addFormToCollection = (e) => { + function addFormToCollection(e) { // ... // add a delete link to the new form @@ -558,7 +603,7 @@ The ``addTagFormDeleteLink()`` function will look something like this: .. code-block:: javascript - const addTagFormDeleteLink = (item) => { + function addTagFormDeleteLink(item) { const removeFormButton = document.createElement('button'); removeFormButton.innerText = 'Delete this tag'; @@ -663,3 +708,4 @@ the relationship between the removed ``Tag`` and ``Task`` object. .. _`symfony-collection`: https://github.com/ninsuo/symfony-collection .. _`ArrayCollection`: https://www.doctrine-project.org/projects/doctrine-collections/en/1.6/index.html .. _`Symfony UX Demo of Form Collections`: https://ux.symfony.com/live-component/demos/form-collection-type +.. _`Stimulus`: https://symfony.com/doc/current/frontend/encore/simple-example.html#stimulus-symfony-ux diff --git a/form/form_customization.rst b/form/form_customization.rst index 005e0eac461..dc09aefe77d 100644 --- a/form/form_customization.rst +++ b/form/form_customization.rst @@ -17,8 +17,8 @@ enough to render an entire form, including all its fields and error messages: .. code-block:: twig - {# form is a variable passed from the controller via either - $this->renderForm('...', ['form' => $form]) + {# form is a variable passed from the controller via + $this->render('...', ['form' => $form]) or $this->render('...', ['form' => $form->createView()]) #} {{ form(form) }} @@ -74,7 +74,7 @@ control over how each form field is rendered, so you can fully customize them: -.. caution:: +.. warning:: If you're rendering each field manually, make sure you don't forget the ``_token`` field that is automatically added for CSRF protection. @@ -103,6 +103,7 @@ That's why Symfony provides other Twig form helpers that render the value of each form field part without adding any HTML around it: * ``field_name()`` +* ``field_id()`` * ``field_value()`` * ``field_label()`` * ``field_help()`` @@ -116,6 +117,7 @@ fields, so you no longer have to deal with form themes: -.. versionadded:: 5.2 +.. versionadded:: 7.3 - The ``field_*()`` helpers were introduced in Symfony 5.2. + The ``field_id()`` helper was introduced in Symfony 7.3. Form Rendering Variables ------------------------ @@ -309,7 +311,7 @@ Renders any errors for the given field. {# render any "global" errors not associated to any form field #} {{ form_errors(form) }} -.. caution:: +.. warning:: In the Bootstrap 4 form theme, ``form_errors()`` is already included in ``form_label()``. Read more about this in the diff --git a/form/form_dependencies.rst b/form/form_dependencies.rst deleted file mode 100644 index 96b067362ff..00000000000 --- a/form/form_dependencies.rst +++ /dev/null @@ -1,12 +0,0 @@ -How to Access Services or Config from Inside a Form -=================================================== - -The content of this article is no longer relevant because in current Symfony -versions, form classes are services by default and you can inject services in -them using the :doc:`service autowiring ` feature. - -Read the article about :doc:`creating custom form types ` -to see an example of how to inject the database service into a form type. In the -same article you can also read about -:ref:`configuration options for form types `, which is -another way of passing services to forms. diff --git a/form/form_themes.rst b/form/form_themes.rst index 5f462ce4bbb..8b82982edaa 100644 --- a/form/form_themes.rst +++ b/form/form_themes.rst @@ -43,14 +43,6 @@ in a single Twig template and they are enabled in the element with the absolute minimum styles to make them usable. It is based on the `Tailwind CSS form plugin`_. -.. versionadded:: 5.1 - - The ``foundation_6_layout.html.twig`` was introduced in Symfony 5.1. - -.. versionadded:: 5.3 - - The ``bootstrap_5_layout.html.twig``, ``bootstrap_5_horizontal_layout.html.twig`` and ``tailwind_2_layout.html.twig`` were introduced in Symfony 5.3. - .. tip:: Read the articles about :doc:`Bootstrap 4 Symfony form theme ` and :doc:`Bootstrap 5 Symfony form theme ` @@ -97,7 +89,7 @@ want to use another theme for all the forms of your app, configure it in the // config/packages/twig.php use Symfony\Config\TwigConfig; - return static function (TwigConfig $twig) { + return static function (TwigConfig $twig): void { $twig->formThemes([ 'bootstrap_5_horizontal_layout.html.twig', ]); @@ -185,7 +177,7 @@ of form themes: {# ... #} -.. caution:: +.. warning:: When using the ``only`` keyword, none of Symfony's built-in form themes (``form_div_layout.html.twig``, etc.) will be applied. In order to render @@ -346,10 +338,6 @@ You can also customize each entry of all collections with the following blocks: {% block collection_entry_help %} ... {% endblock %} {% block collection_entry_errors %} ... {% endblock %} -.. versionadded:: 5.1 - - The ``collection_entry_*`` blocks were introduced in Symfony 5.1. - Finally, you can customize specific form collections instead of all of them. For example, consider the following complex example where a ``TaskManagerType`` has a collection of ``TaskListType`` which in turn has a collection of @@ -528,7 +516,7 @@ you want to apply the theme globally to all forms, define the // config/packages/twig.php use Symfony\Config\TwigConfig; - return static function (TwigConfig $twig) { + return static function (TwigConfig $twig): void { $twig->formThemes([ 'form/my_theme.html.twig', ]); diff --git a/form/inherit_data_option.rst b/form/inherit_data_option.rst index 64001ba074d..2caa0afcdbe 100644 --- a/form/inherit_data_option.rst +++ b/form/inherit_data_option.rst @@ -10,13 +10,13 @@ entities, a ``Company`` and a ``Customer``:: class Company { - private $name; - private $website; + private string $name; + private string $website; - private $address; - private $zipcode; - private $city; - private $country; + private string $address; + private string $zipcode; + private string $city; + private string $country; } .. code-block:: php @@ -26,13 +26,13 @@ entities, a ``Company`` and a ``Customer``:: class Customer { - private $firstName; - private $lastName; + private string $firstName; + private string $lastName; - private $address; - private $zipcode; - private $city; - private $country; + private string $address; + private string $zipcode; + private string $city; + private string $country; } As you can see, each entity shares a few of the same fields: ``address``, @@ -165,6 +165,6 @@ Finally, make this work by adding the location form to your two original forms:: That's it! You have extracted duplicated field definitions to a separate location form that you can reuse wherever you need it. -.. caution:: +.. warning:: Forms with the ``inherit_data`` option set cannot have ``*_SET_DATA`` event listeners. diff --git a/form/type_guesser.rst b/form/type_guesser.rst index 29c9cea0e21..106eb4e7742 100644 --- a/form/type_guesser.rst +++ b/form/type_guesser.rst @@ -44,14 +44,14 @@ This interface requires four methods: Start by creating the class and these methods. Next, you'll learn how to fill each in:: - // src/Form/TypeGuesser/PHPDocTypeGuesser.php + // src/Form/TypeGuesser/PhpDocTypeGuesser.php namespace App\Form\TypeGuesser; use Symfony\Component\Form\FormTypeGuesserInterface; use Symfony\Component\Form\Guess\TypeGuess; use Symfony\Component\Form\Guess\ValueGuess; - class PHPDocTypeGuesser implements FormTypeGuesserInterface + class PhpDocTypeGuesser implements FormTypeGuesserInterface { public function guessType(string $class, string $property): ?TypeGuess { @@ -90,9 +90,9 @@ The ``TypeGuess`` constructor requires three options: type with the highest confidence is used. With this knowledge, you can implement the ``guessType()`` method of the -``PHPDocTypeGuesser``:: +``PhpDocTypeGuesser``:: - // src/Form/TypeGuesser/PHPDocTypeGuesser.php + // src/Form/TypeGuesser/PhpDocTypeGuesser.php namespace App\Form\TypeGuesser; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; @@ -102,7 +102,7 @@ With this knowledge, you can implement the ``guessType()`` method of the use Symfony\Component\Form\Guess\Guess; use Symfony\Component\Form\Guess\TypeGuess; - class PHPDocTypeGuesser implements FormTypeGuesserInterface + class PhpDocTypeGuesser implements FormTypeGuesserInterface { public function guessType(string $class, string $property): ?TypeGuess { @@ -113,30 +113,21 @@ With this knowledge, you can implement the ``guessType()`` method of the } // otherwise, base the type on the @var annotation - switch ($annotations['var']) { - case 'string': - // there is a high confidence that the type is text when - // @var string is used - return new TypeGuess(TextType::class, [], Guess::HIGH_CONFIDENCE); - - case 'int': - case 'integer': - // integers can also be the id of an entity or a checkbox (0 or 1) - return new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE); - - case 'float': - case 'double': - case 'real': - return new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE); - - case 'boolean': - case 'bool': - return new TypeGuess(CheckboxType::class, [], Guess::HIGH_CONFIDENCE); - - default: - // there is a very low confidence that this one is correct - return new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE); - } + return match($annotations['var']) { + // there is a high confidence that the type is text when + // @var string is used + 'string' => new TypeGuess(TextType::class, [], Guess::HIGH_CONFIDENCE), + + // integers can also be the id of an entity or a checkbox (0 or 1) + 'int', 'integer' => new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE), + + 'float', 'double', 'real' => new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE), + + 'boolean', 'bool' => new TypeGuess(CheckboxType::class, [], Guess::HIGH_CONFIDENCE), + + // there is a very low confidence that this one is correct + default => new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE) + }; } protected function readPhpDocAnnotations(string $class, string $property): array @@ -171,7 +162,7 @@ instance with the value of the option. This constructor has 2 arguments: ``null`` is guessed when you believe the value of the option should not be set. -.. caution:: +.. warning:: You should be very careful using the ``guessMaxLength()`` method. When the type is a float, you cannot determine a length (e.g. you want a float to be @@ -197,7 +188,7 @@ and tag it with ``form.type_guesser``: services: # ... - App\Form\TypeGuesser\PHPDocTypeGuesser: + App\Form\TypeGuesser\PhpDocTypeGuesser: tags: [form.type_guesser] .. code-block:: xml @@ -210,7 +201,7 @@ and tag it with ``form.type_guesser``: https://symfony.com/schema/dic/services/services-1.0.xsd"> - + @@ -219,9 +210,9 @@ and tag it with ``form.type_guesser``: .. code-block:: php // config/services.php - use App\Form\TypeGuesser\PHPDocTypeGuesser; + use App\Form\TypeGuesser\PhpDocTypeGuesser; - $container->register(PHPDocTypeGuesser::class) + $container->register(PhpDocTypeGuesser::class) ->addTag('form.type_guesser') ; @@ -232,12 +223,12 @@ and tag it with ``form.type_guesser``: :method:`Symfony\\Component\\Form\\FormFactoryBuilder::addTypeGuessers` of the ``FormFactoryBuilder`` to register new type guessers:: - use App\Form\TypeGuesser\PHPDocTypeGuesser; + use App\Form\TypeGuesser\PhpDocTypeGuesser; use Symfony\Component\Form\Forms; $formFactory = Forms::createFormFactoryBuilder() // ... - ->addTypeGuesser(new PHPDocTypeGuesser()) + ->addTypeGuesser(new PhpDocTypeGuesser()) ->getFormFactory(); // ... diff --git a/form/unit_testing.rst b/form/unit_testing.rst index bcd82a1ee38..9603c5bc0d2 100644 --- a/form/unit_testing.rst +++ b/form/unit_testing.rst @@ -1,7 +1,7 @@ How to Unit Test your Forms =========================== -.. caution:: +.. warning:: This article is intended for developers who create :doc:`custom form types `. If you are using @@ -44,7 +44,7 @@ The simplest ``TypeTestCase`` implementation looks like the following:: class TestedTypeTest extends TypeTestCase { - public function testSubmitValidData() + public function testSubmitValidData(): void { $formData = [ 'test' => 'test', @@ -68,7 +68,7 @@ The simplest ``TypeTestCase`` implementation looks like the following:: $this->assertEquals($expected, $model); } - public function testCustomFormView() + public function testCustomFormView(): void { $formData = new TestObject(); // ... prepare the data as you need @@ -121,7 +121,7 @@ variable exists and will be available in your form themes:: Use `PHPUnit data providers`_ to test multiple form conditions using the same test code. -.. caution:: +.. warning:: When your type relies on the ``EntityType``, you should register the :class:`Symfony\\Bridge\\Doctrine\\Form\\DoctrineOrmExtension`, which will @@ -147,27 +147,27 @@ make sure the ``FormRegistry`` uses the created instance:: namespace App\Tests\Form\Type; use App\Form\Type\TestedType; - use Doctrine\Persistence\ObjectManager; + use Doctrine\ORM\EntityManager; use Symfony\Component\Form\PreloadedExtension; use Symfony\Component\Form\Test\TypeTestCase; // ... class TestedTypeTest extends TypeTestCase { - private $objectManager; + private MockObject&EntityManager $entityManager; protected function setUp(): void { // mock any dependencies - $this->objectManager = $this->createMock(ObjectManager::class); + $this->entityManager = $this->createMock(EntityManager::class); parent::setUp(); } - protected function getExtensions() + protected function getExtensions(): array { // create a type instance with the mocked dependencies - $type = new TestedType($this->objectManager); + $type = new TestedType($this->entityManager); return [ // register the type instances with the PreloadedExtension @@ -175,7 +175,7 @@ make sure the ``FormRegistry`` uses the created instance:: ]; } - public function testSubmitValidData() + public function testSubmitValidData(): void { // ... @@ -210,14 +210,13 @@ allows you to return a list of extensions to register:: class TestedTypeTest extends TypeTestCase { - protected function getExtensions() + protected function getExtensions(): array { $validator = Validation::createValidator(); - // or if you also need to read constraints from annotations + // or if you also need to read constraints from attributes $validator = Validation::createValidatorBuilder() - ->enableAnnotationMapping(true) - ->addDefaultDoctrineAnnotationReader() + ->enableAttributeMapping() ->getValidator(); return [ @@ -241,4 +240,13 @@ guessers using the :method:`Symfony\\Component\\Form\\Test\\FormIntegrationTestC and :method:`Symfony\\Component\\Form\\Test\\FormIntegrationTestCase::getTypeGuessers` methods. +When testing the themes of your forms, consider making your test extend the +:class:`Symfony\\Bridge\\Twig\\Test\\FormLayoutTestCase` class. This saves a lot +of boilerplate and code duplication by implementing the +:class:`Symfony\\Component\\Form\\Test\\FormIntegrationTestCase` methods for you. +All you need to do is to implement the +:method:`Symfony\\Bridge\\Twig\\Test\\FormLayoutTestCase::getTemplatePaths`, the +:method:`Symfony\\Bridge\\Twig\\Test\\FormLayoutTestCase::getTwigExtensions` and +the :method:`Symfony\\Bridge\\Twig\\Test\\FormLayoutTestCase::getThemes` methods. + .. _`PHPUnit data providers`: https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#data-providers diff --git a/form/use_empty_data.rst b/form/use_empty_data.rst index 3290f5df443..5387820693b 100644 --- a/form/use_empty_data.rst +++ b/form/use_empty_data.rst @@ -50,11 +50,9 @@ that constructor with no arguments:: class BlogType extends AbstractType { - private $someDependency; - - public function __construct($someDependency) - { - $this->someDependency = $someDependency; + public function __construct( + private object $someDependency, + ) { } // ... @@ -96,7 +94,7 @@ The closure must accept a ``FormInterface`` instance as the first argument:: public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'empty_data' => function (FormInterface $form) { + 'empty_data' => function (FormInterface $form): Blog { return new Blog($form->get('title')->getData()); }, ]); diff --git a/form/validation_group_service_resolver.rst b/form/validation_group_service_resolver.rst index 9b12bdfec55..82a6f65d6ec 100644 --- a/form/validation_group_service_resolver.rst +++ b/form/validation_group_service_resolver.rst @@ -13,14 +13,10 @@ parameter:: class ValidationGroupResolver { - private $service1; - - private $service2; - - public function __construct($service1, $service2) - { - $this->service1 = $service1; - $this->service2 = $service2; + public function __construct( + private object $service1, + private object $service2, + ) { } public function __invoke(FormInterface $form): array @@ -44,11 +40,9 @@ Then in your form, inject the resolver and set it as the ``validation_groups``:: class MyClassType extends AbstractType { - private $groupResolver; - - public function __construct(ValidationGroupResolver $groupResolver) - { - $this->groupResolver = $groupResolver; + public function __construct( + private ValidationGroupResolver $groupResolver, + ) { } // ... diff --git a/form/without_class.rst b/form/without_class.rst index b2ebdcc5482..5fec7f3a663 100644 --- a/form/without_class.rst +++ b/form/without_class.rst @@ -59,7 +59,7 @@ an array. You can also access POST values (in this case "name") directly through the request object, like so:: - $request->request->get('name'); + $request->getPayload()->get('name'); Be advised, however, that in most cases using the ``getData()`` method is a better choice, since it returns the data (usually an object) after @@ -96,12 +96,12 @@ but here's a short example:: { $builder ->add('firstName', TextType::class, [ - 'constraints' => new Length(['min' => 3]), + 'constraints' => new Length(min: 3), ]) ->add('lastName', TextType::class, [ 'constraints' => [ new NotBlank(), - new Length(['min' => 3]), + new Length(min: 3), ], ]) ; @@ -121,7 +121,7 @@ but here's a short example:: submitted data is validated using the ``Symfony\Component\Validator\Constraints\Valid`` constraint, unless you :doc:`disable validation `. -.. caution:: +.. warning:: When a form is only partially submitted (for example, in an HTTP PATCH request), only the constraints from the submitted form fields will be @@ -137,6 +137,7 @@ This can be done by setting the ``constraints`` option in the use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; + use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; @@ -149,17 +150,15 @@ This can be done by setting the ``constraints`` option in the public function configureOptions(OptionsResolver $resolver): void { - $constraints = [ - 'firstName' => new Length(['min' => 3]), - 'lastName' => [ - new NotBlank(), - new Length(['min' => 3]), - ], - ]; - $resolver->setDefaults([ 'data_class' => null, - 'constraints' => $constraints, + 'constraints' => new Collection([ + 'firstName' => new Length(min: 3), + 'lastName' => [ + new NotBlank(), + new Length(min: 3), + ], + ]), ]); } diff --git a/forms.rst b/forms.rst index 8b8a0534201..008c60a66c6 100644 --- a/forms.rst +++ b/forms.rst @@ -43,8 +43,9 @@ following ``Task`` class:: class Task { - protected $task; - protected $dueDate; + protected string $task; + + protected ?\DateTimeInterface $dueDate; public function getTask(): string { @@ -56,12 +57,12 @@ following ``Task`` class:: $this->task = $task; } - public function getDueDate(): ?\DateTime + public function getDueDate(): ?\DateTimeInterface { return $this->dueDate; } - public function setDueDate(?\DateTime $dueDate): void + public function setDueDate(?\DateTimeInterface $dueDate): void { $this->dueDate = $dueDate; } @@ -144,7 +145,7 @@ use the ``createFormBuilder()`` helper:: // creates a task object and initializes some data for this example $task = new Task(); $task->setTask('Write a blog post'); - $task->setDueDate(new \DateTime('tomorrow')); + $task->setDueDate(new \DateTimeImmutable('tomorrow')); $form = $this->createFormBuilder($task) ->add('task', TextType::class) @@ -225,7 +226,7 @@ use the ``createForm()`` helper (otherwise, use the ``create()`` method of the // creates a task object and initializes some data for this example $task = new Task(); $task->setTask('Write a blog post'); - $task->setDueDate(new \DateTime('tomorrow')); + $task->setDueDate(new \DateTimeImmutable('tomorrow')); $form = $this->createForm(TaskType::class, $task); @@ -288,20 +289,14 @@ Now that the form has been created, the next step is to render it:: $form = $this->createForm(TaskType::class, $task); - return $this->renderForm('task/new.html.twig', [ + return $this->render('task/new.html.twig', [ 'form' => $form, ]); } } -In versions prior to Symfony 5.3, controllers used the method -``$this->render('...', ['form' => $form->createView()])`` to render the form. -The ``renderForm()`` method abstracts this logic and it also sets the 422 HTTP -status code in the response automatically when the submitted form is not valid. - -.. versionadded:: 5.3 - - The ``renderForm()`` method was introduced in Symfony 5.3. +Internally, the ``render()`` method calls ``$form->createView()`` to +transform the form into a *form view* instance. Then, use some :ref:`form helper functions ` to render the form contents: @@ -367,7 +362,7 @@ can set this option to generate forms compatible with the Bootstrap 5 CSS framew // config/packages/twig.php use Symfony\Config\TwigConfig; - return static function (TwigConfig $twig) { + return static function (TwigConfig $twig): void { $twig->formThemes(['bootstrap_5_layout.html.twig']); // ... @@ -421,7 +416,7 @@ written into the form object:: return $this->redirectToRoute('task_success'); } - return $this->renderForm('task/new.html.twig', [ + return $this->render('task/new.html.twig', [ 'form' => $form, ]); } @@ -439,7 +434,12 @@ possible paths: ``task`` and ``dueDate`` properties of the ``$task`` object. Then this object is validated (validation is explained in the next section). If it is invalid, :method:`Symfony\\Component\\Form\\FormInterface::isValid` returns - ``false`` and the form is rendered again, but now with validation errors; + ``false`` and the form is rendered again, but now with validation errors. + + By passing ``$form`` to the ``render()`` method (instead of + ``$form->createView()``), the response code is automatically set to + `HTTP 422 Unprocessable Content`_. This ensures compatibility with tools + relying on the HTTP specification, like `Symfony UX Turbo`_; #. When the user submits the form with valid data, the submitted data is again written into the form, but this time :method:`Symfony\\Component\\Form\\FormInterface::isValid` @@ -482,32 +482,11 @@ to a class. You can add them either to the entity class or by using the To see the first approach - adding constraints to the entity - in action, add the validation constraints, so that the ``task`` field cannot be empty, -and the ``dueDate`` field cannot be empty, and must be a valid ``DateTime`` +and the ``dueDate`` field cannot be empty, and must be a valid ``DateTimeImmutable`` object. .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/Task.php - namespace App\Entity; - - use Symfony\Component\Validator\Constraints as Assert; - - class Task - { - /** - * @Assert\NotBlank - */ - public $task; - - /** - * @Assert\NotBlank - * @Assert\Type("\DateTime") - */ - protected $dueDate; - } - .. code-block:: php-attributes // src/Entity/Task.php @@ -518,11 +497,11 @@ object. class Task { #[Assert\NotBlank] - public $task; + public string $task; #[Assert\NotBlank] - #[Assert\Type(\DateTime::class)] - protected $dueDate; + #[Assert\Type(\DateTimeInterface::class)] + protected \DateTimeInterface $dueDate; } .. code-block:: yaml @@ -534,7 +513,7 @@ object. - NotBlank: ~ dueDate: - NotBlank: ~ - - Type: \DateTime + - Type: \DateTimeInterface .. code-block:: xml @@ -551,7 +530,7 @@ object. - \DateTime + \DateTimeInterface @@ -576,7 +555,7 @@ object. $metadata->addPropertyConstraint('dueDate', new NotBlank()); $metadata->addPropertyConstraint( 'dueDate', - new Type(\DateTime::class) + new Type(\DateTimeInterface::class) ); } } @@ -587,52 +566,6 @@ corresponding errors printed out with the form. To see the second approach - adding constraints to the form - refer to :ref:`this section `. Both approaches can be used together. -Form Validation Messages -~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 5.2 - - The ``legacy_error_messages`` option was introduced in Symfony 5.2 - -The form types have default error messages that are more clear and -user-friendly than the ones provided by the validation constraints. To enable -these new messages set the ``legacy_error_messages`` option to ``false``: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/framework.yaml - framework: - form: - legacy_error_messages: false - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // config/packages/framework.php - use Symfony\Config\FrameworkConfig; - - return static function (FrameworkConfig $framework) { - $framework->form()->legacyErrorMessages(false); - }; - Other Common Form Features -------------------------- @@ -936,7 +869,7 @@ pass ``null`` to it:: } } -.. caution:: +.. warning:: When using a specific :doc:`form validation group `, the field type guesser will still consider *all* validation constraints when @@ -1031,7 +964,6 @@ Advanced Features: /controller/upload_file /security/csrf - /form/form_dependencies /form/create_custom_field_type /form/data_transformers /form/data_mappers @@ -1083,3 +1015,5 @@ Misc.: .. _`Symfony Forms screencast series`: https://symfonycasts.com/screencast/symfony-forms .. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html +.. _`HTTP 422 Unprocessable Content`: https://www.rfc-editor.org/rfc/rfc9110.html#name-422-unprocessable-content +.. _`Symfony UX Turbo`: https://ux.symfony.com/turbo diff --git a/frontend.rst b/frontend.rst index b16c55937d4..404c9ade3a3 100644 --- a/frontend.rst +++ b/frontend.rst @@ -1,107 +1,161 @@ -Managing CSS and JavaScript -=========================== +Front-end Tools: Handling CSS & JavaScript +========================================== -.. admonition:: Screencast - :class: screencast +Symfony gives you the flexibility to choose any front-end tools you want. There +are generally two approaches: - Do you prefer video tutorials? Check out the `Webpack Encore screencast series`_. +#. :ref:`building your HTML with PHP & Twig `; +#. :ref:`building your frontend with a JavaScript framework ` like React, Vue, Svelte, etc. -Symfony ships with a pure JavaScript library - called Webpack Encore - that makes -it a joy to work with CSS and JavaScript. You can use it, use something else, or -create static CSS and JS files in your ``public/`` directory directly and -include them in your templates. +Both work great - and are discussed below. -.. _frontend-webpack-encore: +.. _frontend-twig-php: -Webpack Encore --------------- +Using PHP & Twig +---------------- -`Webpack Encore`_ is a simpler way to integrate `Webpack`_ into your application. -It *wraps* Webpack, giving you a clean & powerful API for bundling JavaScript modules, -pre-processing CSS & JS and compiling and minifying assets. Encore gives you a professional -asset system that's a *delight* to use. +Symfony comes with two powerful options to help you build a modern and fast frontend: + +* :ref:`AssetMapper ` (recommended for new projects) runs + entirely in PHP, doesn't require any build step and leverages modern web standards. + +* :ref:`Webpack Encore ` is built with `Node.js`_ + on top of `Webpack`_. + +================================ ================================== ========== + AssetMapper Encore +================================ ================================== ========== +Production Ready? yes yes +Stable? yes yes +Requirements none Node.js +Requires a build step? no yes +Works in all browsers? yes yes +Supports `Stimulus/UX`_ yes yes +Supports Sass/Tailwind :ref:`yes ` yes +Supports React, Vue, Svelte? yes :ref:`[1] ` yes +Supports TypeScript :ref:`yes ` yes +Removes comments from JavaScript no :ref:`[2] ` yes +Removes comments from CSS no :ref:`[2] ` yes :ref:`[4] ` +Versioned assets always optional +Can update 3rd party packages yes no :ref:`[3] ` +================================ ================================== ========== -Encore is inspired by `Webpacker`_ and `Mix`_, but stays in the spirit of Webpack: -using its features, concepts and naming conventions for a familiar feel. It aims -to solve the most common Webpack use cases. +.. _ux-note-1: -.. tip:: +**[1]** Using JSX (React), Vue, etc with AssetMapper is possible, but you'll +need to use their native tools for pre-compilation. Also, some features (like +Vue single-file components) cannot be compiled down to pure JavaScript that can +be executed by a browser. - Encore is made by `Symfony`_ and works *beautifully* in Symfony applications. - But it can be used in any PHP application and even with other server-side - programming languages! +.. _ux-note-2: -.. _encore-toc: +**[2]** You can install the `SensioLabs Minify Bundle`_ to minify CSS/JS code +(and remove all comments) when compiling assets with AssetMapper. -Encore Documentation --------------------- +.. _ux-note-3: -Getting Started -............... +**[3]** If you use ``npm``, there are update checkers available (e.g. ``npm-check``). -* :doc:`Installation ` -* :doc:`Using Webpack Encore ` +.. _ux-note-4: -Adding more Features -.................... +**[4]** CSS comments can be removed using `CssMinimizerPlugin`_, which is included +in Webpack Encore and configurable via ``Encore.configureCssMinimizerPlugin()``. -* :doc:`CSS Preprocessors: Sass, LESS, etc. ` -* :doc:`PostCSS and autoprefixing ` -* :doc:`Enabling React.js ` -* :doc:`Enabling Vue.js (vue-loader) ` -* :doc:`/frontend/encore/copy-files` -* :doc:`Configuring Babel ` -* :doc:`Source maps ` -* :doc:`Enabling TypeScript (ts-loader) ` +.. _frontend-asset-mapper: -Optimizing -.......... +AssetMapper (Recommended) +~~~~~~~~~~~~~~~~~~~~~~~~~ -* :doc:`Versioning (and the entrypoints.json/manifest.json files) ` -* :doc:`Using a CDN ` -* :doc:`/frontend/encore/code-splitting` -* :doc:`/frontend/encore/split-chunks` -* :doc:`/frontend/encore/url-loader` +.. screencast:: -Guides -...... + Do you prefer video tutorials? Check out the `AssetMapper screencast series`_. -* :doc:`Using Bootstrap CSS & JS ` -* :doc:`jQuery and Legacy Applications ` -* :doc:`Passing Information from Twig to JavaScript ` -* :doc:`webpack-dev-server and Hot Module Replacement (HMR) ` -* :doc:`Adding custom loaders & plugins ` -* :doc:`Advanced Webpack Configuration ` -* :doc:`Using Encore in a Virtual Machine ` +AssetMapper is the recommended system for handling your assets. It runs entirely +in PHP with no complex build step or dependencies. It does this by leveraging +the ``importmap`` feature of your browser, which is available in all browsers thanks +to a polyfill. -Issues & Questions -.................. +:doc:`Read the AssetMapper Documentation ` -* :doc:`FAQ & Common Issues ` +.. _frontend-webpack-encore: + +Webpack Encore +~~~~~~~~~~~~~~ + +.. screencast:: -Full API -........ + Do you prefer video tutorials? Check out the `Webpack Encore screencast series`_. + +`Webpack Encore`_ is a simpler way to integrate `Webpack`_ into your application. +It wraps Webpack, giving you a clean & powerful API for bundling JavaScript modules, +pre-processing CSS & JS and compiling and minifying assets. -* `Full API`_ +:doc:`Read the Encore Documentation ` -Symfony UX Components ---------------------- +Switch from AssetMapper +^^^^^^^^^^^^^^^^^^^^^^^ -.. include:: /frontend/_ux-libraries.rst.inc +By default, new Symfony webapp projects (created with ``symfony new --webapp myapp``) +use AssetMapper. If you still need to use Webpack Encore, use the following steps to +switch. This is best done on a new project and provides the same features (Turbo/Stimulus) +as the default webapp. + +.. code-block:: terminal + + # Remove AssetMapper & Turbo/Stimulus temporarily + $ composer remove symfony/ux-turbo symfony/asset-mapper symfony/stimulus-bundle + + # Add Webpack Encore & Turbo/Stimulus back + $ composer require symfony/webpack-encore-bundle symfony/ux-turbo symfony/stimulus-bundle + + # Install & Build Assets + $ npm install + $ npm run dev + +Stimulus & Symfony UX Components +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once you've installed AssetMapper or Webpack Encore, it's time to start building your +front-end. You can write your JavaScript however you want, but we recommend +using `Stimulus`_, `Turbo`_ and a set of tools called `Symfony UX`_. + +To learn about Stimulus & the UX Components, see +the `StimulusBundle Documentation`_ + +.. _frontend-js: + +Using a Front-end Framework (React, Vue, Svelte, etc) +----------------------------------------------------- + +.. screencast:: + + Do you prefer video tutorials? Check out the `API Platform screencast series`_. + +If you want to use a front-end framework (Next.js, React, Vue, Svelte, etc), +we recommend using their native tools and using Symfony as a pure API. A wonderful +tool to do that is `API Platform`_. Their standard distribution comes with a +Symfony-powered API backend, frontend scaffolding in Next.js (other frameworks +are also supported) and a React admin interface. It comes fully Dockerized and even +contains a web server. Other Front-End Articles ------------------------ -.. toctree:: - :maxdepth: 1 - :glob: - - frontend/* +* :doc:`/frontend/create_ux_bundle` +* :doc:`/frontend/custom_version_strategy` +* :doc:`/frontend/server-data` .. _`Webpack Encore`: https://www.npmjs.com/package/@symfony/webpack-encore .. _`Webpack`: https://webpack.js.org/ -.. _`Webpacker`: https://github.com/rails/webpacker -.. _`Mix`: https://laravel.com/docs/mix -.. _`Symfony`: https://symfony.com/ -.. _`Full API`: https://github.com/symfony/webpack-encore/blob/master/index.js +.. _`Node.js`: https://nodejs.org/ .. _`Webpack Encore screencast series`: https://symfonycasts.com/screencast/webpack-encore +.. _`StimulusBundle Documentation`: https://symfony.com/bundles/StimulusBundle/current/index.html +.. _`Stimulus/UX`: https://symfony.com/bundles/StimulusBundle/current/index.html +.. _`Stimulus`: https://stimulus.hotwired.dev/ +.. _`Turbo`: https://turbo.hotwired.dev/ +.. _`Symfony UX`: https://ux.symfony.com +.. _`API Platform`: https://api-platform.com/ +.. _`SensioLabs Minify Bundle`: https://github.com/sensiolabs/minify-bundle +.. _`AssetMapper screencast series`: https://symfonycasts.com/screencast/asset-mapper +.. _`API Platform screencast series`: https://symfonycasts.com/screencast/api-platform +.. _`CssMinimizerPlugin`: https://webpack.js.org/plugins/css-minimizer-webpack-plugin diff --git a/frontend/_ux-libraries.rst.inc b/frontend/_ux-libraries.rst.inc deleted file mode 100644 index a9d8f15acde..00000000000 --- a/frontend/_ux-libraries.rst.inc +++ /dev/null @@ -1,44 +0,0 @@ -* `ux-autocomplete`_: Transform ``EntityType``, ``ChoiceType`` or *any* - ```` or ````. The label displayed in the ``