diff --git a/.doctor-rst.yaml b/.doctor-rst.yaml index 4f07d84cd25..5ea86e6abc4 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,7 +19,7 @@ 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::' @@ -74,35 +70,41 @@ 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.7.2' # Doctrine + - '.. 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";' @@ -112,4 +114,4 @@ 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)' diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2d35b7df806..fcbdbe0477b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -26,7 +26,7 @@ jobs: - name: "Set-up PHP" uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.2 coverage: none tools: "composer:v2" @@ -93,7 +93,7 @@ jobs: - name: Set-up PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.2 coverage: none - name: Fetch branch from where the PR started diff --git a/_build/build.php b/_build/build.php index be2fb062a77..5298abe779a 100755 --- a/_build/build.php +++ b/_build/build.php @@ -20,7 +20,7 @@ $outputDir = __DIR__.'/output'; $buildConfig = (new BuildConfig()) - ->setSymfonyVersion('5.4') + ->setSymfonyVersion('7.1') ->setContentDir(__DIR__.'/..') ->setOutputDir($outputDir) ->setImagesDir(__DIR__.'/output/_images') 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..8f31032e7a5 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 @@ -525,8 +526,10 @@ /components https://symfony.com/components /components/index https://symfony.com/components /serializer/normalizers /components/serializer#normalizers +/components/serializer#component-serializer-attributes-groups-annotations /components/serializer#component-serializer-attributes-groups-attributes /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,15 @@ /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 diff --git a/_build/spelling_word_list.txt b/_build/spelling_word_list.txt index 70240ceb6d1..fa05ce9430e 100644 --- a/_build/spelling_word_list.txt +++ b/_build/spelling_word_list.txt @@ -3,7 +3,6 @@ Akamai analytics Ansi Ansible -Assetic async authenticator authenticators 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/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/_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..ba3a2209999 100644 --- a/bundles.rst +++ b/bundles.rst @@ -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 { } +.. caution:: + + 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: + +.. caution:: + + 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..5996bcbe43d 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 ----- @@ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -444,7 +444,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 +565,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..347f63b7af5 100644 --- a/bundles/extension.rst +++ b/bundles/extension.rst @@ -6,12 +6,73 @@ 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\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 +81,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 +95,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 +111,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 +131,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 +145,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 +177,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\\**', // ... ]); diff --git a/bundles/override.rst b/bundles/override.rst index 1e4926a1c76..36aea69b231 100644 --- a/bundles/override.rst +++ b/bundles/override.rst @@ -12,12 +12,12 @@ 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:: @@ -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..7264585f233 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'; @@ -647,7 +618,7 @@ 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) @@ -701,7 +672,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 +724,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): -.. versionadded:: 5.1 +.. 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 - The :class:`Symfony\\Component\\Cache\\Marshaller\\SodiumMarshaller` - class was introduced in Symfony 5.1. + # invalidate tag1 & tag2 from cache.app pool + $ php bin/console cache:pool:invalidate-tags tag1 tag2 --pool=cache.app + + # invalidate tag1 & tag2 from cache1 & cache2 pools + $ php bin/console cache:pool:invalidate-tags tag1 tag2 -p cache1 -p cache2 + +Encrypting the Cache +-------------------- To encrypt the cache using ``libsodium``, you can use the :class:`Symfony\\Component\\Cache\\Marshaller\\SodiumMarshaller`. @@ -848,10 +842,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 +884,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..bcb8f7b3c8e 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 ~~~~~~~~~~~~~~~~ @@ -174,11 +181,7 @@ provides access to the form properties (e.g. ``$form->getUri()``, Custom Header Handling ~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.2 - - The ``getHeaders()`` method was introduced in Symfony 5.2. - -The optional HTTP headers passed to the ``request()`` method follows the FastCGI +The optional HTTP headers passed to the ``request()`` method follow the FastCGI request format (uppercase, underscores instead of dashes and prefixed with ``HTTP_``). Before saving those headers to the request, they are lower-cased, with ``HTTP_`` stripped, and underscores converted into dashes. @@ -386,6 +389,16 @@ the requests you made. To do so, call the ``getResponse()`` method of the $browser->request('GET', 'https://foo.com'); $response = $browser->getResponse(); +If you're making requests that result in a JSON response, you may use the +``toArray()`` method to turn the JSON document into a PHP array without having +to call ``json_decode()`` explicitly:: + + $browser = new HttpBrowser(HttpClient::create()); + + $browser->request('GET', 'https://api.foo.com'); + $response = $browser->getResponse()->toArray(); + // $response is a PHP array of the decoded JSON contents + Learn more ---------- diff --git a/components/cache.rst b/components/cache.rst index 857282eb1d0..f5a76f2119d 100644 --- a/components/cache.rst +++ b/components/cache.rst @@ -11,14 +11,8 @@ The Cache Component .. tip:: - The component also contains adapters to convert between PSR-6, PSR-16 and - Doctrine caches. See :doc:`/components/cache/psr6_psr16_adapters` and - :doc:`/components/cache/adapters/doctrine_adapter`. - - .. deprecated:: 5.4 - - Support for Doctrine Cache was deprecated in Symfony 5.4 - and it will be removed in Symfony 6.0. + The component also contains adapters to convert between PSR-6 and PSR-16. + See :doc:`/components/cache/psr6_psr16_adapters`. Installation ------------ @@ -71,7 +65,7 @@ generate and return the value:: use Symfony\Contracts\Cache\ItemInterface; // The callable will only be executed on a cache miss. - $value = $cache->get('my_cache_key', function (ItemInterface $item) { + $value = $cache->get('my_cache_key', function (ItemInterface $item): string { $item->expiresAfter(3600); // ... do some HTTP request or heavy computations @@ -123,7 +117,7 @@ recompute:: use Symfony\Contracts\Cache\ItemInterface; $beta = 1.0; - $value = $cache->get('my_cache_key', function (ItemInterface $item) { + $value = $cache->get('my_cache_key', function (ItemInterface $item): string { $item->expiresAfter(3600); $item->tag(['tag_0', 'tag_1']); @@ -160,7 +154,7 @@ concepts: **Adapter** It implements the actual caching mechanism to store the information in the filesystem, in a database, etc. The component provides several ready to use - adapters for common caching backends (Redis, APCu, Doctrine, PDO, etc.) + adapters for common caching backends (Redis, APCu, PDO, etc.) Basic Usage (PSR-6) ------------------- diff --git a/components/cache/adapters/array_cache_adapter.rst b/components/cache/adapters/array_cache_adapter.rst index 1d8cd87269a..f903771e468 100644 --- a/components/cache/adapters/array_cache_adapter.rst +++ b/components/cache/adapters/array_cache_adapter.rst @@ -26,7 +26,3 @@ method:: // is reached, cache follows the LRU model (least recently used items are deleted) $maxItems = 0 ); - -.. versionadded:: 5.1 - - The ``maxLifetime`` and ``maxItems`` options were introduced in Symfony 5.1. diff --git a/components/cache/adapters/couchbasebucket_adapter.rst b/components/cache/adapters/couchbasebucket_adapter.rst index 172a8fe0f19..aaf400319f4 100644 --- a/components/cache/adapters/couchbasebucket_adapter.rst +++ b/components/cache/adapters/couchbasebucket_adapter.rst @@ -1,9 +1,11 @@ Couchbase Bucket Cache Adapter ============================== -.. versionadded:: 5.1 +.. deprecated:: 7.1 - The Couchbase Bucket adapter was introduced in Symfony 5.1. + The ``CouchbaseBucketAdapter`` is deprecated since Symfony 7.1, use the + :doc:`CouchbaseCollectionAdapter ` + instead. This adapter stores the values in-memory using one (or more) `Couchbase server`_ instances. Unlike the :doc:`APCu adapter `, and similarly to the diff --git a/components/cache/adapters/couchbasecollection_adapter.rst b/components/cache/adapters/couchbasecollection_adapter.rst index 296b7065f1d..25640a20b0f 100644 --- a/components/cache/adapters/couchbasecollection_adapter.rst +++ b/components/cache/adapters/couchbasecollection_adapter.rst @@ -1,10 +1,6 @@ Couchbase Collection Cache Adapter ================================== -.. versionadded:: 5.4 - - The Couchbase Collection adapter was introduced in Symfony 5.4. - This adapter stores the values in-memory using one (or more) `Couchbase server`_ instances. Unlike the :doc:`APCu adapter `, and similarly to the :doc:`Memcached adapter `, it is not limited to the current server's diff --git a/components/cache/adapters/doctrine_adapter.rst b/components/cache/adapters/doctrine_adapter.rst deleted file mode 100644 index b345d310029..00000000000 --- a/components/cache/adapters/doctrine_adapter.rst +++ /dev/null @@ -1,41 +0,0 @@ -Doctrine Cache Adapter -====================== - -.. deprecated:: 5.4 - - The ``DoctrineAdapter`` and ``DoctrineProvider`` classes were deprecated in Symfony 5.4 - and it will be removed in Symfony 6.0. - -This adapter wraps any class extending the `Doctrine Cache`_ abstract provider, allowing -you to use these providers in your application as if they were Symfony Cache adapters. - -This adapter expects a ``\Doctrine\Common\Cache\CacheProvider`` instance as its first -parameter, and optionally a namespace and default cache lifetime as its second and -third parameters:: - - use Doctrine\Common\Cache\CacheProvider; - use Doctrine\Common\Cache\SQLite3Cache; - use Symfony\Component\Cache\Adapter\DoctrineAdapter; - - $provider = new SQLite3Cache(new \SQLite3(__DIR__.'/cache/data.sqlite'), 'youTableName'); - - $cache = new DoctrineAdapter( - - // a cache provider instance - CacheProvider $provider, - - // a string prefixed to the keys of the items stored in this cache - $namespace = '', - - // the default lifetime (in seconds) for cache items that do not define their - // own lifetime, with a value 0 causing items to be stored indefinitely (i.e. - // until the database table is truncated or its rows are otherwise deleted) - $defaultLifetime = 0 - ); - -.. tip:: - - A :class:`Symfony\\Component\\Cache\\DoctrineProvider` class is also provided by the - component to use any PSR6-compatible implementations with Doctrine-compatible classes. - -.. _`Doctrine Cache`: https://github.com/doctrine/cache diff --git a/components/cache/adapters/doctrine_dbal_adapter.rst b/components/cache/adapters/doctrine_dbal_adapter.rst index fc04410bffc..68732ddd3fa 100644 --- a/components/cache/adapters/doctrine_dbal_adapter.rst +++ b/components/cache/adapters/doctrine_dbal_adapter.rst @@ -39,5 +39,22 @@ optional arguments:: necessary to detect the database engine and version without opening the connection. +The adapter uses SQL syntax that is optimized for database server that it is connected to. +The following database servers are known to be compatible: + +* MySQL 5.7 and newer +* MariaDB 10.2 and newer +* Oracle 10g and newer +* SQL Server 2012 and newer +* SQLite 3.24 or later +* PostgreSQL 9.5 or later + +.. note:: + + Newer releases of Doctrine DBAL might increase these minimal versions. Check + the manual page on `Doctrine DBAL Platforms`_ if your database server is + compatible with the installed Doctrine DBAL version. + .. _`Doctrine DBAL Connection`: https://github.com/doctrine/dbal/blob/master/src/Connection.php -.. _`Doctrine DBAL URL`: https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +.. _`Doctrine DBAL URL`: https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/configuration.html#connecting-using-a-url +.. _`Doctrine DBAL Platforms`: https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/platforms.html diff --git a/components/cache/adapters/pdo_adapter.rst b/components/cache/adapters/pdo_adapter.rst index 9cfbfd7bdfa..3cdeb87427a 100644 --- a/components/cache/adapters/pdo_adapter.rst +++ b/components/cache/adapters/pdo_adapter.rst @@ -38,13 +38,6 @@ You can also create this table explicitly by calling the :method:`Symfony\\Component\\Cache\\Adapter\\PdoAdapter::createTable` method in your code. -.. deprecated:: 5.4 - - Using :class:`Symfony\\Component\\Cache\\Adapter\\PdoAdapter` with a - ``Doctrine\DBAL\Connection`` or a DBAL URL is deprecated since Symfony 5.4 - and will be removed in Symfony 6.0. - Use :class:`Symfony\\Component\\Cache\\Adapter\\DoctrineDbalAdapter` instead. - .. tip:: When passed a `Data Source Name (DSN)`_ string (instead of a database connection diff --git a/components/cache/adapters/redis_adapter.rst b/components/cache/adapters/redis_adapter.rst index 2b00058c6bd..719d6056f19 100644 --- a/components/cache/adapters/redis_adapter.rst +++ b/components/cache/adapters/redis_adapter.rst @@ -19,9 +19,9 @@ to utilize a cluster of servers to provide redundancy and/or fail-over is also a **Requirements:** At least one `Redis server`_ must be installed and running to use this adapter. Additionally, this adapter requires a compatible extension or library that implements - ``\Redis``, ``\RedisArray``, ``RedisCluster``, or ``\Predis``. + ``\Redis``, ``\RedisArray``, ``RedisCluster``, ``\Relay\Relay`` or ``\Predis``. -This adapter expects a `Redis`_, `RedisArray`_, `RedisCluster`_, or `Predis`_ instance to be +This adapter expects a `Redis`_, `RedisArray`_, `RedisCluster`_, `Relay`_ or `Predis`_ instance to be passed as the first parameter. A namespace and default cache lifetime can optionally be passed as the second and third parameters:: @@ -55,18 +55,24 @@ helper method allows creating and configuring the Redis client class instance us 'redis://localhost' ); -The DSN can specify either an IP/host (and an optional port) or a socket path, as -well as a database index. To enable TLS for connections, the scheme ``redis`` must -be replaced by ``rediss`` (the second ``s`` means "secure"). +The DSN can specify either an IP/host (and an optional port) or a socket path, as well as a +password and a database index. To enable TLS for connections, the scheme ``redis`` must be +replaced by ``rediss`` (the second ``s`` means "secure"). .. note:: - A `Data Source Name (DSN)`_ for this adapter must use the following format. + A `Data Source Name (DSN)`_ for this adapter must use either one of the following formats. .. code-block:: text redis[s]://[pass@][ip|host|socket[:port]][/db-index] + .. code-block:: text + + redis[s]:[[user]:pass@]?[ip|host|socket[:port]][¶ms] + + Values for placeholders ``[user]``, ``[:port]``, ``[/db-index]`` and ``[¶ms]`` are optional. + Below are common examples of valid DSNs showing a combination of available values:: use Symfony\Component\Cache\Adapter\RedisAdapter; @@ -83,8 +89,11 @@ Below are common examples of valid DSNs showing a combination of available value // socket "/var/run/redis.sock" and auth "bad-pass" RedisAdapter::createConnection('redis://bad-pass@/var/run/redis.sock'); - // a single DSN can define multiple servers using the following syntax: - // host[hostname-or-IP:port] (where port is optional). Sockets must include a trailing ':' + // host "redis1" (docker container) with alternate DSN syntax and selecting database index "3" + RedisAdapter::createConnection('redis:?host[redis1:6379]&dbindex=3'); + + // providing credentials with alternate DSN syntax + RedisAdapter::createConnection('redis:default:verysecurepassword@?host[redis1:6379]&dbindex=3'); // a single DSN can also define multiple servers RedisAdapter::createConnection( @@ -99,6 +108,16 @@ parameter to set the name of your service group:: 'redis:?host[redis1:26379]&host[redis2:26379]&host[redis3:26379]&redis_sentinel=mymaster' ); + // providing credentials + RedisAdapter::createConnection( + 'redis:default:verysecurepassword@?host[redis1:26379]&host[redis2:26379]&host[redis3:26379]&redis_sentinel=mymaster' + ); + + // providing credentials and selecting database index "3" + RedisAdapter::createConnection( + 'redis:default:verysecurepassword@?host[redis1:26379]&host[redis2:26379]&host[redis3:26379]&redis_sentinel=mymaster&dbindex=3' + ); + .. note:: See the :class:`Symfony\\Component\\Cache\\Traits\\RedisTrait` for more options @@ -141,9 +160,9 @@ Available Options ~~~~~~~~~~~~~~~~~ ``class`` (type: ``string``, default: ``null``) - Specifies the connection library to return, either ``\Redis`` or ``\Predis\Client``. - If none is specified, it will return ``\Redis`` if the ``redis`` extension is - available, and ``\Predis\Client`` otherwise. Explicitly set this to ``\Predis\Client`` for Sentinel if you are + Specifies the connection library to return, either ``\Redis``, ``\Relay\Relay`` or ``\Predis\Client``. + If none is specified, fallback value is in following order, depending which one is available first: + ``\Redis``, ``\Relay\Relay``, ``\Predis\Client``. Explicitly set this to ``\Predis\Client`` for Sentinel if you are running into issues when retrieving master information. ``persistent`` (type: ``int``, default: ``0``) @@ -181,6 +200,9 @@ Available Options ``redis_sentinel`` (type: ``string``, default: ``null``) Specifies the master name connected to the sentinels. +``sentinel_master`` (type: ``string``, default: ``null``) + Alias of ``redis_sentinel`` option. + ``dbindex`` (type: ``int``, default: ``0``) Specifies the database index to select. @@ -192,6 +214,11 @@ Available Options ``ssl`` (type: ``array``, default: ``null``) SSL context options. See `php.net/context.ssl`_ for more information. +.. versionadded:: 7.1 + + The option `sentinel_master` as an alias for `redis_sentinel` was introduced + in Symfony 7.1. + .. note:: When using the `Predis`_ library some additional Predis-specific options are available. @@ -199,19 +226,8 @@ Available Options .. _redis-tag-aware-adapter: -Working with Tags ------------------ - -In order to use tag-based invalidation, you can wrap your adapter in :class:`Symfony\\Component\\Cache\\Adapter\\TagAwareAdapter`, but when Redis is used as backend, it's often more interesting to use the dedicated :class:`Symfony\\Component\\Cache\\Adapter\\RedisTagAwareAdapter`. Since tag invalidation logic is implemented in Redis itself, this adapter offers better performance when using tag-based invalidation:: - - use Symfony\Component\Cache\Adapter\RedisAdapter; - use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; - - $client = RedisAdapter::createConnection('redis://localhost'); - $cache = new RedisTagAwareAdapter($client); - Configuring Redis -~~~~~~~~~~~~~~~~~ +----------------- When using Redis as cache, you should configure the ``maxmemory`` and ``maxmemory-policy`` settings. By setting ``maxmemory``, you limit how much memory Redis is allowed to consume. @@ -226,6 +242,28 @@ try to add data when no memory is available. An example setting could look as fo maxmemory 100mb maxmemory-policy allkeys-lru +Working with Tags +----------------- + +In order to use tag-based invalidation, you can wrap your adapter in +:class:`Symfony\\Component\\Cache\\Adapter\\TagAwareAdapter`. However, when Redis +is used as backend, it's often more interesting to use the dedicated +:class:`Symfony\\Component\\Cache\\Adapter\\RedisTagAwareAdapter`. Since tag +invalidation logic is implemented in Redis itself, this adapter offers better +performance when using tag-based invalidation:: + + use Symfony\Component\Cache\Adapter\RedisAdapter; + use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; + + $client = RedisAdapter::createConnection('redis://localhost'); + $cache = new RedisTagAwareAdapter($client); + +.. note:: + + When using RedisTagAwareAdapter, in order to maintain relationships between + tags and cache items, you have to use either ``noeviction`` or ``volatile-*`` + in the Redis ``maxmemory-policy`` eviction policy. + Read more about this topic in the official `Redis LRU Cache Documentation`_. .. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name @@ -233,6 +271,7 @@ Read more about this topic in the official `Redis LRU Cache Documentation`_. .. _`Redis`: https://github.com/phpredis/phpredis .. _`RedisArray`: https://github.com/phpredis/phpredis/blob/develop/arrays.md .. _`RedisCluster`: https://github.com/phpredis/phpredis/blob/develop/cluster.md +.. _`Relay`: https://relay.so/ .. _`Predis`: https://packagist.org/packages/predis/predis .. _`Predis Connection Parameters`: https://github.com/nrk/predis/wiki/Connection-Parameters#list-of-connection-parameters .. _`TCP-keepalive`: https://redis.io/topics/clients#tcp-keepalive diff --git a/components/cache/cache_invalidation.rst b/components/cache/cache_invalidation.rst index 1005d2d09a7..da88ea6273e 100644 --- a/components/cache/cache_invalidation.rst +++ b/components/cache/cache_invalidation.rst @@ -24,7 +24,7 @@ To attach tags to cached items, you need to use the :method:`Symfony\\Contracts\\Cache\\ItemInterface::tag` method that is implemented by cache items:: - $item = $cache->get('cache_key', function (ItemInterface $item) { + $item = $cache->get('cache_key', function (ItemInterface $item): string { // [...] // add one or more tags $item->tag('tag_1'); diff --git a/components/cache/cache_items.rst b/components/cache/cache_items.rst index 475a9c59367..e958125c69d 100644 --- a/components/cache/cache_items.rst +++ b/components/cache/cache_items.rst @@ -26,7 +26,7 @@ The only way to create cache items is via cache pools. When using the Cache Contracts, they are passed as arguments to the recomputation callback:: // $cache pool object was created before - $productsCount = $cache->get('stats.products_count', function (ItemInterface $item) { + $productsCount = $cache->get('stats.products_count', function (ItemInterface $item): string { // [...] }); diff --git a/components/cache/cache_pools.rst b/components/cache/cache_pools.rst index 3a0897defcf..e50c2b67633 100644 --- a/components/cache/cache_pools.rst +++ b/components/cache/cache_pools.rst @@ -37,7 +37,7 @@ and deleting cache items using only two methods and a callback:: $cache = new FilesystemAdapter(); // The callable will only be executed on a cache miss. - $value = $cache->get('my_cache_key', function (ItemInterface $item) { + $value = $cache->get('my_cache_key', function (ItemInterface $item): string { $item->expiresAfter(3600); // ... do some HTTP request or heavy computations diff --git a/components/clock.rst b/components/clock.rst new file mode 100644 index 00000000000..cdbbdd56e6b --- /dev/null +++ b/components/clock.rst @@ -0,0 +1,350 @@ +The Clock Component +=================== + +The Clock component decouples applications from the system clock. This allows +you to fix time to improve testability of time-sensitive logic. + +The component provides a ``ClockInterface`` with the following implementations +for different use cases: + +:class:`Symfony\\Component\\Clock\\NativeClock` + Provides a way to interact with the system clock, this is the same as doing + ``new \DateTimeImmutable()``. +:class:`Symfony\\Component\\Clock\\MockClock` + Commonly used in tests as a replacement for the ``NativeClock`` to be able + to freeze and change the current time using either ``sleep()`` or ``modify()``. +:class:`Symfony\\Component\\Clock\\MonotonicClock` + Relies on ``hrtime()`` and provides a high resolution, monotonic clock, + when you need a precise stopwatch. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/clock + +.. include:: /components/require_autoload.rst.inc + +.. _clock_usage: + +Usage +----- + +The :class:`Symfony\\Component\\Clock\\Clock` class returns the current time and +allows to use any `PSR-20`_ compatible implementation as a global clock in your +application:: + + use Symfony\Component\Clock\Clock; + use Symfony\Component\Clock\MockClock; + + // by default, Clock uses the NativeClock implementation, but you can change + // this by setting any other implementation + Clock::set(new MockClock()); + + // Then, you can get the clock instance + $clock = Clock::get(); + + // Additionally, you can set a timezone + $clock->withTimeZone('Europe/Paris'); + + // From here, you can get the current time + $now = $clock->now(); + + // And sleep for any number of seconds + $clock->sleep(2.5); + +The Clock component also provides the ``now()`` function:: + + use function Symfony\Component\Clock\now; + + // Get the current time as a DatePoint instance + $now = now(); + +The ``now()`` function takes an optional ``modifier`` argument +which will be applied to the current time:: + + $later = now('+3 hours'); + + $yesterday = now('-1 day'); + +You can use any string `accepted by the DateTime constructor`_. + +Later on this page you can learn how to use this clock in your services and tests. +When using the Clock component, you manipulate +:class:`Symfony\\Component\\Clock\\DatePoint` instances. You can learn more +about it in :ref:`the dedicated section `. + +Available Clocks Implementations +-------------------------------- + +The Clock component provides some ready-to-use implementations of the +:class:`Symfony\\Component\\Clock\\ClockInterface`, which you can use +as global clocks in your application depending on your needs. + +NativeClock +~~~~~~~~~~~ + +A clock service replaces creating a new ``DateTime`` or +``DateTimeImmutable`` object for the current time. Instead, you inject the +``ClockInterface`` and call ``now()``. By default, your application will likely +use a ``NativeClock``, which always returns the current system time. In tests it is replaced with a ``MockClock``. + +The following example introduces a service utilizing the Clock component to +determine the current time:: + + use Symfony\Component\Clock\ClockInterface; + + class ExpirationChecker + { + public function __construct( + private ClockInterface $clock + ) {} + + public function isExpired(DateTimeInterface $validUntil): bool + { + return $this->clock->now() > $validUntil; + } + } + +MockClock +~~~~~~~~~ + +The ``MockClock`` is instantiated with a time and does not move forward on its own. The time is +fixed until ``sleep()`` or ``modify()`` are called. This gives you full control over what your code +assumes is the current time. + +When writing a test for this service, you can check both cases where something +is expired or not, by modifying the clock's time:: + + use PHPUnit\Framework\TestCase; + use Symfony\Component\Clock\MockClock; + + class ExpirationCheckerTest extends TestCase + { + public function testIsExpired(): void + { + $clock = new MockClock('2022-11-16 15:20:00'); + $expirationChecker = new ExpirationChecker($clock); + $validUntil = new DateTimeImmutable('2022-11-16 15:25:00'); + + // $validUntil is in the future, so it is not expired + static::assertFalse($expirationChecker->isExpired($validUntil)); + + // Clock sleeps for 10 minutes, so now is '2022-11-16 15:30:00' + $clock->sleep(600); // Instantly changes time as if we waited for 10 minutes (600 seconds) + + // modify the clock, accepts all formats supported by DateTimeImmutable::modify() + static::assertTrue($expirationChecker->isExpired($validUntil)); + + $clock->modify('2022-11-16 15:00:00'); + + // $validUntil is in the future again, so it is no longer expired + static::assertFalse($expirationChecker->isExpired($validUntil)); + } + } + +Monotonic Clock +~~~~~~~~~~~~~~~ + +The ``MonotonicClock`` allows you to implement a precise stopwatch; depending on +the system up to nanosecond precision. It can be used to measure the elapsed +time between two calls without being affected by inconsistencies sometimes introduced +by the system clock, e.g. by updating it. Instead, it consistently increases time, +making it especially useful for measuring performance. + +.. _clock_use-inside-a-service: + +Using a Clock inside a Service +------------------------------ + +Using the Clock component in your services to retrieve the current time makes +them easier to test. For example, by using the ``MockClock`` implementation as +the default one during tests, you will have full control to set the "current time" +to any arbitrary date/time. + +In order to use this component in your services, make their classes use the +:class:`Symfony\\Component\\Clock\\ClockAwareTrait`. Thanks to +:ref:`service autoconfiguration `, the ``setClock()`` method +of the trait will automatically be called by the service container. + +You can now call the ``$this->now()`` method to get the current time:: + + namespace App\TimeUtils; + + use Symfony\Component\Clock\ClockAwareTrait; + + class MonthSensitive + { + use ClockAwareTrait; + + public function isWinterMonth(): bool + { + $now = $this->now(); + + return match ($now->format('F')) { + 'December', 'January', 'February', 'March' => true, + default => false, + }; + } + } + +Thanks to the ``ClockAwareTrait``, and by using the ``MockClock`` implementation, +you can set the current time arbitrarily without having to change your service code. +This will help you test every case of your method without the need of actually +being in a month or another. + +.. _clock_date-point: + +The ``DatePoint`` Class +----------------------- + +The Clock component uses a special :class:`Symfony\\Component\\Clock\\DatePoint` +class. This is a small wrapper on top of PHP's :phpclass:`DateTimeImmutable`. +You can use it seamlessly everywhere a :phpclass:`DateTimeImmutable` or +:phpclass:`DateTimeInterface` is expected. The ``DatePoint`` object fetches the +date and time from the :class:`Symfony\\Component\\Clock\\Clock` class. This means +that if you did any changes to the clock as stated in the +:ref:`usage section `, it will be reflected when creating a new +``DatePoint``. You can also create a new ``DatePoint`` instance directly, for +instance when using it as a default value:: + + use Symfony\Component\Clock\DatePoint; + + class Post + { + public function __construct( + // ... + private \DateTimeImmutable $createdAt = new DatePoint(), + ) { + } + } + +The constructor also allows setting a timezone or custom referenced date:: + + // you can specify a timezone + $withTimezone = new DatePoint(timezone: new \DateTimezone('UTC')); + + // you can also create a DatePoint from a reference date + $referenceDate = new \DateTimeImmutable(); + $relativeDate = new DatePoint('+1month', reference: $referenceDate); + +The ``DatePoint`` class also provides a named constructor to create dates from +timestamps:: + + $dateOfFirstCommitToSymfonyProject = DatePoint::createFromTimestamp(1129645656); + // equivalent to: + // $dateOfFirstCommitToSymfonyProject = (new \DateTimeImmutable())->setTimestamp(1129645656); + + // negative timestamps (for dates before January 1, 1970) and float timestamps + // (for high precision sub-second datetimes) are also supported + $dateOfFirstMoonLanding = DatePoint::createFromTimestamp(-14182940); + +.. versionadded:: 7.1 + + The ``createFromTimestamp()`` method was introduced in Symfony 7.1. + +.. note:: + + In addition ``DatePoint`` offers stricter return types and provides consistent + error handling across versions of PHP, thanks to polyfilling `PHP 8.3's behavior`_ + on the topic. + +``DatePoint`` also allows to set and get the microsecond part of the date and time:: + + $datePoint = new DatePoint(); + $datePoint->setMicrosecond(345); + $microseconds = $datePoint->getMicrosecond(); + +.. note:: + + This feature polyfills PHP 8.4's behavior on the topic, as microseconds manipulation + is not available in previous versions of PHP. + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\Clock\\DatePoint::setMicrosecond` and + :method:`Symfony\\Component\\Clock\\DatePoint::getMicrosecond` methods were + introduced in Symfony 7.1. + +.. _clock_writing-tests: + +Writing Time-Sensitive Tests +---------------------------- + +The Clock component provides another trait, called :class:`Symfony\\Component\\Clock\\Test\\ClockSensitiveTrait`, +to help you write time-sensitive tests. This trait provides methods to freeze +time and restore the global clock after each test. + +Use the ``ClockSensitiveTrait::mockTime()`` method to interact with the mocked +clock in your tests. This method accepts different types as its only argument: + +* A string, which can be a date to set the clock at (e.g. ``1996-07-01``) or an + interval to modify the clock (e.g. ``+2 days``); +* A ``DateTimeImmutable`` to set the clock at; +* A boolean, to freeze or restore the global clock. + +Let's say you want to test the method ``MonthSensitive::isWinterMonth()`` of the +above example. This is how you can write that test:: + + namespace App\Tests\TimeUtils; + + use App\TimeUtils\MonthSensitive; + use PHPUnit\Framework\TestCase; + use Symfony\Component\Clock\Test\ClockSensitiveTrait; + + class MonthSensitiveTest extends TestCase + { + use ClockSensitiveTrait; + + public function testIsWinterMonth(): void + { + $clock = static::mockTime(new \DateTimeImmutable('2022-03-02')); + + $monthSensitive = new MonthSensitive(); + $monthSensitive->setClock($clock); + + $this->assertTrue($monthSensitive->isWinterMonth()); + } + + public function testIsNotWinterMonth(): void + { + $clock = static::mockTime(new \DateTimeImmutable('2023-06-02')); + + $monthSensitive = new MonthSensitive(); + $monthSensitive->setClock($clock); + + $this->assertFalse($monthSensitive->isWinterMonth()); + } + } + +This test will behave the same no matter which time of the year you run it. +By combining the :class:`Symfony\\Component\\Clock\\ClockAwareTrait` and +:class:`Symfony\\Component\\Clock\\Test\\ClockSensitiveTrait`, you have full +control on your time-sensitive code's behavior. + +Exceptions Management +--------------------- + +The Clock component takes full advantage of some `PHP DateTime exceptions`_. +If you pass an invalid string to the clock (e.g. when creating a clock or +modifying a ``MockClock``) you'll get a ``DateMalformedStringException``. If you +pass an invalid timezone, you'll get a ``DateInvalidTimeZoneException``:: + + $userInput = 'invalid timezone'; + + try { + $clock = Clock::get()->withTimeZone($userInput); + } catch (\DateInvalidTimeZoneException $exception) { + // ... + } + +These exceptions are available starting from PHP 8.3. However, thanks to the +`symfony/polyfill-php83`_ dependency required by the Clock component, you can +use them even if your project doesn't use PHP 8.3 yet. + +.. _`PSR-20`: https://www.php-fig.org/psr/psr-20/ +.. _`accepted by the DateTime constructor`: https://www.php.net/manual/en/datetime.formats.php +.. _`PHP DateTime exceptions`: https://wiki.php.net/rfc/datetime-exceptions +.. _`symfony/polyfill-php83`: https://github.com/symfony/polyfill-php83 +.. _`PHP 8.3's behavior`: https://wiki.php.net/rfc/datetime-exceptions diff --git a/components/config/caching.rst b/components/config/caching.rst index 810db48107e..18620c0d8cf 100644 --- a/components/config/caching.rst +++ b/components/config/caching.rst @@ -55,3 +55,17 @@ the cache file itself. This ``.meta`` file contains the serialized resources, whose timestamps are used to determine if the cache is still fresh. When not in debug mode, the cache is considered to be "fresh" as soon as it exists, and therefore no ``.meta`` file will be generated. + +You can explicitly define the absolute path to the meta file:: + + use Symfony\Component\Config\ConfigCache; + use Symfony\Component\Config\Resource\FileResource; + + $cachePath = __DIR__.'/cache/appUserMatcher.php'; + + // the third optional argument indicates the absolute path to the meta file + $userMatcherCache = new ConfigCache($cachePath, true, '/my/absolute/path/to/cache.meta'); + +.. versionadded:: 7.1 + + The argument to customize the meta file path was introduced in Symfony 7.1. diff --git a/components/config/definition.rst b/components/config/definition.rst index c076838d1f9..929246fa915 100644 --- a/components/config/definition.rst +++ b/components/config/definition.rst @@ -54,7 +54,7 @@ implements the :class:`Symfony\\Component\\Config\\Definition\\ConfigurationInte class DatabaseConfiguration implements ConfigurationInterface { - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('database'); @@ -148,6 +148,29 @@ values:: This will restrict the ``delivery`` options to be either ``standard``, ``expedited`` or ``priority``. +You can also provide enum values to ``enumNode()``. Let's define an enumeration +describing the possible states of the example above:: + + enum Delivery: string + { + case Standard = 'standard'; + case Expedited = 'expedited'; + case Priority = 'priority'; + } + +The configuration can now be written like this:: + + $rootNode + ->children() + ->enumNode('delivery') + // You can provide all values of the enum... + ->values(Delivery::cases()) + // ... or you can pass only some values next to other scalar values + ->values([Delivery::Priority, Delivery::Standard, 'other', false]) + ->end() + ->end() + ; + Array Nodes ~~~~~~~~~~~ @@ -432,13 +455,6 @@ The following example shows these methods in practice:: Deprecating the Option ---------------------- -.. versionadded:: 5.1 - - The signature of the ``setDeprecated()`` method changed from - ``setDeprecated(?string $message)`` to - ``setDeprecated(string $package, string $version, ?string $message)`` - in Symfony 5.1. - You can deprecate options using the :method:`Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::setDeprecated` method:: @@ -547,7 +563,9 @@ be large and you may want to split it up into sections. You can do this by making a section a separate node and then appending it into the main tree with ``append()``:: - public function getConfigTreeBuilder() + use Symfony\Component\Config\Definition\Builder\NodeDefinition; + + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('database'); @@ -576,7 +594,7 @@ tree with ``append()``:: return $treeBuilder; } - public function addParametersNode() + public function addParametersNode(): NodeDefinition { $treeBuilder = new TreeBuilder('parameters'); @@ -744,7 +762,7 @@ By changing a string value into an associative array with ``name`` as the key:: ->arrayNode('connection') ->beforeNormalization() ->ifString() - ->then(function ($v) { return ['name' => $v]; }) + ->then(function (string $v): array { return ['name' => $v]; }) ->end() ->children() ->scalarNode('name')->isRequired()->end() @@ -784,7 +802,7 @@ the following ways: - ``ifTrue()`` - ``ifString()`` - ``ifNull()`` -- ``ifEmpty()`` (since Symfony 3.2) +- ``ifEmpty()`` - ``ifArray()`` - ``ifInArray()`` - ``ifNotInArray()`` diff --git a/components/config/resources.rst b/components/config/resources.rst index 22bdd2b34e9..f9b0fda61ae 100644 --- a/components/config/resources.rst +++ b/components/config/resources.rst @@ -30,7 +30,7 @@ an array containing all matches. Resource Loaders ---------------- -For each type of resource (YAML, XML, annotation, etc.) a loader must be +For each type of resource (YAML, XML, attributes, etc.) a loader must be defined. Each loader should implement :class:`Symfony\\Component\\Config\\Loader\\LoaderInterface` or extend the abstract :class:`Symfony\\Component\\Config\\Loader\\FileLoader` class, @@ -43,7 +43,7 @@ which allows for recursively importing other resources:: class YamlUserLoader extends FileLoader { - public function load($resource, $type = null) + public function load($resource, $type = null): void { $configValues = Yaml::parse(file_get_contents($resource)); @@ -54,7 +54,7 @@ which allows for recursively importing other resources:: // $this->import('extra_users.yaml'); } - public function supports($resource, $type = null) + public function supports($resource, $type = null): bool { return is_string($resource) && 'yaml' === pathinfo( $resource, diff --git a/components/console/changing_default_command.rst b/components/console/changing_default_command.rst index cb035950d0b..b739e3b39ba 100644 --- a/components/console/changing_default_command.rst +++ b/components/console/changing_default_command.rst @@ -7,22 +7,24 @@ name to the ``setDefaultCommand()`` method:: namespace Acme\Console\Command; + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; + #[AsCommand(name: 'hello:world')] class HelloWorldCommand extends Command { - protected static $defaultName = 'hello:world'; - - protected function configure() + protected function configure(): void { $this->setDescription('Outputs "Hello World"'); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $output->writeln('Hello World'); + + return Command::SUCCESS; } } diff --git a/components/console/console_arguments.rst b/components/console/console_arguments.rst index 670f19e98d7..da538ac78f1 100644 --- a/components/console/console_arguments.rst +++ b/components/console/console_arguments.rst @@ -11,6 +11,7 @@ Have a look at the following command that has three options:: namespace Acme\Console\Command; + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; @@ -18,14 +19,12 @@ Have a look at the following command that has three options:: use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; + #[AsCommand(name: 'demo:args', description: 'Describe args behaviors')] class DemoArgsCommand extends Command { - protected static $defaultName = 'demo:args'; - - protected function configure() + protected function configure(): void { $this - ->setDescription('Describe args behaviors') ->setDefinition( new InputDefinition([ new InputOption('foo', 'f'), @@ -35,7 +34,7 @@ Have a look at the following command that has three options:: ); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { // ... } diff --git a/components/console/events.rst b/components/console/events.rst index 92659aac82a..f0edf2205ac 100644 --- a/components/console/events.rst +++ b/components/console/events.rst @@ -34,7 +34,7 @@ dispatched. Listeners receive a use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleCommandEvent; - $dispatcher->addListener(ConsoleEvents::COMMAND, function (ConsoleCommandEvent $event) { + $dispatcher->addListener(ConsoleEvents::COMMAND, function (ConsoleCommandEvent $event): void { // gets the input instance $input = $event->getInput(); @@ -65,7 +65,7 @@ C/C++ standard:: use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleCommandEvent; - $dispatcher->addListener(ConsoleEvents::COMMAND, function (ConsoleCommandEvent $event) { + $dispatcher->addListener(ConsoleEvents::COMMAND, function (ConsoleCommandEvent $event): void { // gets the command to be executed $command = $event->getCommand(); @@ -98,7 +98,7 @@ Listeners receive a use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleErrorEvent; - $dispatcher->addListener(ConsoleEvents::ERROR, function (ConsoleErrorEvent $event) { + $dispatcher->addListener(ConsoleEvents::ERROR, function (ConsoleErrorEvent $event): void { $output = $event->getOutput(); $command = $event->getCommand(); @@ -132,7 +132,7 @@ Listeners receive a use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleTerminateEvent; - $dispatcher->addListener(ConsoleEvents::TERMINATE, function (ConsoleTerminateEvent $event) { + $dispatcher->addListener(ConsoleEvents::TERMINATE, function (ConsoleTerminateEvent $event): void { // gets the output $output = $event->getOutput(); @@ -152,7 +152,11 @@ Listeners receive a It is then dispatched just after the ``ConsoleEvents::ERROR`` event. The exit code received in this case is the exception code. -.. _console_signal-event: + Additionally, the event is dispatched when the command is being exited on + a signal. You can learn more about signals in the + :ref:`the dedicated section `. + +.. _console-events_signal: The ``ConsoleEvents::SIGNAL`` Event ----------------------------------- @@ -173,16 +177,31 @@ Listeners receive a use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleSignalEvent; - $dispatcher->addListener(ConsoleEvents::SIGNAL, function (ConsoleSignalEvent $event) { + $dispatcher->addListener(ConsoleEvents::SIGNAL, function (ConsoleSignalEvent $event): void { // gets the signal number $signal = $event->getHandlingSignal(); + // sets the exit code + $event->setExitCode(0); + if (\SIGINT === $signal) { echo "bye bye!"; } }); +It is also possible to abort the exit if you want the command to continue its +execution even after the event has been dispatched, thanks to the +:method:`Symfony\\Component\\Console\\Event\\ConsoleSignalEvent::abortExit` +method:: + + use Symfony\Component\Console\ConsoleEvents; + use Symfony\Component\Console\Event\ConsoleSignalEvent; + + $dispatcher->addListener(ConsoleEvents::SIGNAL, function (ConsoleSignalEvent $event) { + $event->abortExit(); + }); + .. tip:: All the available signals (``SIGINT``, ``SIGQUIT``, etc.) are defined as @@ -209,20 +228,30 @@ handle signals themselves. To do so, implement the return [\SIGINT, \SIGTERM]; } - public function handleSignal(int $signal): void + public function handleSignal(int $signal): int|false { if (\SIGINT === $signal) { // ... } // ... + + // return an integer to set the exit code, or + // false to continue normal execution + return 0; } } -.. versionadded:: 5.2 +Symfony doesn't handle any signal received by the command (not even ``SIGKILL``, +``SIGTERM``, etc). This behavior is intended, as it gives you the flexibility to +handle all signals e.g. to do some tasks before terminating the command. + +.. tip:: - The ``ConsoleSignalEvent`` and ``SignalableCommandInterface`` classes were - introduced in Symfony 5.2. + If you need to fetch the signal name from its integer value (e.g. for logging), + you can use the + :method:`Symfony\\Component\\Console\\SignalRegistry\\SignalMap::getSignalName` + method. .. _`reserved exit codes`: https://www.tldp.org/LDP/abs/html/exitcodes.html .. _`Signals`: https://en.wikipedia.org/wiki/Signal_(IPC) diff --git a/components/console/helpers/cursor.rst b/components/console/helpers/cursor.rst index b070fd31dd6..c5cab6c6d0b 100644 --- a/components/console/helpers/cursor.rst +++ b/components/console/helpers/cursor.rst @@ -1,11 +1,6 @@ Cursor Helper ============= -.. versionadded:: 5.1 - - The :class:`Symfony\\Component\\Console\\Cursor` class was introduced - in Symfony 5.1. - The :class:`Symfony\\Component\\Console\\Cursor` allows you to change the cursor position in a console command. This allows you to write on any position of the output: diff --git a/components/console/helpers/debug_formatter.rst b/components/console/helpers/debug_formatter.rst index 711d0bd5356..10d3c67a79a 100644 --- a/components/console/helpers/debug_formatter.rst +++ b/components/console/helpers/debug_formatter.rst @@ -78,7 +78,7 @@ using // ... $process = new Process(...); - $process->run(function ($type, $buffer) use ($output, $debugFormatter, $process) { + $process->run(function (string $type, string $buffer) use ($output, $debugFormatter, $process): void { $output->writeln( $debugFormatter->progress( spl_object_hash($process), diff --git a/components/console/helpers/processhelper.rst b/components/console/helpers/processhelper.rst index 875b48ab3f8..a02aabfd85d 100644 --- a/components/console/helpers/processhelper.rst +++ b/components/console/helpers/processhelper.rst @@ -81,7 +81,7 @@ A custom process callback can be passed as the fourth argument. Refer to the use Symfony\Component\Process\Process; - $helper->run($output, $process, 'The process failed :(', function ($type, $data) { + $helper->run($output, $process, 'The process failed :(', function (string $type, string $data): void { if (Process::ERR === $type) { // ... do something with the stderr output } else { diff --git a/components/console/helpers/progressbar.rst b/components/console/helpers/progressbar.rst index 4c5cb6da56b..4d524a2008e 100644 --- a/components/console/helpers/progressbar.rst +++ b/components/console/helpers/progressbar.rst @@ -57,6 +57,18 @@ Instead of advancing the bar by a number of steps (with the you can also set the current progress by calling the :method:`Symfony\\Component\\Console\\Helper\\ProgressBar::setProgress` method. +If you are resuming long-standing tasks, it's useful to start drawing the progress +bar at a certain point. Use the second optional argument of ``start()`` to set +that starting point:: + + use Symfony\Component\Console\Helper\ProgressBar; + + // creates a new progress bar (100 units) + $progressBar = new ProgressBar($output, 100); + + // displays the progress bar starting at 25 completed units + $progressBar->start(null, 25); + .. tip:: If your platform doesn't support ANSI codes, updates to the progress @@ -227,10 +239,14 @@ current progress of the bar. Here is a list of the built-in placeholders: * ``memory``: The current memory usage; * ``message``: used to display arbitrary messages in the progress bar (as explained later). +The time fields ``elapsed``, ``remaining`` and ``estimated`` are displayed with +a precision of 2. That means ``172799`` seconds are displayed as +``1 day, 23 hrs`` instead of ``1 day, 23 hrs, 59 mins, 59 secs``. + For instance, here is how you could set the format to be the same as the ``debug`` one:: - $progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%'); + $progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:16s%/%estimated:-16s% %memory:6s%'); Notice the ``:6s`` part added to some placeholders? That's how you can tweak the appearance of the bar (formatting and alignment). The part after the colon @@ -310,7 +326,7 @@ to display it can be customized:: .. caution:: For performance reasons, Symfony redraws the screen once every 100ms. If this is too - fast or to slow for your application, use the methods + fast or too slow for your application, use the methods :method:`Symfony\\Component\\Console\\Helper\\ProgressBar::minSecondsBetweenRedraws` and :method:`Symfony\\Component\\Console\\Helper\\ProgressBar::maxSecondsBetweenRedraws`:: @@ -338,13 +354,23 @@ display that are not available in the list of built-in placeholders, you can create your own. Let's see how you can create a ``remaining_steps`` placeholder that displays the number of remaining steps:: + // This definition is globally registered for all ProgressBar instances ProgressBar::setPlaceholderFormatterDefinition( 'remaining_steps', - function (ProgressBar $progressBar, OutputInterface $output) { + function (ProgressBar $progressBar, OutputInterface $output): int { return $progressBar->getMaxSteps() - $progressBar->getProgress(); } ); +It is also possible to set a placeholder formatter per ProgressBar instance +with the ``setPlaceholderFormatter`` method:: + + $progressBar = new ProgressBar($output, 3, 0); + $progressBar->setFormat('%countdown% [%bar%]'); + $progressBar->setPlaceholderFormatter('countdown', function (ProgressBar $progressBar) { + return $progressBar->getMaxSteps() - $progressBar->getProgress(); + }); + Custom Messages ~~~~~~~~~~~~~~~ diff --git a/components/console/helpers/questionhelper.rst b/components/console/helpers/questionhelper.rst index d5d08f863b8..e33c4ed5fa7 100644 --- a/components/console/helpers/questionhelper.rst +++ b/components/console/helpers/questionhelper.rst @@ -141,10 +141,6 @@ but ``red`` could be set instead (could be more explicit):: return Command::SUCCESS; } -.. versionadded:: 5.2 - - Support for using PHP objects as choice values was introduced in Symfony 5.2. - The option which should be selected by default is provided with the third argument of the constructor. The default is ``null``, which means that no option is the default one. @@ -240,7 +236,7 @@ provide a callback function to dynamically generate suggestions:: // where files and dirs can be found $foundFilesAndDirs = @scandir($inputPath) ?: []; - return array_map(function ($dirOrFile) use ($inputPath) { + return array_map(function (string $dirOrFile) use ($inputPath): string { return $inputPath.$dirOrFile; }, $foundFilesAndDirs); }; @@ -282,11 +278,6 @@ You can also specify if you want to not trim the answer by setting it directly w Accept Multiline Answers ~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.2 - - The ``setMultiline()`` and ``isMultiline()`` methods were introduced in - Symfony 5.2. - By default, the question helper stops reading user input when it receives a newline character (i.e., when the user hits ``ENTER`` once). However, you may specify that the response to a question should allow multiline answers by passing ``true`` to @@ -389,7 +380,7 @@ method:: $helper = $this->getHelper('question'); $question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle'); - $question->setNormalizer(function ($value) { + $question->setNormalizer(function (string $value): string { // $value can be null here return $value ? trim($value) : ''; }); @@ -427,7 +418,7 @@ method:: $helper = $this->getHelper('question'); $question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle'); - $question->setValidator(function ($answer) { + $question->setValidator(function (string $answer): string { if (!is_string($answer) || 'Bundle' !== substr($answer, -6)) { throw new \RuntimeException( 'The name of the bundle should be suffixed with \'Bundle\'' @@ -487,10 +478,10 @@ You can also use a validator with a hidden question:: $helper = $this->getHelper('question'); $question = new Question('Please enter your password'); - $question->setNormalizer(function ($value) { + $question->setNormalizer(function (?string $value): string { return $value ?? ''; }); - $question->setValidator(function ($value) { + $question->setValidator(function (string $value): string { if ('' === trim($value)) { throw new \Exception('The password cannot be empty'); } @@ -516,7 +507,7 @@ from the command line, you need to set the inputs that the command expects:: use Symfony\Component\Console\Tester\CommandTester; // ... - public function testExecute() + public function testExecute(): void { // ... $commandTester = new CommandTester($command); diff --git a/components/console/helpers/table.rst b/components/console/helpers/table.rst index 171412511aa..13bdeb491f0 100644 --- a/components/console/helpers/table.rst +++ b/components/console/helpers/table.rst @@ -154,6 +154,27 @@ The output of this command will be: | (the rest of the rows...) | +-------+------------+--------------------------------+ +By default, table contents are displayed horizontally. You can change this behavior +via the :method:`Symfony\\Component\\Console\\Helper\\Table::setVertical` method:: + + // ... + $table->setVertical(); + $table->render(); + +The output of this command will be: + +.. code-block:: terminal + + +------------------------------+ + | ISBN: 99921-58-10-7 | + | Title: Divine Comedy | + | Author: Dante Alighieri | + |------------------------------| + | ISBN: 9971-5-0210-0 | + | Title: A Tale of Two Cities | + | Author: Charles Dickens | + +------------------------------+ + The table style can be changed to any built-in styles via :method:`Symfony\\Component\\Console\\Helper\\Table::setStyle`:: @@ -269,10 +290,6 @@ Here is a full list of things you can customize: This method can also be used to override a built-in style. -.. versionadded:: 5.2 - - The option to style table cells was introduced in Symfony 5.2. - In addition to the built-in table styles, you can also apply different styles to each table cell via :class:`Symfony\\Component\\Console\\Helper\\TableCellStyle`:: diff --git a/components/console/logger.rst b/components/console/logger.rst index 9136707416f..c3d5c447a89 100644 --- a/components/console/logger.rst +++ b/components/console/logger.rst @@ -16,14 +16,12 @@ PSR-3 compliant logger:: class MyDependency { - private $logger; - - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; + public function __construct( + private LoggerInterface $logger, + ) { } - public function doStuff() + public function doStuff(): void { $this->logger->info('I love Tony Vairelles\' hairdresser.'); } @@ -34,30 +32,26 @@ You can rely on the logger to use this dependency inside a command:: namespace Acme\Console\Command; use Acme\MyDependency; + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\OutputInterface; + #[AsCommand( + name: 'my:command', + description: 'Use an external dependency requiring a PSR-3 logger' + )] class MyCommand extends Command { - protected static $defaultName = 'my:command'; - - protected function configure() - { - $this - ->setDescription( - 'Use an external dependency requiring a PSR-3 logger' - ) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $logger = new ConsoleLogger($output); $myDependency = new MyDependency($logger); $myDependency->doStuff(); + + return Command::SUCCESS; } } diff --git a/components/console/single_command_tool.rst b/components/console/single_command_tool.rst index b05508f232b..97cb09bf030 100644 --- a/components/console/single_command_tool.rst +++ b/components/console/single_command_tool.rst @@ -20,16 +20,11 @@ it is possible to remove this need by declaring a single command application:: ->setVersion('1.0.0') // Optional ->addArgument('foo', InputArgument::OPTIONAL, 'The directory') ->addOption('bar', null, InputOption::VALUE_REQUIRED) - ->setCode(function (InputInterface $input, OutputInterface $output) { + ->setCode(function (InputInterface $input, OutputInterface $output): int { // output arguments and options }) ->run(); -.. versionadded:: 5.1 - - The :class:`Symfony\\Component\\Console\\SingleCommandApplication` class was - introduced in Symfony 5.1. - You can still register a command as usual:: #!/usr/bin/env php diff --git a/components/contracts.rst b/components/contracts.rst index 5fe0280e5a7..56b0394397d 100644 --- a/components/contracts.rst +++ b/components/contracts.rst @@ -57,7 +57,7 @@ convention. For example: { "...": "...", "provide": { - "symfony/cache-implementation": "1.0" + "symfony/cache-implementation": "3.0" } } diff --git a/components/css_selector.rst b/components/css_selector.rst index adebe617424..1331a11e616 100644 --- a/components/css_selector.rst +++ b/components/css_selector.rst @@ -92,7 +92,11 @@ Pseudo-classes are partially supported: * Not supported: ``*:first-of-type``, ``*:last-of-type``, ``*:nth-of-type`` and ``*:nth-last-of-type`` (all these work with an element name (e.g. ``li:first-of-type``) but not with the ``*`` selector). -* Supported: ``*:only-of-type``. +* Supported: ``*:only-of-type``, ``*:scope``, ``*:is`` and ``*:where``. + +.. versionadded:: 7.1 + + The support for ``*:is`` and ``*:where`` was introduced in Symfony 7.1. Learn more ---------- diff --git a/components/dependency_injection.rst b/components/dependency_injection.rst index a6d8521f03a..93e8af711cf 100644 --- a/components/dependency_injection.rst +++ b/components/dependency_injection.rst @@ -31,7 +31,7 @@ you want to make available as a service:: class Mailer { - private $transport; + private string $transport; public function __construct() { @@ -54,11 +54,9 @@ so this is passed into the constructor:: class Mailer { - private $transport; - - public function __construct($transport) - { - $this->transport = $transport; + public function __construct( + private string $transport, + ) { } // ... @@ -95,11 +93,9 @@ like this:: class NewsletterManager { - private $mailer; - - public function __construct(\Mailer $mailer) - { - $this->mailer = $mailer; + public function __construct( + private \Mailer $mailer, + ) { } // ... @@ -128,9 +124,9 @@ it was only optional then you could use setter injection instead:: class NewsletterManager { - private $mailer; + private \Mailer $mailer; - public function setMailer(\Mailer $mailer) + public function setMailer(\Mailer $mailer): void { $this->mailer = $mailer; } @@ -166,6 +162,35 @@ like this:: $newsletterManager = $container->get('newsletter_manager'); +Getting Services That Don't Exist +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, when you try to get a service that doesn't exist, you see an exception. +You can override this behavior as follows:: + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\ContainerInterface; + + $containerBuilder = new ContainerBuilder(); + + // ... + + // the second argument is optional and defines what to do when the service doesn't exist + $newsletterManager = $containerBuilder->get('newsletter_manager', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE); + +These are all the possible behaviors: + + * ``ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE``: throws an exception + at compile time (this is the **default** behavior); + * ``ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE``: throws an + exception at runtime, when trying to access the missing service; + * ``ContainerInterface::NULL_ON_INVALID_REFERENCE``: returns ``null``; + * ``ContainerInterface::IGNORE_ON_INVALID_REFERENCE``: ignores the wrapping + command asking for the reference (for instance, ignore a setter if the service + does not exist); + * ``ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE``: ignores/returns + ``null`` for uninitialized services or invalid references. + Avoiding your Code Becoming Dependent on the Container ------------------------------------------------------ @@ -287,7 +312,7 @@ config files: namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->parameters() // ... ->set('mailer.transport', 'sendmail') @@ -299,12 +324,10 @@ config files: ; $services->set('mailer', 'Mailer') - // the param() method was introduced in Symfony 5.2. ->args([param('mailer.transport')]) ; $services->set('newsletter_manager', 'NewsletterManager') - // In versions earlier to Symfony 5.1 the service() function was called ref() ->call('setMailer', [service('mailer')]) ; }; diff --git a/components/dependency_injection/_imports-parameters-note.rst.inc b/components/dependency_injection/_imports-parameters-note.rst.inc index d17d6d60b26..1389ca78fe3 100644 --- a/components/dependency_injection/_imports-parameters-note.rst.inc +++ b/components/dependency_injection/_imports-parameters-note.rst.inc @@ -31,6 +31,6 @@ // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->import('%kernel.project_dir%/somefile.yaml'); }; diff --git a/components/dependency_injection/compilation.rst b/components/dependency_injection/compilation.rst index beedbf33853..7f991e85b72 100644 --- a/components/dependency_injection/compilation.rst +++ b/components/dependency_injection/compilation.rst @@ -61,7 +61,7 @@ A very simple extension may just load configuration files into the container:: class AcmeDemoExtension implements ExtensionInterface { - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $loader = new XmlFileLoader( $container, @@ -90,7 +90,7 @@ The Extension must specify a ``getAlias()`` method to implement the interface:: { // ... - public function getAlias() + public function getAlias(): string { return 'acme_demo'; } @@ -132,7 +132,7 @@ are loaded:: The values from those sections of the config files are passed into the first argument of the ``load()`` method of the extension:: - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $foo = $configs[0]['foo']; //fooValue $bar = $configs[0]['bar']; //barValue @@ -158,7 +158,7 @@ you could access the config value this way:: use Symfony\Component\Config\Definition\Processor; // ... - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); $processor = new Processor(); @@ -175,12 +175,12 @@ namespace so that the relevant parts of an XML config file are passed to the extension. The other to specify the base path to XSD files to validate the XML configuration:: - public function getXsdValidationBasePath() + public function getXsdValidationBasePath(): string { return __DIR__.'/../Resources/config/'; } - public function getNamespace() + public function getNamespace(): string { return 'http://www.example.com/symfony/schema/'; } @@ -219,7 +219,7 @@ The processed config value can now be added as container parameters as if it were listed in a ``parameters`` section of the config file but with the additional benefit of merging multiple files and validation of the configuration:: - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); $processor = new Processor(); @@ -234,7 +234,7 @@ More complex configuration requirements can be catered for in the Extension classes. For example, you may choose to load a main service configuration file but also load a secondary one only if a certain parameter is set:: - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); $processor = new Processor(); @@ -251,6 +251,33 @@ file but also load a secondary one only if a certain parameter is set:: } } +You can also deprecate container parameters in your extension to warn users +about not using them anymore. This helps with the migration across major versions +of an extension. + +Deprecation is only possible when using PHP to configure the extension, not when +using XML or YAML. Use the ``ContainerBuilder::deprecateParameter()`` method to +provide the deprecation details:: + + public function load(array $configs, ContainerBuilder $containerBuilder) + { + // ... + + $containerBuilder->setParameter('acme_demo.database_user', $configs['db_user']); + + $containerBuilder->deprecateParameter( + 'acme_demo.database_user', + 'acme/database-package', + '1.3', + // optionally you can set a custom deprecation message + '"acme_demo.database_user" is deprecated, you should configure database credentials with the "acme_demo.database_dsn" parameter instead.' + ); + } + +The parameter being deprecated must be set before being declared as deprecated. +Otherwise a :class:`Symfony\\Component\\DependencyInjection\\Exception\\ParameterNotFoundException` +exception will be thrown. + .. note:: Just registering an extension with the container is not enough to get @@ -292,7 +319,7 @@ method is called by implementing { // ... - public function prepend(ContainerBuilder $container) + public function prepend(ContainerBuilder $container): void { // ... @@ -323,7 +350,7 @@ compilation:: class AcmeDemoExtension implements ExtensionInterface, CompilerPassInterface { - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { // ... do something during the compilation } @@ -377,7 +404,7 @@ class implementing the ``CompilerPassInterface``:: class CustomPass implements CompilerPassInterface { - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { // ... do something during the compilation } @@ -475,7 +502,7 @@ serves at dumping the compiled container:: the :ref:`dumpFile() method ` from Symfony Filesystem component or other methods provided by Symfony (e.g. ``$containerConfigCache->write()``) which are atomic. - + ``ProjectServiceContainer`` is the default name given to the dumped container class. However, you can change this with the ``class`` option when you dump it:: diff --git a/components/dom_crawler.rst b/components/dom_crawler.rst index b8c484ab114..ac859efac91 100644 --- a/components/dom_crawler.rst +++ b/components/dom_crawler.rst @@ -66,13 +66,6 @@ tree. isn't meant to dump content, you can see the "fixed" version of your HTML by :ref:`dumping it `. -.. note:: - - If you need better support for HTML5 contents or want to get rid of the - inconsistencies of PHP's DOM extension, install the `html5-php library`_. - The DomCrawler component will use it automatically when the content has - an HTML5 doctype. - Node Filtering ~~~~~~~~~~~~~~ @@ -96,9 +89,9 @@ An anonymous function can be used to filter with more complex criteria:: $crawler = $crawler ->filter('body > p') - ->reduce(function (Crawler $node, $i) { + ->reduce(function (Crawler $node, $i): bool { // filters every other node - return ($i % 2) == 0; + return ($i % 2) === 0; }); To remove a node, the anonymous function must return ``false``. @@ -188,10 +181,6 @@ Get all the child or ancestor nodes:: $crawler->filter('body')->children(); $crawler->filter('body > p')->ancestors(); -.. versionadded:: 5.3 - - The ``ancestors()`` method was introduced in Symfony 5.3. - Get all the direct child nodes matching a CSS selector:: $crawler->filter('body')->children('p.lorem'); @@ -221,25 +210,36 @@ Access the value of the first node of the current selection:: // avoid the exception passing an argument that text() returns when node does not exist $message = $crawler->filterXPath('//body/p')->text('Default text content'); - // by default, text() trims white spaces, including the internal ones + // by default, text() trims whitespace characters, including the internal ones // (e.g. " foo\n bar baz \n " is returned as "foo bar baz") // pass FALSE as the second argument to return the original text unchanged $crawler->filterXPath('//body/p')->text('Default text content', false); - // innerText() is similar to text() but only returns the text that is - // the direct descendant of the current node, excluding any child nodes + // innerText() is similar to text() but returns only text that is a direct + // descendant of the current node, excluding text from child nodes $text = $crawler->filterXPath('//body/p')->innerText(); - // if content is

Foo Bar

- // innerText() returns 'Foo' and text() returns 'Foo Bar' + // if content is

Foo Bar

or

Bar Foo

+ // innerText() returns 'Foo' in both cases; and text() returns 'Foo Bar' and 'Bar Foo' respectively -.. versionadded:: 5.4 + // if there are multiple text nodes, between other child nodes, like + //

Foo Bar Baz

+ // innerText() returns only the first text node 'Foo' - The ``innerText()`` method was introduced in Symfony 5.4. + // like text(), innerText() also trims whitespace characters by default, + // but you can get the unchanged text by passing FALSE as argument + $text = $crawler->filterXPath('//body/p')->innerText(false); Access the attribute value of the first node of the current selection:: $class = $crawler->filterXPath('//body/p')->attr('class'); +.. tip:: + + You can define the default value to use if the node or attribute is empty + by using the second argument of the ``attr()`` method:: + + $class = $crawler->filterXPath('//body/p')->attr('class', 'my-default-class'); + Extract attribute and/or node values from the list of nodes:: $attributes = $crawler @@ -257,7 +257,7 @@ Call an anonymous function on each node of the list:: use Symfony\Component\DomCrawler\Crawler; // ... - $nodeValues = $crawler->filter('p')->each(function (Crawler $node, $i) { + $nodeValues = $crawler->filter('p')->each(function (Crawler $node, $i): string { return $node->text(); }); @@ -267,7 +267,7 @@ The result is an array of values returned by the anonymous function calls. When using nested crawler, beware that ``filterXPath()`` is evaluated in the context of the crawler:: - $crawler->filterXPath('parent')->each(function (Crawler $parentCrawler, $i) { + $crawler->filterXPath('parent')->each(function (Crawler $parentCrawler, $i): avoid { // DON'T DO THIS: direct child can not be found $subCrawler = $parentCrawler->filterXPath('sub-tag/sub-child-tag'); @@ -639,10 +639,6 @@ the whole form or specific field(s):: Resolving a URI ~~~~~~~~~~~~~~~ -.. versionadded:: 5.1 - - The :class:`Symfony\\Component\\DomCrawler\\UriResolver` helper class was added in Symfony 5.1. - The :class:`Symfony\\Component\\DomCrawler\\UriResolver` class takes a URI (relative, absolute, fragment, etc.) and turns it into an absolute URI against another given base URI:: @@ -653,10 +649,23 @@ another given base URI:: UriResolver::resolve('?a=b', 'http://localhost/bar#foo'); // http://localhost/bar?a=b UriResolver::resolve('../../', 'http://localhost/'); // http://localhost/ +Using a HTML5 Parser +~~~~~~~~~~~~~~~~~~~~ + +If you need the :class:`Symfony\\Component\\DomCrawler\\Crawler` to use an HTML5 +parser, set its ``useHtml5Parser`` constructor argument to ``true``:: + + use Symfony\Component\DomCrawler\Crawler; + + $crawler = new Crawler(null, $uri, useHtml5Parser: true); + +By doing so, the crawler will use the HTML5 parser provided by the `masterminds/html5`_ +library to parse the documents. + Learn more ---------- * :doc:`/testing` * :doc:`/components/css_selector` -.. _`html5-php library`: https://github.com/Masterminds/html5-php +.. _`masterminds/html5`: https://packagist.org/packages/masterminds/html5 diff --git a/components/event_dispatcher.rst b/components/event_dispatcher.rst index c3bf0bae1b2..83cead3d19c 100644 --- a/components/event_dispatcher.rst +++ b/components/event_dispatcher.rst @@ -136,7 +136,7 @@ The ``addListener()`` method takes up to three arguments: use Symfony\Contracts\EventDispatcher\Event; - $dispatcher->addListener('acme.foo.action', function (Event $event) { + $dispatcher->addListener('acme.foo.action', function (Event $event): void { // will be executed when the acme.foo.action event is dispatched }); @@ -151,7 +151,7 @@ the ``Event`` object as the single argument:: { // ... - public function onFooAction(Event $event) + public function onFooAction(Event $event): void { // ... do something } @@ -225,13 +225,11 @@ determine which instance is passed. Note that ``AddEventAliasesPass`` has to be processed before ``RegisterListenersPass``. - By default, the listeners pass assumes that the event dispatcher's service + The listeners pass assumes that the event dispatcher's service id is ``event_dispatcher``, that event listeners are tagged with the ``kernel.event_listener`` tag, that event subscribers are tagged with the ``kernel.event_subscriber`` tag and that the alias mapping is - stored as parameter ``event_dispatcher.event_aliases``. You can change these - default values by passing custom values to the constructors of - ``RegisterListenersPass`` and ``AddEventAliasesPass``. + stored as parameter ``event_dispatcher.event_aliases``. .. _event_dispatcher-closures-as-listeners: @@ -350,7 +348,7 @@ Take the following example of a subscriber that subscribes to the class StoreSubscriber implements EventSubscriberInterface { - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ KernelEvents::RESPONSE => [ @@ -361,12 +359,12 @@ Take the following example of a subscriber that subscribes to the ]; } - public function onKernelResponsePre(ResponseEvent $event) + public function onKernelResponsePre(ResponseEvent $event): void { // ... } - public function onKernelResponsePost(ResponseEvent $event) + public function onKernelResponsePost(ResponseEvent $event): void { // ... } @@ -460,7 +458,7 @@ is dispatched, are passed as arguments to the listener:: class MyListener { - public function myEventListener(Event $event, string $eventName, EventDispatcherInterface $dispatcher) + public function myEventListener(Event $event, string $eventName, EventDispatcherInterface $dispatcher): void { // ... do something with the event name } diff --git a/components/event_dispatcher/container_aware_dispatcher.rst b/components/event_dispatcher/container_aware_dispatcher.rst deleted file mode 100644 index ad07d7bc9a8..00000000000 --- a/components/event_dispatcher/container_aware_dispatcher.rst +++ /dev/null @@ -1,7 +0,0 @@ -The Container Aware Event Dispatcher -==================================== - -.. caution:: - - The ``ContainerAwareEventDispatcher`` was removed in Symfony 4.0. Use - ``EventDispatcher`` with closure-proxy injection instead. diff --git a/components/event_dispatcher/generic_event.rst b/components/event_dispatcher/generic_event.rst index 8fba7c41940..41d0a9d66a4 100644 --- a/components/event_dispatcher/generic_event.rst +++ b/components/event_dispatcher/generic_event.rst @@ -54,7 +54,7 @@ Passing a subject:: class FooListener { - public function handler(GenericEvent $event) + public function handler(GenericEvent $event): void { if ($event->getSubject() instanceof Foo) { // ... @@ -75,7 +75,7 @@ access the event arguments:: class FooListener { - public function handler(GenericEvent $event) + public function handler(GenericEvent $event): void { if (isset($event['type']) && 'foo' === $event['type']) { // ... do something @@ -94,7 +94,7 @@ Filtering data:: class FooListener { - public function filter(GenericEvent $event) + public function filter(GenericEvent $event): void { $event['data'] = strtolower($event['data']); } diff --git a/components/event_dispatcher/immutable_dispatcher.rst b/components/event_dispatcher/immutable_dispatcher.rst index 0a930352bfe..a6a98c47f37 100644 --- a/components/event_dispatcher/immutable_dispatcher.rst +++ b/components/event_dispatcher/immutable_dispatcher.rst @@ -13,9 +13,10 @@ To use it, first create a normal ``EventDispatcher`` dispatcher and register some listeners or subscribers:: use Symfony\Component\EventDispatcher\EventDispatcher; + use Symfony\Contracts\EventDispatcher\Event; $dispatcher = new EventDispatcher(); - $dispatcher->addListener('foo.action', function ($event) { + $dispatcher->addListener('foo.action', function (Event $event): void { // ... }); diff --git a/components/expression_language.rst b/components/expression_language.rst index 1ddd0fddb30..5ad835a8d94 100644 --- a/components/expression_language.rst +++ b/components/expression_language.rst @@ -14,8 +14,6 @@ Installation .. include:: /components/require_autoload.rst.inc -How can the Expression Engine Help Me? - .. _how-can-the-expression-engine-help-me: How can the Expression Language Help Me? @@ -79,6 +77,57 @@ The main class of the component is See :doc:`/reference/formats/expression_language` to learn the syntax of the ExpressionLanguage component. +Null Coalescing Operator +........................ + +.. note:: + + This content has been moved to the :ref:`null coalescing operator ` + section of ExpressionLanguage syntax reference page. + +Parsing and Linting Expressions +............................... + +The ExpressionLanguage component provides a way to parse and lint expressions. +The :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::parse` +method returns a :class:`Symfony\\Component\\ExpressionLanguage\\ParsedExpression` +instance that can be used to inspect and manipulate the expression. The +:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::lint`, on the +other hand, returns a boolean indicating if the expression is valid or not:: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + + $expressionLanguage = new ExpressionLanguage(); + + var_dump($expressionLanguage->parse('1 + 2', [])); + // displays the AST nodes of the expression which can be + // inspected and manipulated + + var_dump($expressionLanguage->lint('1 + 2', [])); // displays true + +The behavior of these methods can be configured with some flags defined in the +:class:`Symfony\\Component\\ExpressionLanguage\\Parser` class: + +* ``IGNORE_UNKNOWN_VARIABLES``: don't throw an exception if a variable is not + defined in the expression; +* ``IGNORE_UNKNOWN_FUNCTIONS``: don't throw an exception if a function is not + defined in the expression. + +This is how you can use these flags:: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + use Symfony\Component\ExpressionLanguage\Parser; + + $expressionLanguage = new ExpressionLanguage(); + + // this returns true because the unknown variables and functions are ignored + var_dump($expressionLanguage->lint('unknown_var + unknown_function()', Parser::IGNORE_UNKNOWN_VARIABLES | Parser::IGNORE_UNKNOWN_FUNCTIONS)); + +.. versionadded:: 7.1 + + The support for flags in the ``parse()`` and ``lint()`` methods + was introduced in Symfony 7.1. + Passing in Variables -------------------- @@ -91,7 +140,7 @@ PHP type (including objects):: class Apple { - public $variety; + public string $variety; } $apple = new Apple(); @@ -261,9 +310,9 @@ Example:: use Symfony\Component\ExpressionLanguage\ExpressionLanguage; $expressionLanguage = new ExpressionLanguage(); - $expressionLanguage->register('lowercase', function ($str) { + $expressionLanguage->register('lowercase', function ($str): string { return sprintf('(is_string(%1$s) ? strtolower(%1$s) : %1$s)', $str); - }, function ($arguments, $str) { + }, function ($arguments, $str): string { if (!is_string($str)) { return $str; } @@ -299,12 +348,12 @@ register:: class StringExpressionLanguageProvider implements ExpressionFunctionProviderInterface { - public function getFunctions() + public function getFunctions(): array { return [ - new ExpressionFunction('lowercase', function ($str) { + new ExpressionFunction('lowercase', function ($str): string { return sprintf('(is_string(%1$s) ? strtolower(%1$s) : %1$s)', $str); - }, function ($arguments, $str) { + }, function ($arguments, $str): string { if (!is_string($str)) { return $str; } diff --git a/components/filesystem.rst b/components/filesystem.rst index 600fdf3ae9e..dabf3f81872 100644 --- a/components/filesystem.rst +++ b/components/filesystem.rst @@ -216,15 +216,17 @@ systems (unlike PHP's :phpfunction:`readlink` function):: // returns its absolute fully resolved final version of the target (if there are nested links, they are resolved) $filesystem->readlink('/path/to/link', true); -Its behavior is the following:: +Its behavior is the following: -* When ``$canonicalize`` is ``false`` (the default value): - * if ``$path`` does not exist or is not a link, it returns ``null``. - * if ``$path`` is a link, it returns the next direct target of the link without considering the existence of the target. +* When ``$canonicalize`` is ``false``: + + * if ``$path`` does not exist or is not a link, it returns ``null``. + * if ``$path`` is a link, it returns the next direct target of the link without considering the existence of the target. * When ``$canonicalize`` is ``true``: - * if ``$path`` does not exist, it returns null. - * if ``$path`` exists, it returns its absolute fully resolved final version. + + * if ``$path`` does not exist, it returns null. + * if ``$path`` exists, it returns its absolute fully resolved final version. .. note:: @@ -282,20 +284,17 @@ exception on failure:: // returns a path like : /tmp/prefix_wyjgtF.png $filesystem->tempnam('/tmp', 'prefix_', '.png'); -.. versionadded:: 5.1 - - The option to set a suffix in ``tempnam()`` was introduced in Symfony 5.1. - .. _filesystem-dumpfile: ``dumpFile`` ~~~~~~~~~~~~ :method:`Symfony\\Component\\Filesystem\\Filesystem::dumpFile` saves the given -contents into a file. It does this in an atomic manner: it writes a temporary -file first and then moves it to the new file location when it's finished. -This means that the user will always see either the complete old file or -complete new file (but never a partially-written file):: +contents into a file (creating the file and its directory if they don't exist). +It does this in an atomic manner: it writes a temporary file first and then moves +it to the new file location when it's finished. This means that the user will +always see either the complete old file or complete new file (but never a +partially-written file):: $filesystem->dumpFile('file.txt', 'Hello World'); @@ -314,16 +313,24 @@ contents at the end of some file:: If either the file or its containing directory doesn't exist, this method creates them before appending the contents. -.. versionadded:: 5.4 +``readFile`` +~~~~~~~~~~~~ - The third argument of ``appendToFile()`` was introduced in Symfony 5.4. +.. versionadded:: 7.1 -Path Manipulation Utilities ---------------------------- + The ``readFile()`` method was introduced in Symfony 7.1. + +:method:`Symfony\\Component\\Filesystem\\Filesystem::readFile` returns all the +contents of a file as a string. Unlike the :phpfunction:`file_get_contents` function +from PHP, it throws an exception when the given file path is not readable and +when passing the path to a directory instead of a file:: + + $contents = $filesystem->readFile('/some/path/to/file.txt'); -.. versionadded:: 5.4 +The ``$contents`` variable now stores all the contents of the ``file.txt`` file. - The :class:`Symfony\\Component\\Filesystem\\Path` class was introduced in Symfony 5.4. +Path Manipulation Utilities +--------------------------- Dealing with file paths usually involves some difficulties: diff --git a/components/filesystem/lock_handler.rst b/components/filesystem/lock_handler.rst deleted file mode 100644 index 5997fd3887b..00000000000 --- a/components/filesystem/lock_handler.rst +++ /dev/null @@ -1,7 +0,0 @@ -LockHandler -=========== - -.. caution:: - - The ``LockHandler`` utility was removed in Symfony 4.0. Use the new Symfony - :doc:`Lock component ` instead. diff --git a/components/finder.rst b/components/finder.rst index c696d7290ab..a3b91470b62 100644 --- a/components/finder.rst +++ b/components/finder.rst @@ -177,10 +177,6 @@ The rules of a directory always override the rules of its parent directories. starting from the directory used to search files/directories. To be consistent with Git behavior, you should explicitly search from the Git repository root. -.. versionadded:: 5.4 - - Recursive support for ``.gitignore`` files was introduced in Symfony 5.4. - File Name ~~~~~~~~~ @@ -357,13 +353,23 @@ it is called with the file as a :class:`Symfony\\Component\\Finder\\SplFileInfo` instance. The file is excluded from the result set if the Closure returns ``false``. +The ``filter()`` method includes a second optional argument to prune directories. +If set to ``true``, this method completely skips the excluded directories instead +of traversing the entire file/directory structure and excluding them later. When +using a closure, return ``false`` for the directories which you want to prune. + +Pruning directories early can improve performance significantly depending on the +file/directory hierarchy complexity and the number of excluded directories. + Sorting Results --------------- -Sort the results by name or by type (directories first, then files):: +Sort the results by name, extension, size or type (directories first, then files):: $finder->sortByName(); - + $finder->sortByCaseInsensitiveName(); + $finder->sortByExtension(); + $finder->sortBySize(); $finder->sortByType(); .. tip:: @@ -373,6 +379,11 @@ Sort the results by name or by type (directories first, then files):: as its argument to use PHP's `natural sort order`_ algorithm instead (e.g. ``file1.txt``, ``file2.txt``, ``file10.txt``). + The ``sortByCaseInsensitiveName()`` method uses the case insensitive + :phpfunction:`strcasecmp` PHP function. Pass ``true`` as its argument to use + PHP's case insensitive `natural sort order`_ algorithm instead (i.e. the + :phpfunction:`strnatcasecmp` PHP function) + Sort the files and directories by the last accessed, changed or modified time:: $finder->sortByAccessedTime(); @@ -383,7 +394,7 @@ Sort the files and directories by the last accessed, changed or modified time:: You can also define your own sorting algorithm with the ``sort()`` method:: - $finder->sort(function (\SplFileInfo $a, \SplFileInfo $b) { + $finder->sort(function (\SplFileInfo $a, \SplFileInfo $b): int { return strcmp($a->getRealPath(), $b->getRealPath()); }); diff --git a/components/form.rst b/components/form.rst index f8af0c71090..7584d223032 100644 --- a/components/form.rst +++ b/components/form.rst @@ -204,7 +204,7 @@ to bootstrap or access Twig and add the :class:`Symfony\\Bridge\\Twig\\Extension ])); $formEngine = new TwigRendererEngine([$defaultFormTheme], $twig); $twig->addRuntimeLoader(new FactoryRuntimeLoader([ - FormRenderer::class => function () use ($formEngine, $csrfManager) { + FormRenderer::class => function () use ($formEngine, $csrfManager): FormRenderer { return new FormRenderer($formEngine, $csrfManager); }, ])); @@ -392,10 +392,11 @@ is created from the form factory. use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; class TaskController extends AbstractController { - public function new(Request $request) + public function new(Request $request): Response { // createFormBuilder is a shortcut to get the "form factory" // and then call "createBuilder()" on it @@ -451,10 +452,11 @@ an "edit" form), pass in the default data when creating your form builder: use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\HttpFoundation\Response; class DefaultController extends AbstractController { - public function new(Request $request) + public function new(Request $request): Response { $defaults = [ 'dueDate' => new \DateTime('tomorrow'), @@ -536,10 +538,11 @@ by :method:`Symfony\\Component\\Form\\Form::handleRequest` to determine whether use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\FormType; + use Symfony\Component\HttpFoundation\Response; class DefaultController extends AbstractController { - public function search() + public function search(): Response { $formBuilder = $this->createFormBuilder(null, [ 'action' => '/search', @@ -581,10 +584,11 @@ method: use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\HttpFoundation\Response; class TaskController extends AbstractController { - public function new(Request $request) + public function new(Request $request): Response { $form = $this->createFormBuilder() ->add('task', TextType::class) @@ -676,12 +680,13 @@ option when building each field: use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\Type; class DefaultController extends AbstractController { - public function new(Request $request) + public function new(Request $request): Response { $form = $this->createFormBuilder() ->add('task', TextType::class, [ @@ -744,10 +749,11 @@ method to access the list of errors. It returns a // "firstName" field $errors = $form['firstName']->getErrors(); - // a FormErrorIterator instance in a flattened structure + // a FormErrorIterator instance including child forms in a flattened structure + // use getOrigin() to determine the form causing the error $errors = $form->getErrors(true); - // a FormErrorIterator instance representing the form tree structure + // a FormErrorIterator instance including child forms without flattening the output structure $errors = $form->getErrors(true, false); Clearing Form Errors diff --git a/components/http_foundation.rst b/components/http_foundation.rst index f1adc0effcd..21e9bbfb13e 100644 --- a/components/http_foundation.rst +++ b/components/http_foundation.rst @@ -139,8 +139,18 @@ has some methods to filter the input values: :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getInt` Returns the parameter value converted to integer; +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getEnum` + Returns the parameter value converted to a PHP enum; + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getString` + Returns the parameter value as a string; + :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::filter` Filters the parameter by using the PHP :phpfunction:`filter_var` function. + If invalid values are found, a + :class:`Symfony\\Component\\HttpKernel\\Exception\\BadRequestHttpException` + is thrown. The ``FILTER_NULL_ON_FAILURE`` flag can be used to ignore invalid + values. All getters take up to two arguments: the first one is the parameter name and the second one is the default value to return if the parameter does not @@ -164,19 +174,19 @@ doesn't support returning arrays, so you need to use the following code:: // the query string is '?foo[bar]=baz' // don't use $request->query->get('foo'); use the following instead: - $request->query->all()['foo']; + $request->query->all('foo'); // returns ['bar' => 'baz'] + // if the requested parameter does not exist, an empty array is returned: + $request->query->all('qux'); + // returns [] + $request->query->get('foo[bar]'); // returns null $request->query->all()['foo']['bar']; // returns 'baz' -.. deprecated:: 5.1 - - The array support in ``get()`` method was deprecated in Symfony 5.1. - .. _component-foundation-attributes: Thanks to the public ``attributes`` property, you can store additional data @@ -198,9 +208,12 @@ If the request body is a JSON string, it can be accessed using $data = $request->toArray(); -.. versionadded:: 5.2 +If the request data could be ``$_POST`` data *or* a JSON string, you can use +the :method:`Symfony\\Component\\HttpFoundation\\Request::getPayload` method +which returns an instance of :class:`Symfony\\Component\\HttpFoundation\\InputBag` +wrapping this data:: - The ``toArray()`` method was introduced in Symfony 5.2. + $data = $request->getPayload(); Identifying a Request ~~~~~~~~~~~~~~~~~~~~~ @@ -287,10 +300,6 @@ this complexity and defines some methods for the most common tasks:: HeaderUtils::parseQuery('foo[bar.baz]=qux'); // => ['foo' => ['bar.baz' => 'qux']] -.. versionadded:: 5.2 - - The ``parseQuery()`` method was introduced in Symfony 5.2. - Accessing ``Accept-*`` Headers Data ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -371,6 +380,71 @@ the ``checkIp()`` method from :class:`Symfony\\Component\\HttpFoundation\\IpUtil $isIpInCIDRv6 = IpUtils::checkIp($ipv6, $CIDRv6); // $isIpInCIDRv6 = true +Check if an IP Belongs to a Private Subnet +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to know if an IP address belongs to a private subnet, you can +use the ``isPrivateIp()`` method from the +:class:`Symfony\\Component\\HttpFoundation\\IpUtils` to do that:: + + use Symfony\Component\HttpFoundation\IpUtils; + + $ipv4 = '192.168.1.1'; + $isPrivate = IpUtils::isPrivateIp($ipv4); + // $isPrivate = true + + $ipv6 = '2a01:198:603:10:396e:4789:8e99:890f'; + $isPrivate = IpUtils::isPrivateIp($ipv6); + // $isPrivate = false + +Matching a Request Against a Set of Rules +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The HttpFoundation component provides some matcher classes that allow you to +check if a given request meets certain conditions (e.g. it comes from some IP +address, it uses a certain HTTP method, etc.): + +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\AttributesRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\ExpressionRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\HeaderRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\HostRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\IpsRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\IsJsonRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\MethodRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\PathRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\PortRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\QueryParameterRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\SchemeRequestMatcher` + +You can use them individually or combine them using the +:class:`Symfony\\Component\\HttpFoundation\\ChainRequestMatcher` class:: + + use Symfony\Component\HttpFoundation\ChainRequestMatcher; + use Symfony\Component\HttpFoundation\RequestMatcher\HostRequestMatcher; + use Symfony\Component\HttpFoundation\RequestMatcher\PathRequestMatcher; + use Symfony\Component\HttpFoundation\RequestMatcher\SchemeRequestMatcher; + + // use only one criteria to match the request + $schemeMatcher = new SchemeRequestMatcher('https'); + if ($schemeMatcher->matches($request)) { + // ... + } + + // use a set of criteria to match the request + $matcher = new ChainRequestMatcher([ + new HostRequestMatcher('example.com'), + new PathRequestMatcher('/admin'), + ]); + + if ($matcher->matches($request)) { + // ... + } + +.. versionadded:: 7.1 + + The ``HeaderRequestMatcher`` and ``QueryParameterRequestMatcher`` were + introduced in Symfony 7.1. + Accessing other Data ~~~~~~~~~~~~~~~~~~~~ @@ -462,6 +536,14 @@ Sending the response to the client is done by calling the method $response->send(); +The ``send()`` method takes an optional ``flush`` argument. If set to +``false``, functions like ``fastcgi_finish_request()`` or +``litespeed_finish_request()`` are not called. This is useful when debugging +your application to see which exceptions are thrown in listeners of the +:class:`Symfony\\Component\\HttpKernel\\Event\\TerminateEvent`. You can learn +more about it in +:ref:`the dedicated section about Kernel events `. + Setting Cookies ~~~~~~~~~~~~~~~ @@ -492,9 +574,15 @@ a new object with the modified property:: ->withDomain('.example.com') ->withSecure(true); -.. versionadded:: 5.1 +It is possible to define partitioned cookies, also known as `CHIPS`_, by using the +:method:`Symfony\\Component\\HttpFoundation\\Cookie::withPartitioned` method:: - The ``with*()`` methods were introduced in Symfony 5.1. + $cookie = Cookie::create('foo') + ->withValue('bar') + ->withPartitioned(); + + // you can also set the partitioned argument to true when using the `create()` factory method + $cookie = Cookie::create('name', 'value', partitioned: true); Managing the HTTP Cache ~~~~~~~~~~~~~~~~~~~~~~~ @@ -508,6 +596,8 @@ of methods to manipulate the HTTP headers related to the cache: * :method:`Symfony\\Component\\HttpFoundation\\Response::setExpires` * :method:`Symfony\\Component\\HttpFoundation\\Response::setMaxAge` * :method:`Symfony\\Component\\HttpFoundation\\Response::setSharedMaxAge` +* :method:`Symfony\\Component\\HttpFoundation\\Response::setStaleIfError` +* :method:`Symfony\\Component\\HttpFoundation\\Response::setStaleWhileRevalidate` * :method:`Symfony\\Component\\HttpFoundation\\Response::setTtl` * :method:`Symfony\\Component\\HttpFoundation\\Response::setClientTtl` * :method:`Symfony\\Component\\HttpFoundation\\Response::setLastModified` @@ -535,16 +625,13 @@ call:: 'proxy_revalidate' => false, 'max_age' => 600, 's_maxage' => 600, + 'stale_if_error' => 86400, + 'stale_while_revalidate' => 60, 'immutable' => true, 'last_modified' => new \DateTime(), 'etag' => 'abcdef', ]); -.. versionadded:: 5.1 - - The ``must_revalidate``, ``no_cache``, ``no_store``, ``no_transform`` and - ``proxy_revalidate`` directives were introduced in Symfony 5.1. - To check if the Response validators (``ETag``, ``Last-Modified``) match a conditional value specified in the client Request, use the :method:`Symfony\\Component\\HttpFoundation\\Response::isNotModified` @@ -581,7 +668,7 @@ represented by a PHP callable instead of a string:: use Symfony\Component\HttpFoundation\StreamedResponse; $response = new StreamedResponse(); - $response->setCallback(function () { + $response->setCallback(function (): void { var_dump('Hello World'); flush(); sleep(2); @@ -604,6 +691,98 @@ represented by a PHP callable instead of a string:: // disables FastCGI buffering in nginx only for this response $response->headers->set('X-Accel-Buffering', 'no'); +Streaming a JSON Response +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`Symfony\\Component\\HttpFoundation\\StreamedJsonResponse` allows to +stream large JSON responses using PHP generators to keep the used resources low. + +The class constructor expects an array which represents the JSON structure and +includes the list of contents to stream. In addition to PHP generators, which are +recommended to minimize memory usage, it also supports any kind of PHP Traversable +containing JSON serializable data:: + + use Symfony\Component\HttpFoundation\StreamedJsonResponse; + + // any method or function returning a PHP Generator + function loadArticles(): \Generator { + yield ['title' => 'Article 1']; + yield ['title' => 'Article 2']; + yield ['title' => 'Article 3']; + }; + + $response = new StreamedJsonResponse( + // JSON structure with generators in which will be streamed as a list + [ + '_embedded' => [ + 'articles' => loadArticles(), + ], + ], + ); + +When loading data via Doctrine, you can use the ``toIterable()`` method to +fetch results row by row and minimize resources consumption. +See the `Doctrine Batch processing`_ documentation for more:: + + public function __invoke(): Response + { + return new StreamedJsonResponse( + [ + '_embedded' => [ + 'articles' => $this->loadArticles(), + ], + ], + ); + } + + public function loadArticles(): \Generator + { + // get the $entityManager somehow (e.g. via constructor injection) + $entityManager = ... + + $queryBuilder = $entityManager->createQueryBuilder(); + $queryBuilder->from(Article::class, 'article'); + $queryBuilder->select('article.id') + ->addSelect('article.title') + ->addSelect('article.description'); + + return $queryBuilder->getQuery()->toIterable(); + } + +If you return a lot of data, consider calling the :phpfunction:`flush` function +after some specific item count to send the contents to the browser:: + + public function loadArticles(): \Generator + { + // ... + + $count = 0; + foreach ($queryBuilder->getQuery()->toIterable() as $article) { + yield $article; + + if (0 === ++$count % 100) { + flush(); + } + } + } + +Alternatively, you can also pass any iterable to ``StreamedJsonResponse``, +including generators:: + + public function loadArticles(): \Generator + { + yield ['title' => 'Article 1']; + yield ['title' => 'Article 2']; + yield ['title' => 'Article 3']; + } + + public function __invoke(): Response + { + // ... + + return new StreamedJsonResponse(loadArticles()); + } + .. _component-http-foundation-serving-files: Serving Files @@ -680,6 +859,23 @@ It is possible to delete the file after the response is sent with the :method:`Symfony\\Component\\HttpFoundation\\BinaryFileResponse::deleteFileAfterSend` method. Please note that this will not work when the ``X-Sendfile`` header is set. +Alternatively, ``BinaryFileResponse`` supports instances of ``\SplTempFileObject``. +This is useful when you want to serve a file that has been created in memory +and that will be automatically deleted after the response is sent:: + + use Symfony\Component\HttpFoundation\BinaryFileResponse; + + $file = new \SplTempFileObject(); + $file->fwrite('Hello World'); + $file->rewind(); + + $response = new BinaryFileResponse($file); + +.. versionadded:: 7.1 + + The support for ``\SplTempFileObject`` in ``BinaryFileResponse`` + was introduced in Symfony 7.1. + If the size of the served file is unknown (e.g. because it's being generated on the fly, or because a PHP stream filter is registered on it, etc.), you can pass a ``Stream`` instance to ``BinaryFileResponse``. This will disable ``Range`` and ``Content-Length`` @@ -793,11 +989,6 @@ Symfony offers two methods to interact with this preference: * :method:`Symfony\\Component\\HttpFoundation\\Request::preferSafeContent`; * :method:`Symfony\\Component\\HttpFoundation\\Response::setContentSafe`; -.. versionadded:: 5.1 - - The ``preferSafeContent()`` and ``setContentSafe()`` methods were introduced - in Symfony 5.1. - The following example shows how to detect if the user agent prefers "safe" content:: if ($request->preferSafeContent()) { @@ -810,10 +1001,6 @@ The following example shows how to detect if the user agent prefers "safe" conte Generating Relative and Absolute URLs ------------------------------------- -.. versionadded:: 5.4 - - The feature to generate relative and absolute URLs was introduced in Symfony 5.4. - Generating absolute and relative URLs for a given path is a common need in some applications. In Twig templates you can use the :ref:`absolute_url() ` and @@ -830,14 +1017,12 @@ methods. You can inject this as a service anywhere in your application:: class UserApiNormalizer { - private UrlHelper $urlHelper; - - public function __construct(UrlHelper $urlHelper) - { - $this->urlHelper = $urlHelper; + public function __construct( + private UrlHelper $urlHelper, + ) { } - public function normalize($user) + public function normalize($user): array { return [ 'avatar' => $this->urlHelper->getAbsoluteUrl($user->avatar()->path()), @@ -863,3 +1048,5 @@ Learn More .. _`valid JSON top-level value`: https://www.json.org/json-en.html .. _OWASP guidelines: https://cheatsheetseries.owasp.org/cheatsheets/AJAX_Security_Cheat_Sheet.html#always-return-json-with-an-object-on-the-outside .. _RFC 8674: https://tools.ietf.org/html/rfc8674 +.. _Doctrine Batch processing: https://www.doctrine-project.org/projects/doctrine-orm/en/2.14/reference/batch-processing.html#iterating-results +.. _`CHIPS`: https://developer.mozilla.org/en-US/docs/Web/Privacy/Partitioned_cookies diff --git a/components/http_kernel.rst b/components/http_kernel.rst index 3a367347a8d..97de70b66df 100644 --- a/components/http_kernel.rst +++ b/components/http_kernel.rst @@ -63,7 +63,7 @@ that system:: Request $request, int $type = self::MAIN_REQUEST, bool $catch = true - ); + ): Response; } Internally, :method:`HttpKernel::handle() ` - @@ -261,11 +261,6 @@ on the request's information. b) A new instance of your controller class is instantiated with no constructor arguments. - c) If the controller implements :class:`Symfony\\Component\\DependencyInjection\\ContainerAwareInterface`, - ``setContainer()`` is called on the controller object and the container - is passed to it. This step is also specific to the :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\ControllerResolver` - sub-class used by the Symfony Framework. - .. _component-http-kernel-kernel-controller: 3) The ``kernel.controller`` Event @@ -280,7 +275,11 @@ After the controller callable has been determined, ``HttpKernel::handle()`` dispatches the ``kernel.controller`` event. Listeners to this event might initialize some part of the system that needs to be initialized after certain things have been determined (e.g. the controller, routing information) but before -the controller is executed. For some examples, see the Symfony section below. +the controller is executed. + +Another typical use-case for this event is to retrieve the attributes from +the controller using the :method:`Symfony\\Component\\HttpKernel\\Event\\ControllerEvent::getAttributes` +method. See the Symfony section below for some examples. Listeners to this event can also change the controller callable completely by calling :method:`ControllerEvent::setController ` @@ -288,18 +287,15 @@ on the event object that's passed to listeners on this event. .. sidebar:: ``kernel.controller`` in the Symfony Framework - There are a few minor listeners to the ``kernel.controller`` event in - the Symfony Framework, and many deal with collecting profiler data when - the profiler is enabled. + An interesting listener to ``kernel.controller`` in the Symfony + Framework is :class:`Symfony\\Component\\HttpKernel\\EventListener\\CacheAttributeListener`. + This class fetches ``#[Cache]`` attribute configuration from the + controller and uses it to configure :doc:`HTTP caching ` + on the response. - One interesting listener comes from the `SensioFrameworkExtraBundle`_. This - listener's `@ParamConverter`_ functionality allows you to pass a full object - (e.g. a ``Post`` object) to your controller instead of a scalar value (e.g. - an ``id`` parameter that was on your route). The listener - - ``ParamConverterListener`` - uses reflection to look at each of the - arguments of the controller and tries to use different methods to convert - those to objects, which are then stored in the ``attributes`` property of - the ``Request`` object. Read the next section to see why this is important. + There are a few other minor listeners to the ``kernel.controller`` event in + the Symfony Framework that deal with collecting profiler data when the + profiler is enabled. 4) Getting the Controller Arguments ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -338,10 +334,10 @@ of arguments that should be passed when executing that callable. available through the `variadic`_ argument. This functionality is provided by resolvers implementing the - :class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentValueResolverInterface`. + :class:`Symfony\\Component\\HttpKernel\\Controller\\ValueResolverInterface`. There are four implementations which provide the default behavior of Symfony but customization is the key here. By implementing the - ``ArgumentValueResolverInterface`` yourself and passing this to the + ``ValueResolverInterface`` yourself and passing this to the ``ArgumentResolver``, you can extend this functionality. .. _component-http-kernel-calling-controller: @@ -400,12 +396,12 @@ return a ``Response``. .. sidebar:: ``kernel.view`` in the Symfony Framework - There is no default listener inside the Symfony Framework for the ``kernel.view`` - event. However, `SensioFrameworkExtraBundle`_ *does* add a listener to this - event. If your controller returns an array, and you place the `@Template`_ - annotation above the controller, then this listener renders a template, - passes the array you returned from your controller to that template, and - creates a ``Response`` containing the returned content from that template. + There is a default listener inside the Symfony Framework for the ``kernel.view`` + event. If your controller action returns an array, and you apply the + :ref:`#[Template] attribute ` to that + controller action, then this listener renders a template, passes the array + you returned from your controller to that template, and creates a ``Response`` + containing the returned content from that template. Additionally, a popular community bundle `FOSRestBundle`_ implements a listener on this event which aims to give you a robust view layer @@ -524,6 +520,17 @@ comes with an :class:`Symfony\\Component\\HttpKernel\\EventListener\\ErrorListen which if you choose to use, will do this and more by default (see the sidebar below for more details). +The :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` exposes the +:method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::isKernelTerminating` +method, which you can use to determine if the kernel is currently terminating +at the moment the exception was thrown. + +.. versionadded:: 7.1 + + The + :method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::isKernelTerminating` + method was introduced in Symfony 7.1. + .. note:: When setting a response for the ``kernel.exception`` event, the propagation @@ -630,7 +637,7 @@ else that can be used to create a working example:: $routes = new RouteCollection(); $routes->add('hello', new Route('/hello/{name}', [ - '_controller' => function (Request $request) { + '_controller' => function (Request $request): Response { return new Response( sprintf("Hello %s", $request->get('name')) ); @@ -701,7 +708,7 @@ look like this:: use Symfony\Component\HttpKernel\Event\RequestEvent; // ... - public function onKernelRequest(RequestEvent $event) + public function onKernelRequest(RequestEvent $event): void { if (!$event->isMainRequest()) { return; @@ -750,7 +757,4 @@ Learn more .. _reflection: https://www.php.net/manual/en/book.reflection.php .. _FOSRestBundle: https://github.com/friendsofsymfony/FOSRestBundle .. _`PHP FPM`: https://www.php.net/manual/en/install.fpm.php -.. _`SensioFrameworkExtraBundle`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html -.. _`@ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html -.. _`@Template`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/view.html .. _variadic: https://www.php.net/manual/en/functions.arguments.php#functions.variable-arg-list diff --git a/components/inflector.rst b/components/inflector.rst deleted file mode 100644 index 89cf170c904..00000000000 --- a/components/inflector.rst +++ /dev/null @@ -1,8 +0,0 @@ -The Inflector Component -======================= - -.. deprecated:: 5.1 - - The Inflector component was deprecated in Symfony 5.1 and its code was moved - into the :doc:`String ` component. - :ref:`Read the new Inflector docs `. diff --git a/components/intl.rst b/components/intl.rst index 8e4cfb5a9f6..ba3cbdcb959 100644 --- a/components/intl.rst +++ b/components/intl.rst @@ -3,13 +3,6 @@ The Intl Component This component provides access to the localization data of the `ICU library`_. -.. caution:: - - The replacement layer is limited to the ``en`` locale. If you want to use - other locales, you should `install the intl extension`_. There is no conflict - between the two because, even if you use the extension, this package can still - be useful to access the ICU data. - .. seealso:: This article explains how to use the Intl features as an independent component @@ -178,6 +171,37 @@ You may convert codes between two-letter alpha2 and three-letter alpha3 codes:: $alpha2Code = Countries::getAlpha2Code($alpha3Code); +Numeric Country Codes +~~~~~~~~~~~~~~~~~~~~~ + +The `ISO 3166-1 numeric`_ standard defines three-digit country codes to represent +countries, dependent territories, and special areas of geographical interest. + +The main advantage over the ISO 3166-1 alphabetic codes (alpha-2 and alpha-3) is +that these numeric codes are independent from the writing system. The alphabetic +codes use the 26-letter English alphabet, which might be unavailable or difficult +to use for people and systems using non-Latin scripts (e.g. Arabic or Japanese). + +The :class:`Symfony\\Component\\Intl\\Countries` class provides access to these +numeric country codes:: + + use Symfony\Component\Intl\Countries; + + \Locale::setDefault('en'); + + $numericCodes = Countries::getNumericCodes(); + // ('alpha2Code' => 'numericCode') + // => ['AA' => '958', 'AD' => '020', ...] + + $numericCode = Countries::getNumericCode('FR'); + // => '250' + + $alpha2 = Countries::getAlpha2FromNumeric('250'); + // => 'FR' + + $exists = Countries::numericCodeExists('250'); + // => true + Locales ~~~~~~~ @@ -246,10 +270,6 @@ can change if the number is used in cash transactions or in other scenarios $fractionDigits = Currencies::getFractionDigits('SEK'); // returns: 2 $cashFractionDigits = Currencies::getCashFractionDigits('SEK'); // returns: 0 -.. versionadded:: 5.3 - - The ``getCashFractionDigits()`` method was introduced in Symfony 5.3. - Some currencies require to round numbers to the nearest increment of some value (e.g. 5 cents). This increment might be different if numbers are formatted for cash transactions or other scenarios (e.g. accounting):: @@ -264,10 +284,6 @@ cash transactions or other scenarios (e.g. accounting):: $roundingIncrement = Currencies::getRoundingIncrement('CAD'); // returns: 0 $cashRoundingIncrement = Currencies::getCashRoundingIncrement('CAD'); // returns: 5 -.. versionadded:: 5.3 - - The ``getCashRoundingIncrement()`` method was introduced in Symfony 5.3. - All methods (except for ``getFractionDigits()``, ``getCashFractionDigits()``, ``getRoundingIncrement()`` and ``getCashRoundingIncrement()``) accept the translation locale as the last, optional parameter, which defaults to the @@ -364,6 +380,27 @@ to catching the exception, you can also check if a given timezone ID is valid:: $isValidTimezone = Timezones::exists($timezoneId); +.. _component-intl-emoji-transliteration: + +Emoji Transliteration +~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides utilities to translate emojis into their textual representation +in all languages. Read the documentation about :ref:`emoji transliteration ` +to learn more about this feature. + +Disk Space +---------- + +If you need to save disk space (e.g. because you deploy to some service with tight size +constraints), run this command (e.g. as an automated script after ``composer install``) to compress the +internal Symfony Intl data files using the PHP ``zlib`` extension: + +.. code-block:: terminal + + # adjust the path to the 'compress' binary based on your application installation + $ php ./vendor/symfony/intl/Resources/bin/compress + Learn more ---------- @@ -377,11 +414,11 @@ Learn more /reference/forms/types/locale /reference/forms/types/timezone -.. _install the intl extension: https://www.php.net/manual/en/intl.setup.php .. _ICU library: https://icu.unicode.org/ .. _`Unicode ISO 15924 Registry`: https://www.unicode.org/iso15924/iso15924-codes.html .. _`ISO 3166-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 .. _`ISO 3166-1 alpha-3`: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3 +.. _`ISO 3166-1 numeric`: https://en.wikipedia.org/wiki/ISO_3166-1_numeric .. _`UTC/GMT time offsets`: https://en.wikipedia.org/wiki/List_of_UTC_time_offsets .. _`daylight saving time (DST)`: https://en.wikipedia.org/wiki/Daylight_saving_time .. _`ISO 639-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_639-1 diff --git a/components/ldap.rst b/components/ldap.rst index a0bec3c25dd..89094fad0b7 100644 --- a/components/ldap.rst +++ b/components/ldap.rst @@ -156,11 +156,6 @@ delete existing ones:: // Removing an existing entry $entryManager->remove(new Entry('cn=Test User,dc=symfony,dc=com')); -.. versionadded:: 5.3 - - The option to make attribute names case-insensitive in ``getAttribute()`` - and ``hasAttribute()`` was introduced in Symfony 5.3. - Batch Updating ______________ diff --git a/components/lock.rst b/components/lock.rst index e97d66862f2..5a76223112b 100644 --- a/components/lock.rst +++ b/components/lock.rst @@ -83,13 +83,10 @@ key of the lock:: class RefreshTaxonomy { - private object $article; - private Key $key; - - public function __construct(object $article, Key $key) - { - $this->article = $article; - $this->key = $key; + public function __construct( + private object $article, + private Key $key, + ) { } public function getArticle(): object @@ -157,12 +154,6 @@ When the store does not support blocking locks by implementing the will retry to acquire the lock in a non-blocking way until the lock is acquired. -.. versionadded:: 5.2 - - Default logic to retry acquiring a non-blocking lock was introduced in - Symfony 5.2. Prior to 5.2, you needed to wrap a store without support - for blocking locks in :class:`Symfony\\Component\\Lock\\Store\\RetryTillSaveStore`. - Expiring Locks -------------- @@ -285,11 +276,6 @@ for 3600 seconds or until ``Lock::release()`` is called:: Shared Locks ------------ -.. versionadded:: 5.2 - - Shared locks (and the associated ``acquireRead()`` method and - ``SharedLockStoreInterface``) were introduced in Symfony 5.2. - A shared or `readers-writer lock`_ is a synchronization primitive that allows concurrent access for read-only operations, while write operations require exclusive access. This means that multiple threads can read the data in parallel @@ -467,10 +453,6 @@ support blocking, and expects a TTL to avoid stalled locks:: MongoDbStore ~~~~~~~~~~~~ -.. versionadded:: 5.1 - - The ``MongoDbStore`` was introduced in Symfony 5.1. - The MongoDbStore saves locks on a MongoDB server ``>=2.2``, it requires a ``\MongoDB\Collection`` or ``\MongoDB\Client`` from `mongodb/mongodb`_ or a `MongoDB Connection String`_. @@ -481,7 +463,7 @@ avoid stalled locks:: $mongo = 'mongodb://localhost/database?collection=lock'; $options = [ - 'gcProbablity' => 0.001, + 'gcProbability' => 0.001, 'database' => 'myapp', 'collection' => 'lock', 'uriOptions' => [], @@ -494,7 +476,7 @@ The ``MongoDbStore`` takes the following ``$options`` (depending on the first pa ============= ================================================================================================ Option Description ============= ================================================================================================ -gcProbablity Should a TTL Index be created expressed as a probability from 0.0 to 1.0 (Defaults to ``0.001``) +gcProbability Should a TTL Index be created expressed as a probability from 0.0 to 1.0 (Defaults to ``0.001``) database The name of the database collection The name of the collection uriOptions Array of URI options for `MongoDBClient::__construct`_ @@ -528,13 +510,12 @@ MongoDB Connection String: PdoStore ~~~~~~~~ -The PdoStore saves locks in an SQL database. It is identical to DoctrineDbalStore -but requires a `PDO`_ connection or a `Data Source Name (DSN)`_. This store does -not support blocking, and expects a TTL to avoid stalled locks:: +The PdoStore saves locks in an SQL database. It requires a `PDO`_ connection or a `Data Source Name (DSN)`_. +This store does not support blocking, and expects a TTL to avoid stalled locks:: use Symfony\Component\Lock\Store\PdoStore; - // a PDO or DSN for lazy connecting through PDO + // a PDO instance or DSN for lazy connecting through PDO $databaseConnectionOrDSN = 'mysql:host=127.0.0.1;dbname=app'; $store = new PdoStore($databaseConnectionOrDSN, ['db_username' => 'myuser', 'db_password' => 'mypassword']); @@ -548,11 +529,6 @@ You can also create this table explicitly by calling the :method:`Symfony\\Component\\Lock\\Store\\PdoStore::createTable` method in your code. -.. deprecated:: 5.4 - - Using ``PdoStore`` with Doctrine DBAL is deprecated in Symfony 5.4. - Use ``DoctrineDbalStore`` instead. - .. _lock-store-dbal: DoctrineDbalStore @@ -572,26 +548,30 @@ does not support blocking, and expects a TTL to avoid stalled locks:: This store does not support TTL lower than 1 second. -The table where values are stored is created automatically on the first call to -the :method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::save` method. +The table where values are stored will be automatically generated when your run +the command: + +.. code-block:: terminal + + $ php bin/console make:migration + +If you prefer to create the table yourself and it has not already been created, you can +create this table explicitly by calling the +:method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::createTable` method. You can also add this table to your schema by calling :method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::configureSchema` method -in your code or create this table explicitly by calling the -:method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::createTable` method. - -.. versionadded:: 5.4 +in your code - The ``DoctrineDbalStore`` was introduced in Symfony 5.4 to replace ``PdoStore`` - when used with Doctrine DBAL. +If the table has not been created upstream, it will be created automatically on the first call to +the :method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::save` method. .. _lock-store-pgsql: PostgreSqlStore ~~~~~~~~~~~~~~~ -The PostgreSqlStore and DoctrineDbalPostgreSqlStore uses `Advisory Locks`_ provided by PostgreSQL. -It is identical to DoctrineDbalPostgreSqlStore but requires `PDO`_ connection or -a `Data Source Name (DSN)`_. It supports native blocking, as well as sharing +The PostgreSqlStore uses `Advisory Locks`_ provided by PostgreSQL. It requires a +`PDO`_ connection or a `Data Source Name (DSN)`_. It supports native blocking, as well as sharing locks:: use Symfony\Component\Lock\Store\PostgreSqlStore; @@ -603,15 +583,6 @@ locks:: In opposite to the ``PdoStore``, the ``PostgreSqlStore`` does not need a table to store locks and it does not expire. -.. versionadded:: 5.2 - - The ``PostgreSqlStore`` was introduced in Symfony 5.2. - -.. deprecated:: 5.4 - - Using ``PostgreSqlStore`` with Doctrine DBAL is deprecated in Symfony 5.4. - Use ``DoctrineDbalPostgreSqlStore`` instead. - .. _lock-store-dbal-pgsql: DoctrineDbalPostgreSqlStore @@ -630,18 +601,13 @@ a `Doctrine DBAL URL`_. It supports native blocking, as well as sharing locks:: In opposite to the ``DoctrineDbalStore``, the ``DoctrineDbalPostgreSqlStore`` does not need a table to store locks and does not expire. -.. versionadded:: 5.4 - - The ``DoctrineDbalPostgreSqlStore`` was introduced in Symfony 5.4 to replace - ``PostgreSqlStore`` when used with Doctrine DBAL. - .. _lock-store-redis: RedisStore ~~~~~~~~~~ The RedisStore saves locks on a Redis server, it requires a Redis connection -implementing the ``\Redis``, ``\RedisArray``, ``\RedisCluster`` or +implementing the ``\Redis``, ``\RedisArray``, ``\RedisCluster``, ``\Relay\Relay`` or ``\Predis`` classes. This store does not support blocking, and expects a TTL to avoid stalled locks:: @@ -905,7 +871,7 @@ about `Expire Data from Collections by Setting TTL`_ in MongoDB. .. tip:: ``MongoDbStore`` will attempt to automatically create a TTL index. It's - recommended to set constructor option ``gcProbablity`` to ``0.0`` to + recommended to set constructor option ``gcProbability`` to ``0.0`` to disable this behavior if you have manually dealt with TTL index creation. .. caution:: diff --git a/components/messenger.rst b/components/messenger.rst index e26e7838107..8d6652fb160 100644 --- a/components/messenger.rst +++ b/components/messenger.rst @@ -113,7 +113,7 @@ that will do the required processing for your message:: class MyMessageHandler { - public function __invoke(MyMessage $message) + public function __invoke(MyMessage $message): void { // Message processing... } @@ -146,7 +146,7 @@ Here are some important envelope stamps that are shipped with the Symfony Messen to delay handling of an asynchronous message. * :class:`Symfony\\Component\\Messenger\\Stamp\\DispatchAfterCurrentBusStamp`, to make the message be handled after the current bus has executed. Read more - at :doc:`/messenger/dispatch_after_current_bus`. + at :ref:`messenger-transactional-messages`. * :class:`Symfony\\Component\\Messenger\\Stamp\\HandledStamp`, a stamp that marks the message as handled by a specific handler. Allows accessing the handler returned value and the handler name. @@ -162,6 +162,10 @@ Here are some important envelope stamps that are shipped with the Symfony Messen to configure the validation groups used when the validation middleware is enabled. * :class:`Symfony\\Component\\Messenger\\Stamp\\ErrorDetailsStamp`, an internal stamp when a message fails due to an exception in the handler. +* :class:`Symfony\\Component\\Scheduler\\Messenger\\ScheduledStamp`, + a stamp that marks the message as produced by a scheduler. This helps + differentiate it from messages created "manually". You can learn more about it + in the :doc:`Scheduler documentation `. .. note:: @@ -174,11 +178,6 @@ Here are some important envelope stamps that are shipped with the Symfony Messen :class:`Symfony\\Component\\Messenger\\Transport\\Serialization\\Normalizer\\FlattenExceptionNormalizer` which helps error reporting in the Messenger context. -.. versionadded:: 5.2 - - The ``ErrorDetailsStamp`` stamp and the ``FlattenExceptionNormalizer`` - were introduced in Symfony 5.2. - Instead of dealing directly with the messages in the middleware you receive the envelope. Hence you can inspect the envelope content and its stamps, or add any:: @@ -248,13 +247,10 @@ you can create your own message sender:: class ImportantActionToEmailSender implements SenderInterface { - private $mailer; - private $toEmail; - - public function __construct(MailerInterface $mailer, string $toEmail) - { - $this->mailer = $mailer; - $this->toEmail = $toEmail; + public function __construct( + private MailerInterface $mailer, + private string $toEmail, + ) { } public function send(Envelope $envelope): Envelope @@ -300,15 +296,12 @@ do is to write your own CSV receiver:: class NewOrdersFromCsvFileReceiver implements ReceiverInterface { - private $serializer; - private $filePath; private $connection; - public function __construct(SerializerInterface $serializer, string $filePath) - { - $this->serializer = $serializer; - $this->filePath = $filePath; - + public function __construct( + private SerializerInterface $serializer, + private string $filePath, + ) { // Available connection bundled with the Messenger component // can be found in "Symfony\Component\Messenger\Bridge\*\Transport\Connection". $this->connection = /* create your connection */; diff --git a/components/options_resolver.rst b/components/options_resolver.rst index c01f727139a..c8052d0d395 100644 --- a/components/options_resolver.rst +++ b/components/options_resolver.rst @@ -23,7 +23,7 @@ Imagine you have a ``Mailer`` class which has four options: ``host``, class Mailer { - protected $options; + protected array $options; public function __construct(array $options = []) { @@ -37,7 +37,7 @@ check which options are set:: class Mailer { // ... - public function sendMail($from, $to) + public function sendMail($from, $to): void { $mail = ...; @@ -121,7 +121,7 @@ code:: { // ... - public function sendMail($from, $to) + public function sendMail($from, $to): void { $mail = ...; $mail->setHost($this->options['host']); @@ -147,7 +147,7 @@ It's a good practice to split the option configuration into a separate method:: $this->options = $resolver->resolve($options); } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'host' => 'smtp.example.org', @@ -166,7 +166,7 @@ than processing options. Second, sub-classes may now override the // ... class GoogleMailer extends Mailer { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); @@ -189,7 +189,7 @@ For example, to make the ``host`` option required, you can do:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setRequired('host'); @@ -213,7 +213,7 @@ one required option:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setRequired(['host', 'username', 'password']); @@ -228,7 +228,7 @@ retrieve the names of all required options:: // ... class GoogleMailer extends Mailer { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); @@ -251,7 +251,7 @@ been set:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setRequired('host'); @@ -261,7 +261,7 @@ been set:: // ... class GoogleMailer extends Mailer { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); @@ -296,7 +296,7 @@ correctly. To validate the types of the options, call { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... @@ -347,7 +347,7 @@ to verify that the passed option contains one of these values:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setDefault('transport', 'sendmail'); @@ -370,7 +370,7 @@ For options with more complicated validation schemes, pass a closure which returns ``true`` for acceptable values and ``false`` for invalid values:: // ... - $resolver->setAllowedValues('transport', function ($value) { + $resolver->setAllowedValues('transport', function (string $value): bool { // return true or false }); @@ -408,12 +408,12 @@ option. You can configure a normalizer by calling { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... - $resolver->setNormalizer('host', function (Options $options, $value) { - if ('http://' !== substr($value, 0, 7)) { + $resolver->setNormalizer('host', function (Options $options, string $value): string { + if (!str_starts_with($value, 'http://')) { $value = 'http://'.$value; } @@ -430,11 +430,11 @@ if you need to use other options during normalization:: class Mailer { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... - $resolver->setNormalizer('host', function (Options $options, $value) { - if ('http://' !== substr($value, 0, 7) && 'https://' !== substr($value, 0, 8)) { + $resolver->setNormalizer('host', function (Options $options, string $value): string { + if (!str_starts_with($value, 'http://') && !str_starts_with($value, 'https://')) { if ('ssl' === $options['encryption']) { $value = 'https://'.$value; } else { @@ -470,12 +470,12 @@ these options, you can return the desired default value:: class Mailer { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setDefault('encryption', null); - $resolver->setDefault('port', function (Options $options) { + $resolver->setDefault('port', function (Options $options): int { if ('ssl' === $options['encryption']) { return 465; } @@ -502,7 +502,7 @@ the closure:: class Mailer { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setDefaults([ @@ -514,11 +514,11 @@ the closure:: class GoogleMailer extends Mailer { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); - $resolver->setDefault('host', function (Options $options, $previousValue) { + $resolver->setDefault('host', function (Options $options, string $previousValue): string { if ('ssl' === $options['encryption']) { return 'secure.example.org'; } @@ -545,14 +545,14 @@ from the default:: class Mailer { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setDefault('port', 25); } // ... - public function sendMail($from, $to) + public function sendMail(string $from, string $to): void { // Is this the default value or did the caller of the class really // set the port to 25? @@ -572,14 +572,14 @@ be included in the resolved options if it was actually passed to { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setDefined('port'); } // ... - public function sendMail($from, $to) + public function sendMail(string $from, string $to): void { if (array_key_exists('port', $this->options)) { echo 'Set!'; @@ -606,7 +606,7 @@ options in one go:: class Mailer { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setDefined(['port', 'encryption']); @@ -622,7 +622,7 @@ let you find out which options are defined:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); @@ -652,9 +652,9 @@ default value:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { - $resolver->setDefault('spool', function (OptionsResolver $spoolResolver) { + $resolver->setDefault('spool', function (OptionsResolver $spoolResolver): void { $spoolResolver->setDefaults([ 'type' => 'file', 'path' => '/path/to/spool', @@ -664,7 +664,7 @@ default value:: }); } - public function sendMail($from, $to) + public function sendMail(string $from, string $to): void { if ('memory' === $this->options['spool']['type']) { // ... @@ -687,10 +687,10 @@ to the closure to access to them:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('sandbox', false); - $resolver->setDefault('spool', function (OptionsResolver $spoolResolver, Options $parent) { + $resolver->setDefault('spool', function (OptionsResolver $spoolResolver, Options $parent): void { $spoolResolver->setDefaults([ 'type' => $parent['sandbox'] ? 'memory' : 'file', // ... @@ -711,15 +711,15 @@ In same way, parent options can access to the nested options as normal arrays:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { - $resolver->setDefault('spool', function (OptionsResolver $spoolResolver) { + $resolver->setDefault('spool', function (OptionsResolver $spoolResolver): void { $spoolResolver->setDefaults([ 'type' => 'file', // ... ]); }); - $resolver->setDefault('profiling', function (Options $options) { + $resolver->setDefault('profiling', function (Options $options): void { return 'file' === $options['spool']['type']; }); } @@ -733,10 +733,6 @@ In same way, parent options can access to the nested options as normal arrays:: Prototype Options ~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.3 - - Prototype options were introduced in Symfony 5.3. - There are situations where you will have to resolve and validate a set of options that may repeat many times within another option. Let's imagine a ``connections`` option that will accept an array of database connections @@ -744,7 +740,7 @@ with ``host``, ``database``, ``user`` and ``password`` each. The best way to implement this is to define the ``connections`` option as prototype:: - $resolver->setDefault('connections', function (OptionsResolver $connResolver) { + $resolver->setDefault('connections', function (OptionsResolver $connResolver): void { $connResolver ->setPrototype(true) ->setRequired(['host', 'database']) @@ -782,13 +778,6 @@ connections. Deprecating the Option ~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.1 - - The signature of the ``setDeprecated()`` method changed from - ``setDeprecated(string $option, ?string $message)`` to - ``setDeprecated(string $option, string $package, string $version, $message)`` - in Symfony 5.1. - Once an option is outdated or you decided not to maintain it anymore, you can deprecate it using the :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setDeprecated` method:: @@ -838,7 +827,7 @@ the option:: ->setDefault('encryption', null) ->setDefault('port', null) ->setAllowedTypes('port', ['null', 'int']) - ->setDeprecated('port', 'acme/package', '1.2', function (Options $options, $value) { + ->setDeprecated('port', 'acme/package', '1.2', function (Options $options, ?int $value): string { if (null === $value) { return 'Passing "null" to option "port" is deprecated, pass an integer instead.'; } @@ -860,6 +849,26 @@ the option:: This closure receives as argument the value of the option after validating it and before normalizing it when the option is being resolved. +Ignore not defined Options +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, all options are resolved and validated, resulting in a +:class:`Symfony\\Component\\OptionsResolver\\Exception\\UndefinedOptionsException` +if an unknown option is passed. You can ignore not defined options by using the +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::ignoreUndefined` method:: + + // ... + $resolver + ->setDefined(['hostname']) + ->setIgnoreUndefined(true) + ; + + // option "version" will be ignored + $resolver->resolve([ + 'hostname' => 'acme/package', + 'version' => '1.2.3' + ]); + Chaining Option Configurations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -874,7 +883,7 @@ method:: class InvoiceMailer { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->define('host') @@ -890,10 +899,6 @@ method:: } } -.. versionadded:: 5.1 - - The ``define()`` and ``info()`` methods were introduced in Symfony 5.1. - Performance Tweaks ~~~~~~~~~~~~~~~~~~ @@ -906,9 +911,9 @@ can change your code to do the configuration only once per class:: // ... class Mailer { - private static $resolversByClass = []; + private static array $resolversByClass = []; - protected $options; + protected array $options; public function __construct(array $options = []) { @@ -924,7 +929,7 @@ can change your code to do the configuration only once per class:: $this->options = self::$resolversByClass[$class]->resolve($options); } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... } @@ -939,9 +944,9 @@ method ``clearOptionsConfig()`` and call it periodically:: // ... class Mailer { - private static $resolversByClass = []; + private static array $resolversByClass = []; - public static function clearOptionsConfig() + public static function clearOptionsConfig(): void { self::$resolversByClass = []; } diff --git a/components/phpunit_bridge.rst b/components/phpunit_bridge.rst index b1965cca0d6..ba37bc0ecda 100644 --- a/components/phpunit_bridge.rst +++ b/components/phpunit_bridge.rst @@ -7,8 +7,8 @@ The PHPUnit Bridge It comes with the following features: -* Forces the tests to use a consistent locale (``C``) (if you create - locale-sensitive tests, use PHPUnit's ``setLocale()`` method); +* Sets by default a consistent locale (``C``) for your tests (if you + create locale-sensitive tests, use PHPUnit's ``setLocale()`` method); * Auto-register ``class_exists`` to load Doctrine annotations (when used); @@ -19,10 +19,11 @@ It comes with the following features: * Provides a ``ClockMock``, ``DnsMock`` and ``ClassExistsMock`` classes for tests sensitive to time, network or class existence; -* Provides a modified version of PHPUnit that allows 1. separating the - dependencies of your app from those of phpunit to prevent any unwanted - constraints to apply; 2. running tests in parallel when a test suite is split - in several phpunit.xml files; 3. recording and replaying skipped tests; +* Provides a modified version of PHPUnit that allows: + + #. separating the dependencies of your app from those of phpunit to prevent any unwanted constraints to apply; + #. running tests in parallel when a test suite is split in several phpunit.xml files; + #. recording and replaying skipped tests; * It allows to create tests that are compatible with multiple PHPUnit versions (because it provides polyfills for missing methods, namespaced aliases for @@ -287,13 +288,38 @@ Here is a summary that should help you pick the right configuration: | | cannot afford to use one of the modes above. | +------------------------+-----------------------------------------------------+ -Baseline Deprecations +Ignoring Deprecations ..................... If your application has some deprecations that you can't fix for some reasons, -you can tell Symfony to ignore them. The trick is to create a file with the -allowed deprecations and define it as the "deprecation baseline". Deprecations -inside that file are ignored but the rest of deprecations are still reported. +you can tell Symfony to ignore them. + +You need first to create a text file where each line is a deprecation to ignore +defined as a regular expression. Lines beginning with a hash (``#``) are +considered comments: + +.. code-block:: terminal + + # This file contains patterns to be ignored while testing for use of + # deprecated code. + + %The "Symfony\\Component\\Validator\\Context\\ExecutionContextInterface::.*\(\)" method is considered internal Used by the validator engine\. (Should not be called by user\W+code\. )?It may change without further notice\. You should not extend it from "[^"]+"\.% + %The "PHPUnit\\Framework\\TestCase::addWarning\(\)" method is considered internal% + +Then, you can run the following command to use that file and ignore those deprecations: + +.. code-block:: terminal + + $ SYMFONY_DEPRECATIONS_HELPER='ignoreFile=./tests/baseline-ignore' ./vendor/bin/simple-phpunit + +Baseline Deprecations +..................... + +You can also take a snapshot of deprecations currently triggered by your application +code, and ignore those during your test runs, still reporting newly added ones. +The trick is to create a file with the allowed deprecations and define it as the +"deprecation baseline". Deprecations inside that file are ignored but the rest of +deprecations are still reported. First, generate the file with the allowed deprecations (run the same command whenever you want to update the existing file): @@ -311,11 +337,6 @@ Then, you can run the following command to use that file and ignore those deprec $ SYMFONY_DEPRECATIONS_HELPER='baselineFile=./tests/allowed.json' ./vendor/bin/simple-phpunit -.. versionadded:: 5.2 - - The ``baselineFile`` and ``generateBaseline`` options were introduced in - Symfony 5.2. - Disabling the Verbose Output ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -331,10 +352,6 @@ The ``quiet`` option hides details for the specified deprecation types, but will not change the outcome in terms of exit code. That's what :ref:`max ` is for, and both settings are orthogonal. -.. versionadded:: 5.1 - - The ``quiet`` option was introduced in Symfony 5.1. - Disabling the Deprecation Helper ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -375,19 +392,44 @@ the compiling and warming up of the container: $ php bin/console debug:container --deprecations -.. versionadded:: 5.1 - - The ``--deprecations`` option was introduced in Symfony 5.1. - Log Deprecations ~~~~~~~~~~~~~~~~ For turning the verbose output off and write it to a log file instead you can use ``SYMFONY_DEPRECATIONS_HELPER='logFile=/path/deprecations.log'``. -.. versionadded:: 5.3 +Setting The Locale For Tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the PHPUnit Bridge forces the locale to ``C`` to avoid locale +issues in tests. This behavior can be changed by setting the +``SYMFONY_PHPUNIT_LOCALE`` environment variable to the desired locale: + +.. code-block:: bash + + # .env.test + SYMFONY_PHPUNIT_LOCALE="fr_FR" + +Alternatively, you can set this environment variable in the PHPUnit +configuration file: + +.. code-block:: xml + + + + + - The ``logFile`` option was introduced in Symfony 5.3. + + + + + + +Finally, if you want to avoid the bridge to force any locale, you can set the +``SYMFONY_PHPUNIT_LOCALE`` environment variable to ``0``. .. _write-assertions-about-deprecations: @@ -412,7 +454,7 @@ times (order matters):: /** * @group legacy */ - public function testDeprecatedCode() + public function testDeprecatedCode(): void { // test some code that triggers the following deprecation: // trigger_deprecation('vendor-name/package-name', '5.1', 'This "Foo" method is deprecated.'); @@ -426,11 +468,6 @@ times (order matters):: } } -.. deprecated:: 5.1 - - Symfony versions previous to 5.1 also included a ``@expectedDeprecation`` - annotation to test deprecations, but it was deprecated in favor of the method. - Display the Full Stack Trace ---------------------------- @@ -482,36 +519,6 @@ PHPUnit to remove the return type (introduced in PHPUnit 8) from ``setUp()``, ``tearDown()``, ``setUpBeforeClass()`` and ``tearDownAfterClass()`` methods. This allows you to write a test compatible with both PHP 5 and PHPUnit 8. -Alternatively, you can use the trait :class:`Symfony\\Bridge\\PhpUnit\\SetUpTearDownTrait`, -which provides the right signature for the ``setUp()``, ``tearDown()``, -``setUpBeforeClass()`` and ``tearDownAfterClass()`` methods and delegates the -call to the ``doSetUp()``, ``doTearDown()``, ``doSetUpBeforeClass()`` and -``doTearDownAfterClass()`` methods:: - - use PHPUnit\Framework\TestCase; - use Symfony\Bridge\PhpUnit\SetUpTearDownTrait; - - class MyTest extends TestCase - { - // when using the SetUpTearDownTrait, methods like doSetUp() can - // be defined with and without the 'void' return type, as you wish - use SetUpTearDownTrait; - - private function doSetUp() - { - // ... - } - - protected function doSetUp(): void - { - // ... - } - } - -.. deprecated:: 5.3 - - The ``SetUpTearDownTrait`` was deprecated in Symfony 5.3. - Using Namespaced PHPUnit Classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -533,7 +540,7 @@ If you have this kind of time-related tests:: class MyTest extends TestCase { - public function testSomething() + public function testSomething(): void { $stopwatch = new Stopwatch(); @@ -559,8 +566,9 @@ Clock Mocking The :class:`Symfony\\Bridge\\PhpUnit\\ClockMock` class provided by this bridge allows you to mock the PHP's built-in time functions ``time()``, ``microtime()``, -``sleep()``, ``usleep()`` and ``gmdate()``. Additionally the function ``date()`` -is mocked so it uses the mocked time if no timestamp is specified. +``sleep()``, ``usleep()``, ``gmdate()``, and ``hrtime()``. Additionally the +function ``date()`` is mocked so it uses the mocked time if no timestamp is +specified. Other functions with an optional timestamp parameter that defaults to ``time()`` will still use the system time instead of the mocked time. This means that you @@ -599,7 +607,7 @@ test:: */ class MyTest extends TestCase { - public function testSomething() + public function testSomething(): void { $stopwatch = new Stopwatch(); @@ -627,7 +635,7 @@ different class, do it explicitly using ``ClockMock::register(MyClass::class)``: class MyClass { - public function getTimeInHours() + public function getTimeInHours(): void { return time() / 3600; } @@ -645,7 +653,7 @@ different class, do it explicitly using ``ClockMock::register(MyClass::class)``: */ class MyTest extends TestCase { - public function testGetTimeInHours() + public function testGetTimeInHours(): void { ClockMock::register(MyClass::class); @@ -693,7 +701,7 @@ associated to a valid host:: class MyTest extends TestCase { - public function testEmail() + public function testEmail(): void { $validator = new DomainValidator(['checkDnsRecord' => true]); $isValid = $validator->validate('example.com'); @@ -715,7 +723,7 @@ the data you expect to get for the given hosts:: */ class DomainValidatorTest extends TestCase { - public function testEmails() + public function testEmails(): void { DnsMock::withMockedHosts([ 'example.com' => [['type' => 'A', 'ip' => '1.2.3.4']], @@ -756,6 +764,7 @@ reason, this component also provides mocks for these PHP functions: * :phpfunction:`class_exists` * :phpfunction:`interface_exists` * :phpfunction:`trait_exists` +* :phpfunction:`enum_exists` Use Case ~~~~~~~~ @@ -785,7 +794,7 @@ are installed during tests) would look like:: class MyClassTest extends TestCase { - public function testHello() + public function testHello(): void { $class = new MyClass(); $result = $class->hello(); // "The dependency behavior." @@ -806,7 +815,7 @@ classes, interfaces and/or traits for the code to run:: { // ... - public function testHelloDefault() + public function testHelloDefault(): void { ClassExistsMock::register(MyClass::class); ClassExistsMock::withMockedClasses([DependencyClass::class => false]); @@ -818,6 +827,16 @@ classes, interfaces and/or traits for the code to run:: } } +Note that mocking a class with ``ClassExistsMock::withMockedClasses()`` +will make :phpfunction:`class_exists`, :phpfunction:`interface_exists` +and :phpfunction:`trait_exists` return true. + +To register an enumeration and mock :phpfunction:`enum_exists`, +``ClassExistsMock::withMockedEnums()`` must be used. Note that, like in +PHP 8.1 and later, calling ``class_exists`` on a enum will return ``true``. +That's why calling ``ClassExistsMock::withMockedEnums()`` will also register the enum +as a mocked class. + Troubleshooting --------------- @@ -929,11 +948,6 @@ If you have installed the bridge through Composer, you can run it by calling e.g of PHPUnit to be considered. This is useful when testing a framework that does not support the latest version(s) of PHPUnit. -.. versionadded:: 5.2 - - The ``SYMFONY_MAX_PHPUNIT_VERSION`` env variable was introduced in - Symfony 5.2. - .. tip:: If you still need to use ``prophecy`` (but not ``symfony/yaml``), @@ -957,11 +971,6 @@ If you have installed the bridge through Composer, you can run it by calling e.g - .. versionadded:: 5.3 - - The ``SYMFONY_PHPUNIT_REQUIRE`` env variable was introduced in - Symfony 5.3. - Code Coverage Listener ---------------------- @@ -974,7 +983,7 @@ Consider the following example:: class Bar { - public function barMethod() + public function barMethod(): string { return 'bar'; } @@ -982,14 +991,12 @@ Consider the following example:: class Foo { - private $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; + public function __construct( + private Bar $bar, + ) { } - public function fooMethod() + public function fooMethod(): string { $this->bar->barMethod(); @@ -999,7 +1006,7 @@ Consider the following example:: class FooTest extends PHPUnit\Framework\TestCase { - public function test() + public function test(): void { $bar = new Bar(); $foo = new Foo($bar); diff --git a/components/process.rst b/components/process.rst index 163df6d9fdb..9502665dde1 100644 --- a/components/process.rst +++ b/components/process.rst @@ -100,10 +100,6 @@ with a non-zero code):: Configuring Process Options --------------------------- -.. versionadded:: 5.2 - - The feature to configure process options was introduced in Symfony 5.2. - Symfony uses the PHP :phpfunction:`proc_open` function to run the processes. You can configure the options passed to the ``other_options`` argument of ``proc_open()`` using the ``setOptions()`` method:: @@ -193,7 +189,7 @@ anonymous function to the use Symfony\Component\Process\Process; $process = new Process(['ls', '-lsa']); - $process->run(function ($type, $buffer) { + $process->run(function ($type, $buffer): void { if (Process::ERR === $type) { echo 'ERR > '.$buffer; } else { @@ -271,7 +267,7 @@ in the output and its type:: $process = new Process(['ls', '-lsa']); $process->start(); - $process->wait(function ($type, $buffer) { + $process->wait(function ($type, $buffer): void { if (Process::ERR === $type) { echo 'ERR > '.$buffer; } else { @@ -290,7 +286,7 @@ process and checks its output to wait until its fully initialized:: // ... do other things // waits until the given anonymous function returns true - $process->waitUntil(function ($type, $output) { + $process->waitUntil(function ($type, $output): bool { return $output === 'Ready. Waiting for commands...'; }); @@ -419,6 +415,36 @@ instead:: ); $process->run(); +Executing a PHP Child Process with the Same Configuration +--------------------------------------------------------- + +When you start a PHP process, it uses the default configuration defined in +your ``php.ini`` file. You can bypass these options with the ``-d`` command line +option. For example, if ``memory_limit`` is set to ``256M``, you can disable this +memory limit when running some command like this: +``php -d memory_limit=-1 bin/console app:my-command``. + +However, if you run the command via the Symfony ``Process`` class, PHP will use +the settings defined in the ``php.ini`` file. You can solve this issue by using +the :class:`Symfony\\Component\\Process\\PhpSubprocess` class to run the command:: + + use Symfony\Component\Process\Process; + + class MyCommand extends Command + { + protected function execute(InputInterface $input, OutputInterface $output): int + { + // the memory_limit (and any other config option) of this command is + // the one defined in php.ini instead of the new values (optionally) + // passed via the '-d' command option + $childProcess = new Process(['bin/console', 'cache:pool:prune']); + + // the memory_limit (and any other config option) of this command takes + // into account the values (optionally) passed via the '-d' command option + $childProcess = new PhpSubprocess(['bin/console', 'cache:pool:prune']); + } + } + Process Timeout --------------- @@ -453,10 +479,6 @@ check regularly:: You can get the process start time using the ``getStartTime()`` method. - .. versionadded:: 5.1 - - The ``getStartTime()`` method was introduced in Symfony 5.1. - .. _reference-process-signal: Process Idle Timeout @@ -489,6 +511,20 @@ When running a program asynchronously, you can send it POSIX signals with the // will send a SIGKILL to the process $process->signal(SIGKILL); +You can make the process ignore signals by using the +:method:`Symfony\\Component\\Process\\Process::setIgnoredSignals` +method. The given signals won't be propagated to the child process:: + + use Symfony\Component\Process\Process; + + $process = new Process(['find', '/', '-name', 'rabbit']); + $process->setIgnoredSignals([SIGKILL, SIGUSR1]); + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\Process\\Process::setIgnoredSignals` + method was introduced in Symfony 7.1. + Process Pid ----------- diff --git a/components/property_access.rst b/components/property_access.rst index 78b125cd391..600481dce1a 100644 --- a/components/property_access.rst +++ b/components/property_access.rst @@ -1,7 +1,7 @@ The PropertyAccess Component ============================ - The PropertyAccess component provides function to read and write from/to an + The PropertyAccess component provides functions to read and write from/to an object or array using a simple string notation. Installation @@ -59,6 +59,9 @@ method:: // Symfony\Component\PropertyAccess\Exception\NoSuchIndexException $value = $propertyAccessor->getValue($person, '[age]'); + // You can avoid the exception by adding the nullsafe operator + $value = $propertyAccessor->getValue($person, '[age?]'); + You can also use multi dimensional arrays:: // ... @@ -74,6 +77,18 @@ You can also use multi dimensional arrays:: var_dump($propertyAccessor->getValue($persons, '[0][first_name]')); // 'Wouter' var_dump($propertyAccessor->getValue($persons, '[1][first_name]')); // 'Ryan' +.. tip:: + + If the key of the array contains a dot ``.`` or a left square bracket ``[``, + you must escape those characters with a backslash. In the above example, + if the array key was ``first.name`` instead of ``first_name``, you should + access its value as follows:: + + var_dump($propertyAccessor->getValue($persons, '[0][first\.name]')); // 'Wouter' + var_dump($propertyAccessor->getValue($persons, '[1][first\.name]')); // 'Ryan' + + Right square brackets ``]`` don't need to be escaped in array keys. + Reading from Objects -------------------- @@ -115,9 +130,9 @@ it with ``get``. So the actual method becomes ``getFirstName()``:: // ... class Person { - private $firstName = 'Wouter'; + private string $firstName = 'Wouter'; - public function getFirstName() + public function getFirstName(): string { return $this->firstName; } @@ -137,15 +152,15 @@ getters, this means that you can do something like this:: // ... class Person { - private $author = true; - private $children = []; + private bool $author = true; + private array $children = []; - public function isAuthor() + public function isAuthor(): bool { return $this->author; } - public function hasChildren() + public function hasChildren(): bool { return 0 !== count($this->children); } @@ -174,7 +189,7 @@ method:: // ... class Person { - public $name; + public string $name; } $person = new Person(); @@ -186,6 +201,36 @@ method:: // instead of throwing an exception the following code returns null $value = $propertyAccessor->getValue($person, 'birthday'); +Accessing Nullable Property Paths +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Consider the following PHP code:: + + class Person + { + } + + class Comment + { + public ?Person $person = null; + public string $message; + } + + $comment = new Comment(); + $comment->message = 'test'; + +Given that ``$person`` is nullable, an object graph like ``comment.person.profile`` +will trigger an exception when the ``$person`` property is ``null``. The solution +is to mark all nullable properties with the nullsafe operator (``?``):: + + // This code throws an exception of type + // Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException + var_dump($propertyAccessor->getValue($comment, 'person.firstname')); + + // If a property marked with the nullsafe operator is null, the expression is + // no longer evaluated and null is returned immediately without throwing an exception + var_dump($propertyAccessor->getValue($comment, 'person?.firstname')); // null + .. _components-property-access-magic-get: Magic ``__get()`` Method @@ -196,11 +241,11 @@ The ``getValue()`` method can also use the magic ``__get()`` method:: // ... class Person { - private $children = [ + private array $children = [ 'Wouter' => [...], ]; - public function __get($id) + public function __get($id): mixed { return $this->children[$id]; } @@ -220,11 +265,6 @@ The ``getValue()`` method can also use the magic ``__get()`` method:: When implementing the magic ``__get()`` method, you also need to implement ``__isset()``. -.. versionadded:: 5.2 - - The magic ``__get()`` method can be disabled since in Symfony 5.2. - see `Enable other Features`_. - .. _components-property-access-magic-call: Magic ``__call()`` Method @@ -236,11 +276,11 @@ enable this feature by using :class:`Symfony\\Component\\PropertyAccess\\Propert // ... class Person { - private $children = [ + private array $children = [ 'wouter' => [...], ]; - public function __call($name, $args) + public function __call($name, $args): mixed { $property = lcfirst(substr($name, 3)); if ('get' === substr($name, 0, 3)) { @@ -294,26 +334,26 @@ can use setters, the magic ``__set()`` method or properties to set values:: // ... class Person { - public $firstName; - private $lastName; - private $children = []; + public string $firstName; + private string $lastName; + private array $children = []; - public function setLastName($name) + public function setLastName($name): void { $this->lastName = $name; } - public function getLastName() + public function getLastName(): string { return $this->lastName; } - public function getChildren() + public function getChildren(): array { return $this->children; } - public function __set($property, $value) + public function __set($property, $value): void { $this->$property = $value; } @@ -335,9 +375,9 @@ see `Enable other Features`_:: // ... class Person { - private $children = []; + private array $children = []; - public function __call($name, $args) + public function __call($name, $args): mixed { $property = lcfirst(substr($name, 3)); if ('get' === substr($name, 0, 3)) { @@ -361,10 +401,10 @@ see `Enable other Features`_:: var_dump($person->getWouter()); // [...] -.. versionadded:: 5.2 +.. note:: - The magic ``__set()`` method can be disabled since in Symfony 5.2. - see `Enable other Features`_. + The ``__set()`` method support is enabled by default. + See `Enable other Features`_ if you want to disable it. Writing to Array Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -378,7 +418,7 @@ properties through *adder* and *remover* methods:: /** * @var string[] */ - private $children = []; + private array $children = []; public function getChildren(): array { @@ -405,7 +445,7 @@ The PropertyAccess component checks for methods called ``add()``. Both methods must be defined. For instance, in the previous example, the component looks for the ``addChild()`` and ``removeChild()`` methods to access the ``children`` property. -`The Inflector component`_ is used to find the singular of a property name. +`The String component`_ inflector is used to find the singular of a property name. If available, *adder* and *remover* methods have priority over a *setter* method. @@ -481,15 +521,15 @@ You can also mix objects and arrays:: // ... class Person { - public $firstName; - private $children = []; + public string $firstName; + private array $children = []; - public function setChildren($children) + public function setChildren($children): void { $this->children = $children; } - public function getChildren() + public function getChildren(): array { return $this->children; } @@ -544,4 +584,4 @@ Or you can pass parameters directly to the constructor (not the recommended way) // enable handling of magic __call, __set but not __get: $propertyAccessor = new PropertyAccessor(PropertyAccessor::MAGIC_CALL | PropertyAccessor::MAGIC_SET); -.. _The Inflector component: https://github.com/symfony/inflector +.. _`The String component`: https://github.com/symfony/string diff --git a/components/property_info.rst b/components/property_info.rst index 45e20c29449..892cd5345a3 100644 --- a/components/property_info.rst +++ b/components/property_info.rst @@ -118,7 +118,7 @@ class exposes public methods to extract several types of information: * :ref:`List of properties `: :method:`Symfony\\Component\\PropertyInfo\\PropertyListExtractorInterface::getProperties` * :ref:`Property type `: :method:`Symfony\\Component\\PropertyInfo\\PropertyTypeExtractorInterface::getTypes` - (including typed properties since PHP 7.4) + (including typed properties) * :ref:`Property description `: :method:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface::getShortDescription` and :method:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface::getLongDescription` * :ref:`Property access details `: :method:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface::isReadable` and :method:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface::isWritable` * :ref:`Property initializable through the constructor `: :method:`Symfony\\Component\\PropertyInfo\\PropertyInitializableExtractorInterface::isInitializable` @@ -183,6 +183,26 @@ for a property:: See :ref:`components-property-info-type` for info about the ``Type`` class. +Documentation Block +~~~~~~~~~~~~~~~~~~~ + +Extractors that implement :class:`Symfony\\Component\\PropertyInfo\\PropertyDocBlockExtractorInterface` +can provide the full documentation block for a property as a string:: + + $docBlock = $propertyInfo->getDocBlock($class, $property); + /* + Example Result + -------------- + string(79): + This is the subsequent paragraph in the DocComment. + It can span multiple lines. + */ + +.. versionadded:: 7.1 + + The :class:`Symfony\\Component\\PropertyInfo\\PropertyDocBlockExtractorInterface` + interface was introduced in Symfony 7.1. + .. _property-info-description: Description Information @@ -225,7 +245,9 @@ provide whether properties are readable or writable as booleans:: The :class:`Symfony\\Component\\PropertyInfo\\Extractor\\ReflectionExtractor` looks for getter/isser/setter/hasser method in addition to whether or not a property is public to determine if it's accessible. This based on how the :doc:`PropertyAccess ` -works. +works. It assumes camel case style method names following `PSR-1`_. For example, +both ``myProperty`` and ``my_property`` properties are readable if there's a +``getMyProperty()`` method and writable if there's a ``setMyProperty()`` method. .. _property-info-initializable: @@ -333,10 +355,6 @@ methods. The ``list`` pseudo type is returned by the PropertyInfo component as an array with integer as the key type. -.. versionadded:: 5.4 - - The support for the ``list`` pseudo type was introduced in Symfony 5.4. - .. _`components-property-info-extractors`: Extractors @@ -362,7 +380,7 @@ Using PHP reflection, the :class:`Symfony\\Component\\PropertyInfo\\Extractor\\R provides list, type and access information from setter and accessor methods. It can also give the type of a property (even extracting it from the constructor arguments), and if it is initializable through the constructor. It supports -return and scalar types for PHP 7:: +return and scalar types:: use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; @@ -415,6 +433,43 @@ library is present:: // Description information. $phpDocExtractor->getShortDescription($class, $property); $phpDocExtractor->getLongDescription($class, $property); + $phpDocExtractor->getDocBlock($class, $property); + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\PropertyInfo\\Extractor\\PhpDocExtractor::getDocBlock` + method was introduced in Symfony 7.1. + +PhpStanExtractor +~~~~~~~~~~~~~~~~ + +.. note:: + + This extractor depends on the `phpstan/phpdoc-parser`_ and + `phpdocumentor/reflection-docblock`_ libraries. + +This extractor fetches information thanks to the PHPStan parser. It gathers +information from annotations of properties and methods, such as ``@var``, +``@param`` or ``@return``:: + + // src/Domain/Foo.php + class Foo + { + /** + * @param string $bar + */ + public function __construct( + private string $bar, + ) { + } + } + + // Extraction.php + use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; + use App\Domain\Foo; + + $phpStanExtractor = new PhpStanExtractor(); + $phpStanExtractor->getTypesFromConstructor(Foo::class, 'bar'); SerializerExtractor ~~~~~~~~~~~~~~~~~~~ @@ -423,20 +478,17 @@ SerializerExtractor This extractor depends on the `symfony/serializer`_ library. -Using :ref:`groups metadata ` +Using :ref:`groups metadata ` from the :doc:`Serializer component `, the :class:`Symfony\\Component\\PropertyInfo\\Extractor\\SerializerExtractor` provides list information. This extractor is *not* registered automatically with the ``property_info`` service in the Symfony Framework:: - use Doctrine\Common\Annotations\AnnotationReader; use Symfony\Component\PropertyInfo\Extractor\SerializerExtractor; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; - use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; + use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; - $serializerClassMetadataFactory = new ClassMetadataFactory( - new AnnotationLoader(new AnnotationReader) - ); + $serializerClassMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $serializerExtractor = new SerializerExtractor($serializerClassMetadataFactory); // the `serializer_groups` option must be configured (may be set to null) @@ -444,11 +496,7 @@ with the ``property_info`` service in the Symfony Framework:: If ``serializer_groups`` is set to ``null``, serializer groups metadata won't be checked but you will get only the properties considered by the Serializer -Component (notably the ``@Ignore`` annotation is taken into account). - -.. versionadded:: 5.2 - - Support for the ``null`` value in ``serializer_groups`` was introduced in Symfony 5.2. +Component (notably the ``#[Ignore]`` attribute is taken into account). DoctrineExtractor ~~~~~~~~~~~~~~~~~ @@ -491,26 +539,19 @@ on the constructor arguments:: // src/Domain/Foo.php class Foo { - private $bar; - - public function __construct(string $bar) - { - $this->bar = $bar; + public function __construct( + private string $bar, + ) { } } // Extraction.php - use Symfony\Component\PropertyInfo\Extractor\ConstructorExtractor; use App\Domain\Foo; + use Symfony\Component\PropertyInfo\Extractor\ConstructorExtractor; $constructorExtractor = new ConstructorExtractor([new ReflectionExtractor()]); $constructorExtractor->getTypes(Foo::class, 'bar')[0]->getBuiltinType(); // returns 'string' -.. versionadded:: 5.2 - - The :class:`Symfony\\Component\\PropertyInfo\\Extractor\\ConstructorExtractor` - was introduced in Symfony 5.2. - .. _`components-property-information-extractors-creation`: Creating Your Own Extractors @@ -536,8 +577,10 @@ service by defining it as a service with one or more of the following * ``property_info.initializable_extractor`` if it provides initializable information (it checks if a property can be initialized through the constructor). +.. _`PSR-1`: https://www.php-fig.org/psr/psr-1/ .. _`phpDocumentor Reflection`: https://github.com/phpDocumentor/ReflectionDocBlock .. _`phpdocumentor/reflection-docblock`: https://packagist.org/packages/phpdocumentor/reflection-docblock +.. _`phpstan/phpdoc-parser`: https://packagist.org/packages/phpstan/phpdoc-parser .. _`Doctrine ORM`: https://www.doctrine-project.org/projects/orm.html .. _`symfony/serializer`: https://packagist.org/packages/symfony/serializer .. _`symfony/doctrine-bridge`: https://packagist.org/packages/symfony/doctrine-bridge diff --git a/components/runtime.rst b/components/runtime.rst index eba9e39661d..7d17e7e7456 100644 --- a/components/runtime.rst +++ b/components/runtime.rst @@ -5,10 +5,6 @@ The Runtime Component to make sure the application can run with runtimes like `PHP-PM`_, `ReactPHP`_, `Swoole`_, etc. without any changes. -.. versionadded:: 5.3 - - The Runtime component was introduced in Symfony 5.3. - Installation ------------ @@ -31,7 +27,7 @@ to look like this:: require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; - return function (array $context) { + return function (array $context): Kernel { return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); }; @@ -65,7 +61,7 @@ To make a console application, the bootstrap code would look like:: require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; - return function (array $context) { + return function (array $context): Application { $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); // returning an "Application" makes the Runtime run a Console @@ -133,12 +129,13 @@ Resolvable Arguments The closure returned from the front-controller may have zero or more arguments:: // public/index.php + use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; - return function (InputInterface $input, OutputInterface $output) { + return function (InputInterface $input, OutputInterface $output): Application { // ... }; @@ -183,7 +180,7 @@ a number of different applications are supported:: require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; - return function () { + return static function (): Kernel { return new Kernel('prod', false); }; @@ -202,7 +199,7 @@ The ``SymfonyRuntime`` can handle these applications: require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; - return function () { + return static function (): Response { return new Response('Hello world'); }; @@ -216,8 +213,8 @@ The ``SymfonyRuntime`` can handle these applications: require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; - return function (Command $command) { - $command->setCode(function (InputInterface $input, OutputInterface $output) { + return static function (Command $command): Command { + $command->setCode(static function (InputInterface $input, OutputInterface $output): void { $output->write('Hello World'); }); @@ -235,9 +232,9 @@ The ``SymfonyRuntime`` can handle these applications: require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; - return function (array $context) { + return static function (array $context): Application { $command = new Command('hello'); - $command->setCode(function (InputInterface $input, OutputInterface $output) { + $command->setCode(static function (InputInterface $input, OutputInterface $output): void { $output->write('Hello World'); }); @@ -260,7 +257,7 @@ applications: require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; - return function () { + return static function (): RunnerInterface { return new class implements RunnerInterface { public function run(): int { @@ -278,8 +275,8 @@ applications: // public/index.php require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; - return function () { - $app = function() { + return static function (): callable { + $app = static function(): int { echo 'Hello World'; return 0; @@ -294,7 +291,7 @@ applications: require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; - return function () { + return function (): void { echo 'Hello world'; }; @@ -367,10 +364,6 @@ these options: Defines the name of the env var that stores the value of the :ref:`debug mode ` flag to use when running the application. -.. versionadded:: 5.4 - - The ``env_var_name`` and ``debug_var_name`` options were introduced in Symfony 5.4. - Create Your Own Runtime ----------------------- @@ -381,12 +374,6 @@ logic could be versioned as a part of a normal package. If the application author decides to use this component, the package maintainer of the Runtime class will have more control and can fix bugs and add features. -.. note:: - - Before Symfony 5.3, the Symfony bootstrap logic was part of a Flex recipe. - Since recipes are rarely updated by users, bug patches would rarely be - installed. - The Runtime component is designed to be totally generic and able to run any application outside of the global state in 6 steps: @@ -423,6 +410,7 @@ the `PSR-15`_ interfaces for HTTP request handling. However, a ReactPHP application will need some special logic to *run*. That logic is added in a new class implementing :class:`Symfony\\Component\\Runtime\\RunnerInterface`:: + use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use React\EventLoop\Factory as ReactFactory; @@ -432,13 +420,10 @@ is added in a new class implementing :class:`Symfony\\Component\\Runtime\\Runner class ReactPHPRunner implements RunnerInterface { - private $application; - private $port; - - public function __construct(RequestHandlerInterface $application, int $port) - { - $this->application = $application; - $this->port = $port; + public function __construct( + private RequestHandlerInterface $application, + private int $port, + ) { } public function run(): int @@ -449,7 +434,7 @@ is added in a new class implementing :class:`Symfony\\Component\\Runtime\\Runner // configure ReactPHP to correctly handle the PSR-15 application $server = new ReactHttpServer( $loop, - function (ServerRequestInterface $request) use ($application) { + function (ServerRequestInterface $request) use ($application): ResponseInterface { return $application->handle($request); } ); @@ -472,7 +457,7 @@ always using this ``ReactPHPRunner``:: class ReactPHPRuntime extends GenericRuntime { - private $port; + private int $port; public function __construct(array $options) { @@ -496,7 +481,7 @@ The end user will now be able to create front controller like:: require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; - return function (array $context) { + return function (array $context): SomeCustomPsr15Application { return new SomeCustomPsr15Application(); }; diff --git a/components/semaphore.rst b/components/semaphore.rst index 84e272451c4..5715b426053 100644 --- a/components/semaphore.rst +++ b/components/semaphore.rst @@ -4,10 +4,6 @@ The Semaphore Component The Semaphore Component manages `semaphores`_, a mechanism to provide exclusive access to a shared resource. -.. versionadded:: 5.2 - - The Semaphore Component was introduced in Symfony 5.2. - Installation ------------ diff --git a/components/serializer.rst b/components/serializer.rst index 0da80f10e0e..64d7b38a025 100644 --- a/components/serializer.rst +++ b/components/serializer.rst @@ -78,7 +78,7 @@ exists in your project:: private int $age; private string $name; private bool $sportsperson; - private ?\DateTime $createdAt; + private ?\DateTimeInterface $createdAt; // Getters public function getAge(): int @@ -91,7 +91,7 @@ exists in your project:: return $this->name; } - public function getCreatedAt() + public function getCreatedAt(): ?\DateTimeInterface { return $this->createdAt; } @@ -118,7 +118,7 @@ exists in your project:: $this->sportsperson = $sportsperson; } - public function setCreatedAt(?\DateTime $createdAt = null): void + public function setCreatedAt(?\DateTimeInterface $createdAt = null): void { $this->createdAt = $createdAt; } @@ -249,35 +249,34 @@ Assume you have the following plain-old-PHP object:: class MyObj { - public $foo; + public string $foo; - private $bar; + private string $bar; - public function getBar() + public function getBar(): string { return $this->bar; } - public function setBar($bar) + public function setBar($bar): string { return $this->bar = $bar; } } -The definition of serialization can be specified using annotations, XML -or YAML. The :class:`Symfony\\Component\\Serializer\\Mapping\\Factory\\ClassMetadataFactory` +The definition of serialization can be specified using attributes, XML or YAML. +The :class:`Symfony\\Component\\Serializer\\Mapping\\Factory\\ClassMetadataFactory` that will be used by the normalizer must be aware of the format to use. The following code shows how to initialize the :class:`Symfony\\Component\\Serializer\\Mapping\\Factory\\ClassMetadataFactory` for each format: -* Annotations in PHP files:: +* Attributes in PHP files:: - use Doctrine\Common\Annotations\AnnotationReader; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; - use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; + use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); * YAML files:: @@ -293,41 +292,12 @@ for each format: $classMetadataFactory = new ClassMetadataFactory(new XmlFileLoader('/path/to/your/definition.xml')); -.. _component-serializer-attributes-groups-annotations: +.. _component-serializer-attributes-groups-attributes: Then, create your groups definition: .. configuration-block:: - .. code-block:: php-annotations - - namespace Acme; - - use Symfony\Component\Serializer\Annotation\Groups; - - class MyObj - { - /** - * @Groups({"group1", "group2"}) - */ - public $foo; - - /** - * @Groups({"group4"}) - */ - public $anotherProperty; - - /** - * @Groups("group3") - */ - public function getBar() // is* methods are also supported - { - return $this->bar; - } - - // ... - } - .. code-block:: php-attributes namespace Acme; @@ -337,10 +307,10 @@ Then, create your groups definition: class MyObj { #[Groups(['group1', 'group2'])] - public $foo; + public string $foo; #[Groups(['group4'])] - public $anotherProperty; + public string $anotherProperty; #[Groups(['group3'])] public function getBar() // is* methods are also supported @@ -420,10 +390,6 @@ You are now able to serialize only attributes in the groups you want:: ); // $obj2 = MyObj(foo: 'foo', anotherProperty: 'anotherProperty', bar: 'bar') -.. versionadded:: 5.2 - - The ``*`` special value for ``groups`` was introduced in Symfony 5.2. - .. _ignoring-attributes-when-serializing: Selecting Specific Attributes @@ -437,15 +403,15 @@ It is also possible to serialize only a set of specific attributes:: class User { - public $familyName; - public $givenName; - public $company; + public string $familyName; + public string $givenName; + public Company $company; } class Company { - public $name; - public $address; + public string $name; + public string $address; } $company = new Company(); @@ -475,27 +441,11 @@ Ignoring Attributes All accessible attributes are included by default when serializing objects. There are two options to ignore some of those attributes. -Option 1: Using ``@Ignore`` Annotation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Option 1: Using ``#[Ignore]`` Attribute +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. configuration-block:: - .. code-block:: php-annotations - - namespace App\Model; - - use Symfony\Component\Serializer\Annotation\Ignore; - - class MyClass - { - public $foo; - - /** - * @Ignore() - */ - public $bar; - } - .. code-block:: php-attributes namespace App\Model; @@ -504,10 +454,10 @@ Option 1: Using ``@Ignore`` Annotation class MyClass { - public $foo; + public string $foo; #[Ignore] - public $bar; + public string $bar; } .. code-block:: yaml @@ -584,8 +534,8 @@ Given you have the following object:: class Company { - public $name; - public $address; + public string $name; + public string $address; } And in the serialized form, all attributes must be prefixed by ``org_`` like @@ -599,12 +549,12 @@ A custom name converter can handle such cases:: class OrgPrefixNameConverter implements NameConverterInterface { - public function normalize(string $propertyName) + public function normalize(string $propertyName): string { return 'org_'.$propertyName; } - public function denormalize(string $propertyName) + public function denormalize(string $propertyName): string { // removes 'org_' prefix return str_starts_with($propertyName, 'org_') ? substr($propertyName, 4) : $propertyName; @@ -661,14 +611,12 @@ processes:: class Person { - private $firstName; - - public function __construct($firstName) - { - $this->firstName = $firstName; + public function __construct( + private string $firstName, + ) { } - public function getFirstName() + public function getFirstName(): string { return $this->firstName; } @@ -696,7 +644,7 @@ this is already set up and you only need to provide the configuration. Otherwise use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory); @@ -710,27 +658,6 @@ defines a ``Person`` entity with a ``firstName`` property: .. configuration-block:: - .. code-block:: php-annotations - - namespace App\Entity; - - use Symfony\Component\Serializer\Annotation\SerializedName; - - class Person - { - /** - * @SerializedName("customer_name") - */ - private $firstName; - - public function __construct($firstName) - { - $this->firstName = $firstName; - } - - // ... - } - .. code-block:: php-attributes namespace App\Entity; @@ -739,12 +666,10 @@ defines a ``Person`` entity with a ``firstName`` property: class Person { - #[SerializedName('customer_name')] - private $firstName; - - public function __construct($firstName) - { - $this->firstName = $firstName; + public function __construct( + #[SerializedName('customer_name')] + private string $firstName, + ) { } // ... @@ -776,15 +701,48 @@ deserializing objects:: $serialized = $serializer->serialize(new Person('Kévin'), 'json'); // {"customer_name": "Kévin"} -Serializing Boolean Attributes ------------------------------- +.. _serializing-boolean-attributes: + +Handling Boolean Attributes And Values +-------------------------------------- + +During Serialization +~~~~~~~~~~~~~~~~~~~~ If you are using isser methods (methods prefixed by ``is``, like ``App\Model\Person::isSportsperson()``), the Serializer component will automatically detect and use it to serialize related attributes. -The ``ObjectNormalizer`` also takes care of methods starting with ``has`` and -``get``. +The ``ObjectNormalizer`` also takes care of methods starting with ``has``, ``get``, +and ``can``. + +During Deserialization +~~~~~~~~~~~~~~~~~~~~~~ + +PHP considers many different values as true or false. For example, the +strings ``true``, ``1``, and ``yes`` are considered true, while +``false``, ``0``, and ``no`` are considered false. + +When deserializing, the Serializer component can take care of this +automatically. This can be done by using the ``AbstractNormalizer::FILTER_BOOL`` +context option:: + + use Acme\Person; + use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; + use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + use Symfony\Component\Serializer\Serializer; + + $normalizer = new ObjectNormalizer(); + $serializer = new Serializer([$normalizer]); + + $data = $serializer->denormalize(['sportsperson' => 'yes'], Person::class, context: [AbstractNormalizer::FILTER_BOOL => true]); + +This context makes the deserialization process behave like the +:phpfunction:`filter_var` function with the ``FILTER_VALIDATE_BOOL`` flag. + +.. versionadded:: 7.1 + + The ``AbstractNormalizer::FILTER_BOOL`` context option was introduced in Symfony 7.1. Using Callbacks to Serialize Properties with Object Instances ------------------------------------------------------------- @@ -799,7 +757,7 @@ When serializing, you can set a callback to format a specific object property:: $encoder = new JsonEncoder(); // all callback parameters are optional (you can omit the ones you don't use) - $dateCallback = function ($attributeValue, $object, string $attributeName, ?string $format = null, array $context = []) { + $dateCallback = function (object $attributeValue, object $object, string $attributeName, ?string $format = null, array $context = []): string { return $attributeValue instanceof \DateTime ? $attributeValue->format(\DateTime::ATOM) : ''; }; @@ -848,12 +806,12 @@ The Serializer component provides several built-in normalizers: :class:`Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer` This normalizer leverages the :doc:`PropertyAccess Component ` to read and write in the object. It means that it can access to properties - directly and through getters, setters, hassers, issers, adders and removers. It supports - calling the constructor during the denormalization process. + directly and through getters, setters, hassers, issers, canners, adders and removers. + It supports calling the constructor during the denormalization process. Objects are normalized to a map of property names and values (names are - generated by removing the ``get``, ``set``, ``has``, ``is``, ``add`` or ``remove`` prefix from - the method name and transforming the first letter to lowercase; e.g. + generated by removing the ``get``, ``set``, ``has``, ``is``, ``can``, ``add`` or ``remove`` + prefix from the method name and transforming the first letter to lowercase; e.g. ``getFirstName()`` -> ``firstName``). The ``ObjectNormalizer`` is the most powerful normalizer. It is configured by @@ -876,6 +834,11 @@ The Serializer component provides several built-in normalizers: Objects are normalized to a map of property names to property values. + If you prefer to only normalize certain properties (e.g. only public properties) + set the ``PropertyNormalizer::NORMALIZE_VISIBILITY`` context option and + combine the following values: ``PropertyNormalizer::NORMALIZE_PUBLIC``, + ``PropertyNormalizer::NORMALIZE_PROTECTED`` or ``PropertyNormalizer::NORMALIZE_PRIVATE``. + :class:`Symfony\\Component\\Serializer\\Normalizer\\JsonSerializableNormalizer` This normalizer works with classes that implement :phpclass:`JsonSerializable`. @@ -891,8 +854,14 @@ The Serializer component provides several built-in normalizers: :class:`Symfony\\Component\\Serializer\\Normalizer\\DateTimeNormalizer` This normalizer converts :phpclass:`DateTimeInterface` objects (e.g. - :phpclass:`DateTime` and :phpclass:`DateTimeImmutable`) into strings. - By default, it uses the `RFC3339`_ format. + :phpclass:`DateTime` and :phpclass:`DateTimeImmutable`) into strings, + integers or floats. By default, it converts them to strings using the `RFC3339`_ format. + To convert the objects to integers or floats, set the serializer context option + ``DateTimeNormalizer::CAST_KEY`` to ``int`` or ``float``. + + .. versionadded:: 7.1 + + The ``DateTimeNormalizer::CAST_KEY`` context option was introduced in Symfony 7.1. :class:`Symfony\\Component\\Serializer\\Normalizer\\DateTimeZoneNormalizer` This normalizer converts :phpclass:`DateTimeZone` objects into strings that @@ -909,10 +878,8 @@ The Serializer component provides several built-in normalizers: :class:`Symfony\\Component\\Serializer\\Normalizer\\BackedEnumNormalizer` This normalizer converts a \BackedEnum objects into strings or integers. - .. versionadded:: 5.4 - - The ``BackedEnumNormalizer`` was introduced in Symfony 5.4. - PHP BackedEnum requires at least PHP 8.1. + By default, an exception is thrown when data is not a valid backed enumeration. If you + want ``null`` instead, you can set the ``BackedEnumNormalizer::ALLOW_INVALID_VALUES`` option. :class:`Symfony\\Component\\Serializer\\Normalizer\\FormErrorNormalizer` This normalizer works with classes that implement @@ -932,7 +899,7 @@ The Serializer component provides several built-in normalizers: Normalizes a PHP object using an object that implements :class:`Symfony\\Component\\Serializer\\Normalizer\\NormalizableInterface`. :class:`Symfony\\Component\\Serializer\\Normalizer\\UidNormalizer` - This normalizer converts objects that implement + This normalizer converts objects that extend :class:`Symfony\\Component\\Uid\\AbstractUid` into strings. The default normalization format for objects that implement :class:`Symfony\\Component\\Uid\\Uuid` is the `RFC 4122`_ format (example: ``d9e7a184-5d5b-11ea-a62a-3499710062d0``). @@ -945,13 +912,14 @@ The Serializer component provides several built-in normalizers: Also it can denormalize ``uuid`` or ``ulid`` strings to :class:`Symfony\\Component\\Uid\\Uuid` or :class:`Symfony\\Component\\Uid\\Ulid`. The format does not matter. -.. versionadded:: 5.2 - - The ``UidNormalizer`` was introduced in Symfony 5.2. - -.. versionadded:: 5.3 - - The ``UidNormalizer`` normalization formats were introduced in Symfony 5.3. +:class:`Symfony\\Component\\Serializer\\Normalizer\\TranslatableNormalizer` + This normalizer converts objects that implement + :class:`Symfony\\Contracts\\Translation\\TranslatableInterface` into + translated strings, using the + :method:`Symfony\\Contracts\\Translation\\TranslatableInterface::trans` + method. You can define the locale to use to translate the object by + setting the ``TranslatableNormalizer::NORMALIZATION_LOCALE_KEY`` serializer + context option. .. note:: @@ -1004,7 +972,7 @@ faster alternative to the use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->services() // ... ->set('get_set_method_normalizer', GetSetMethodNormalizer::class) @@ -1070,6 +1038,18 @@ context to pass in these options using the key ``json_encode_options`` or $this->serializer->serialize($data, 'json', ['json_encode_options' => \JSON_PRESERVE_ZERO_FRACTION]); +These are the options available: + +=============================== =========================================================================================================== ================================ +Option Description Default +=============================== ========================================================================================================== ================================ +``json_decode_associative`` If set to true returns the result as an array, returns a nested ``stdClass`` hierarchy otherwise. ``false`` +``json_decode_detailed_errors`` If set to true, exceptions thrown on parsing of JSON are more specific. Requires `seld/jsonlint`_ package. ``false`` +``json_decode_options`` `$flags`_ passed to :phpfunction:`json_decode` function. ``0`` +``json_encode_options`` `$flags`_ passed to :phpfunction:`json_encode` function. ``\JSON_PRESERVE_ZERO_FRACTION`` +``json_decode_recursion_depth`` Sets maximum recursion depth. ``512`` +=============================== ========================================================================================================== ================================ + The ``CsvEncoder`` ~~~~~~~~~~~~~~~~~~ @@ -1110,10 +1090,6 @@ Option Description D ``output_utf8_bom`` Outputs special `UTF-8 BOM`_ along with encoded data ``false`` ======================= ===================================================== ========================== -.. versionadded:: 5.3 - - The ``csv_end_of_line`` option was introduced in Symfony 5.3. - The ``XmlEncoder`` ~~~~~~~~~~~~~~~~~~ @@ -1187,8 +1163,11 @@ always as a collection. behavior can be changed with the optional context key ``XmlEncoder::DECODER_IGNORED_NODE_TYPES``. Data with ``#comment`` keys are encoded to XML comments by default. This can be - changed with the optional ``$encoderIgnoredNodeTypes`` argument of the - ``XmlEncoder`` class constructor. + changed by adding the ``\XML_COMMENT_NODE`` option to the ``XmlEncoder::ENCODER_IGNORED_NODE_TYPES`` + key of the ``$defaultContext`` of the ``XmlEncoder`` constructor or + directly to the ``$context`` argument of the ``encode()`` method:: + + $xmlEncoder->encode($array, 'xml', [XmlEncoder::ENCODER_IGNORED_NODE_TYPES => [\XML_COMMENT_NODE]]); The ``XmlEncoder`` Context Options .................................. @@ -1218,10 +1197,21 @@ Option Description ``encoder_ignored_node_types`` Array of node types (`DOM XML_* constants`_) ``[]`` to be ignored while encoding ``load_options`` XML loading `options with libxml`_ ``\LIBXML_NONET | \LIBXML_NOBLANKS`` +``save_options`` XML saving `options with libxml`_ ``0`` ``remove_empty_tags`` If set to true, removes all empty tags in the ``false`` generated XML +``cdata_wrapping`` If set to false, will not wrap any value ``true`` + matching the ``cdata_wrapping_pattern`` regex in + `a CDATA section`_ like following: + ```` +``cdata_wrapping_pattern`` A regular expression pattern to determine if a ``/[<>&]/`` + value should be wrapped in a CDATA section ============================== ================================================= ========================== +.. versionadded:: 7.1 + + The ``cdata_wrapping_pattern`` option was introduced in Symfony 7.1. + Example with custom ``context``:: use Symfony\Component\Serializer\Encoder\XmlEncoder; @@ -1281,6 +1271,40 @@ Option Description Defaul to customize the encoding / decoding YAML string =============== ======================================================== ========================== +.. _component-serializer-context-builders: + +Context Builders +---------------- + +Instead of passing plain PHP arrays to the :ref:`serialization context `, +you can use "context builders" to define the context using a fluent interface:: + + use Symfony\Component\Serializer\Context\Encoder\CsvEncoderContextBuilder; + use Symfony\Component\Serializer\Context\Normalizer\ObjectNormalizerContextBuilder; + + $initialContext = [ + 'custom_key' => 'custom_value', + ]; + + $contextBuilder = (new ObjectNormalizerContextBuilder()) + ->withContext($initialContext) + ->withGroups(['group1', 'group2']); + + $contextBuilder = (new CsvEncoderContextBuilder()) + ->withContext($contextBuilder) + ->withDelimiter(';'); + + $serializer->serialize($something, 'csv', $contextBuilder->toArray()); + +.. note:: + + The Serializer component provides a context builder + for each :ref:`normalizer ` + and :ref:`encoder `. + + You can also :doc:`create custom context builders ` + to deal with your context values. + Skipping ``null`` Values ------------------------ @@ -1289,14 +1313,36 @@ You can change this behavior by setting the ``AbstractObjectNormalizer::SKIP_NUL to ``true``:: $dummy = new class { - public $foo; - public $bar = 'notNull'; + public ?string $foo = null; + public string $bar = 'notNull'; }; $normalizer = new ObjectNormalizer(); $result = $normalizer->normalize($dummy, 'json', [AbstractObjectNormalizer::SKIP_NULL_VALUES => true]); // ['bar' => 'notNull'] +Require all Properties +---------------------- + +By default, the Serializer will add ``null`` to nullable properties when the parameters for those are not provided. +You can change this behavior by setting the ``AbstractNormalizer::REQUIRE_ALL_PROPERTIES`` context option +to ``true``:: + + class Dummy + { + public function __construct( + public string $foo, + public ?string $bar, + ) { + } + } + + $data = ['foo' => 'notNull']; + + $normalizer = new ObjectNormalizer(); + $result = $normalizer->denormalize($data, Dummy::class, 'json', [AbstractNormalizer::REQUIRE_ALL_PROPERTIES => true]); + // throws Symfony\Component\Serializer\Exception\MissingConstructorArgumentException + Skipping Uninitialized Properties --------------------------------- @@ -1327,11 +1373,6 @@ context option to ``false``:: to ``false`` will throw an ``\Error`` instance if the given object has uninitialized properties as the normalizer cannot read them (directly or via getter/isser methods). -.. versionadded:: 5.4 - - The ``AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES`` constant was - introduced in Symfony 5.4. - .. _component-serializer-handling-circular-references: Collecting Type Errors While Denormalizing @@ -1363,10 +1404,6 @@ collect all exceptions at once, and to get the object partially denormalized:: return $this->json($violations, 400); } -.. versionadded:: 5.4 - - The ``COLLECT_DENORMALIZATION_ERRORS`` option was introduced in Symfony 5.4. - Handling Circular References ---------------------------- @@ -1374,25 +1411,25 @@ Circular references are common when dealing with entity relations:: class Organization { - private $name; - private $members; + private string $name; + private array $members; - public function setName($name) + public function setName($name): void { $this->name = $name; } - public function getName() + public function getName(): string { return $this->name; } - public function setMembers(array $members) + public function setMembers(array $members): void { $this->members = $members; } - public function getMembers() + public function getMembers(): array { return $this->members; } @@ -1400,25 +1437,25 @@ Circular references are common when dealing with entity relations:: class Member { - private $name; - private $organization; + private string $name; + private Organization $organization; - public function setName($name) + public function setName(string $name): void { $this->name = $name; } - public function getName() + public function getName(): string { return $this->name; } - public function setOrganization(Organization $organization) + public function setOrganization(Organization $organization): void { $this->organization = $organization; } - public function getOrganization() + public function getOrganization(): Organization { return $this->organization; } @@ -1450,7 +1487,7 @@ having unique identifiers:: $encoder = new JsonEncoder(); $defaultContext = [ - AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) { + AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function (object $object, ?string $format, array $context): string { return $object->getName(); }, ]; @@ -1473,12 +1510,12 @@ structure:: class MyObj { - public $foo; + public string $foo; /** * @var self */ - public $child; + public MyObj $child; } $level1 = new MyObj(); @@ -1497,22 +1534,6 @@ Here, we set it to 2 for the ``$child`` property: .. configuration-block:: - .. code-block:: php-annotations - - namespace Acme; - - use Symfony\Component\Serializer\Annotation\MaxDepth; - - class MyObj - { - /** - * @MaxDepth(2) - */ - public $child; - - // ... - } - .. code-block:: php-attributes namespace Acme; @@ -1522,7 +1543,7 @@ Here, we set it to 2 for the ``$child`` property: class MyObj { #[MaxDepth(2)] - public $child; + public MyObj $child; // ... } @@ -1574,22 +1595,19 @@ Instead of throwing an exception, a custom callable can be executed when the maximum depth is reached. This is especially useful when serializing entities having unique identifiers:: - use Doctrine\Common\Annotations\AnnotationReader; use Symfony\Component\Serializer\Annotation\MaxDepth; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; - use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; + use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; class Foo { - public $id; + public int $id; - /** - * @MaxDepth(1) - */ - public $child; + #[MaxDepth(1)] + public MyObj $child; } $level1 = new Foo(); @@ -1603,10 +1621,10 @@ having unique identifiers:: $level3->id = 3; $level2->child = $level3; - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); // all callback parameters are optional (you can omit the ones you don't use) - $maxDepthHandler = function ($innerObject, $outerObject, string $attributeName, ?string $format = null, array $context = []) { + $maxDepthHandler = function (object $innerObject, object $outerObject, string $attributeName, ?string $format = null, array $context = []): string { return '/foos/'.$innerObject->id; }; @@ -1684,17 +1702,14 @@ context option:: class MyObj { - private $foo; - private $bar; - - public function __construct($foo, $bar) - { - $this->foo = $foo; - $this->bar = $bar; + public function __construct( + private string $foo, + private string $bar, + ) { } } - $normalizer = new ObjectNormalizer($classMetadataFactory); + $normalizer = new ObjectNormalizer(); $serializer = new Serializer([$normalizer]); $data = $serializer->denormalize( @@ -1728,34 +1743,34 @@ parameter of the ``ObjectNormalizer``:: class ObjectOuter { - private $inner; - private $date; + private ObjectInner $inner; + private \DateTimeInterface $date; - public function getInner() + public function getInner(): ObjectInner { return $this->inner; } - public function setInner(ObjectInner $inner) + public function setInner(ObjectInner $inner): void { $this->inner = $inner; } - public function setDate(\DateTimeInterface $date) + public function getDate(): \DateTimeInterface { - $this->date = $date; + return $this->date; } - public function getDate() + public function setDate(\DateTimeInterface $date): void { - return $this->date; + $this->date = $date; } } class ObjectInner { - public $foo; - public $bar; + public string $foo; + public string $bar; } $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); @@ -1807,7 +1822,7 @@ this is already set up and you only need to provide the configuration. Otherwise use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $discriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); @@ -1822,23 +1837,6 @@ and ``BitBucketCodeRepository`` classes: .. configuration-block:: - .. code-block:: php-annotations - - namespace App; - - use Symfony\Component\Serializer\Annotation\DiscriminatorMap; - - /** - * @DiscriminatorMap(typeProperty="type", mapping={ - * "github"="App\GitHubCodeRepository", - * "bitbucket"="App\BitBucketCodeRepository" - * }) - */ - abstract class CodeRepository - { - // ... - } - .. code-block:: php-attributes namespace App; @@ -1881,6 +1879,11 @@ and ``BitBucketCodeRepository`` classes: +.. note:: + + The values of the ``mapping`` array option must be strings. + Otherwise, they will be cast into strings automatically. + Once configured, the serializer uses the mapping to pick the correct class:: $serialized = $serializer->serialize(new GitHubCodeRepository(), 'json'); @@ -1926,3 +1929,6 @@ Learn more .. _`RFC 4122`: https://tools.ietf.org/html/rfc4122 .. _`PHP reflection`: https://php.net/manual/en/book.reflection.php .. _`data URI`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs +.. _seld/jsonlint: https://github.com/Seldaek/jsonlint +.. _$flags: https://www.php.net/manual/en/json.constants.php +.. _`a CDATA section`: https://en.wikipedia.org/wiki/CDATA diff --git a/components/type_info.rst b/components/type_info.rst new file mode 100644 index 00000000000..30ae11aa222 --- /dev/null +++ b/components/type_info.rst @@ -0,0 +1,86 @@ +The TypeInfo Component +====================== + +The TypeInfo component extracts type information from PHP elements like properties, +arguments and return types. + +This component provides: + +* A powerful ``Type`` definition that can handle unions, intersections, and generics + (and can be extended to support more types in the future); +* A way to get types from PHP elements such as properties, method arguments, + return types, and raw strings. + +.. caution:: + + This component is :doc:`experimental ` and + could be changed at any time without prior notice. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/type-info + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +This component gives you a :class:`Symfony\\Component\\TypeInfo\\Type` object that +represents the PHP type of anything you built or asked to resolve. + +There are two ways to use this component. First one is to create a type manually thanks +to the :class:`Symfony\\Component\\TypeInfo\\Type` static methods as following:: + + use Symfony\Component\TypeInfo\Type; + + Type::int(); + Type::nullable(Type::string()); + Type::generic(Type::object(Collection::class), Type::int()); + Type::list(Type::bool()); + Type::intersection(Type::object(\Stringable::class), Type::object(\Iterator::class)); + + // Many others are available and can be + // found in Symfony\Component\TypeInfo\TypeFactoryTrait + +The second way of using the component is to use ``TypeInfo`` to resolve a type +based on reflection or a simple string:: + + use Symfony\Component\TypeInfo\Type; + use Symfony\Component\TypeInfo\TypeResolver\TypeResolver; + + // Instantiate a new resolver + $typeResolver = TypeResolver::create(); + + // Then resolve types for any subject + $typeResolver->resolve(new \ReflectionProperty(Dummy::class, 'id')); // returns an "int" Type instance + $typeResolver->resolve('bool'); // returns a "bool" Type instance + + // Types can be instantiated thanks to static factories + $type = Type::list(Type::nullable(Type::bool())); + + // Type instances have several helper methods + + // returns the main type (e.g. in this example it returns an "array" Type instance); + // for nullable types (e.g. string|null) it returns the non-null type (e.g. string) + // and for compound types (e.g. int|string) it throws an exception because both types + // can be considered the main one, so there's no way to pick one + $baseType = $type->getBaseType(); + + // for collections, it returns the type of the item used as the key; + // in this example, the collection is a list, so it returns an "int" Type instance + $keyType = $type->getCollectionKeyType(); + + // you can chain the utility methods (e.g. to introspect the values of the collection) + // the following code will return true + $isValueNullable = $type->getCollectionValueType()->isNullable(); + +Each of these calls will return you a ``Type`` instance that corresponds to the +static method used. You can also resolve types from a string (as shown in the +``bool`` parameter of the previous example) + +.. note:: + + To support raw string resolving, you need to install ``phpstan/phpdoc-parser`` package. diff --git a/components/uid.rst b/components/uid.rst index 1731c392dba..7195d393ed3 100644 --- a/components/uid.rst +++ b/components/uid.rst @@ -4,10 +4,6 @@ The UID Component The UID component provides utilities to work with `unique identifiers`_ (UIDs) such as UUIDs and ULIDs. -.. versionadded:: 5.1 - - The UID component was introduced in Symfony 5.1. - Installation ------------ @@ -31,42 +27,120 @@ Generating UUIDs ~~~~~~~~~~~~~~~~ Use the named constructors of the ``Uuid`` class or any of the specific classes -to create each type of UUID:: +to create each type of UUID: + +**UUID v1** (time-based) + +Generates the UUID using a timestamp and the MAC address of your device +(`read UUIDv1 spec `__). +Both are obtained automatically, so you don't have to pass any constructor argument:: + + use Symfony\Component\Uid\Uuid; + + // $uuid is an instance of Symfony\Component\Uid\UuidV1 + $uuid = Uuid::v1(); + +.. tip:: + + It's recommended to use UUIDv7 instead of UUIDv1 because it provides + better entropy. + +**UUID v2** (DCE security) + +Similar to UUIDv1 but with a very high likelihood of ID collision +(`read UUIDv2 spec `__). +It's part of the authentication mechanism of DCE (Distributed Computing Environment) +and the UUID includes the POSIX UIDs (user/group ID) of the user who generated it. +This UUID variant is **not implemented** by the Uid component. + +**UUID v3** (name-based, MD5) + +Generates UUIDs from names that belong, and are unique within, some given namespace +(`read UUIDv3 spec `__). +This variant is useful to generate deterministic UUIDs from arbitrary strings. +It works by populating the UUID contents with the``md5`` hash of concatenating +the namespace and the name:: + + use Symfony\Component\Uid\Uuid; + + // you can use any of the predefined namespaces... + $namespace = Uuid::fromString(Uuid::NAMESPACE_OID); + // ...or use a random namespace: + // $namespace = Uuid::v4(); + + // $name can be any arbitrary string + // $uuid is an instance of Symfony\Component\Uid\UuidV3 + $uuid = Uuid::v3($namespace, $name); + +These are the default namespaces defined by the standard: + +* ``Uuid::NAMESPACE_DNS`` if you are generating UUIDs for `DNS entries `__ +* ``Uuid::NAMESPACE_URL`` if you are generating UUIDs for `URLs `__ +* ``Uuid::NAMESPACE_OID`` if you are generating UUIDs for `OIDs (object identifiers) `__ +* ``Uuid::NAMESPACE_X500`` if you are generating UUIDs for `X500 DNs (distinguished names) `__ + +**UUID v4** (random) + +Generates a random UUID (`read UUIDv4 spec `__). +Because of its randomness, it ensures uniqueness across distributed systems +without the need for a central coordinating entity. It's privacy-friendly +because it doesn't contain any information about where and when it was generated:: use Symfony\Component\Uid\Uuid; - // UUID type 1 generates the UUID using the MAC address of your device and a timestamp. - // Both are obtained automatically, so you don't have to pass any constructor argument. - $uuid = Uuid::v1(); // $uuid is an instance of Symfony\Component\Uid\UuidV1 + // $uuid is an instance of Symfony\Component\Uid\UuidV4 + $uuid = Uuid::v4(); + +**UUID v5** (name-based, SHA-1) - // UUID type 4 generates a random UUID, so you don't have to pass any constructor argument. - $uuid = Uuid::v4(); // $uuid is an instance of Symfony\Component\Uid\UuidV4 +It's the same as UUIDv3 (explained above) but it uses ``sha1`` instead of +``md5`` to hash the given namespace and name (`read UUIDv5 spec `__). +This makes it more secure and less prone to hash collisions. + +.. _uid-uuid-v6: + +**UUID v6** (reordered time-based) + +It rearranges the time-based fields of the UUIDv1 to make it lexicographically +sortable (like :ref:`ULIDs `). It's more efficient for database indexing +(`read UUIDv6 spec `__):: + + use Symfony\Component\Uid\Uuid; - // UUID type 3 and 5 generate a UUID hashing the given namespace and name. Type 3 uses - // MD5 hashes and Type 5 uses SHA-1. The namespace is another UUID (e.g. a Type 4 UUID) - // and the name is an arbitrary string (e.g. a product name; if it's unique). - $namespace = Uuid::v4(); - $name = $product->getUniqueName(); + // $uuid is an instance of Symfony\Component\Uid\UuidV6 + $uuid = Uuid::v6(); - $uuid = Uuid::v3($namespace, $name); // $uuid is an instance of Symfony\Component\Uid\UuidV3 - $uuid = Uuid::v5($namespace, $name); // $uuid is an instance of Symfony\Component\Uid\UuidV5 +.. tip:: - // the namespaces defined by RFC 4122 (see https://tools.ietf.org/html/rfc4122#appendix-C) - // are available as PHP constants and as string values - $uuid = Uuid::v3(Uuid::NAMESPACE_DNS, $name); // same as: Uuid::v3('dns', $name); - $uuid = Uuid::v3(Uuid::NAMESPACE_URL, $name); // same as: Uuid::v3('url', $name); - $uuid = Uuid::v3(Uuid::NAMESPACE_OID, $name); // same as: Uuid::v3('oid', $name); - $uuid = Uuid::v3(Uuid::NAMESPACE_X500, $name); // same as: Uuid::v3('x500', $name); + It's recommended to use UUIDv7 instead of UUIDv6 because it provides + better entropy. - // UUID type 6 is not yet part of the UUID standard. It's lexicographically sortable - // (like ULIDs) and contains a 60-bit timestamp and 63 extra unique bits. - // It's defined in https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-6 - $uuid = Uuid::v6(); // $uuid is an instance of Symfony\Component\Uid\UuidV6 +.. _uid-uuid-v7: -.. versionadded:: 5.3 +**UUID v7** (UNIX timestamp) - The ``Uuid::NAMESPACE_*`` constants and the namespace string values (``'dns'``, - ``'url'``, etc.) were introduced in Symfony 5.3. +Generates time-ordered UUIDs based on a high-resolution Unix Epoch timestamp +source (the number of milliseconds since midnight 1 Jan 1970 UTC, leap seconds excluded) +(`read UUIDv7 spec `__). +It's recommended to use this version over UUIDv1 and UUIDv6 because it provides +better entropy (and a more strict chronological order of UUID generation):: + + use Symfony\Component\Uid\Uuid; + + // $uuid is an instance of Symfony\Component\Uid\UuidV7 + $uuid = Uuid::v7(); + +**UUID v8** (custom) + +Provides an RFC-compatible format for experimental or vendor-specific use cases +(`read UUIDv8 spec `__). +The only requirement is to set the variant and version bits of the UUID. The rest +of the UUID value is specific to each implementation and no format should be assumed:: + + use Symfony\Component\Uid\Uuid; + + // $uuid is an instance of Symfony\Component\Uid\UuidV8 + $uuid = Uuid::v8(); If your UUID value is already generated in another format, use any of the following methods to create a ``Uuid`` object from it:: @@ -78,11 +152,6 @@ following methods to create a ``Uuid`` object from it:: $uuid = Uuid::fromBase58('TuetYWNHhmuSQ3xPoVLv9M'); $uuid = Uuid::fromRfc4122('d9e7a184-5d5b-11ea-a62a-3499710062d0'); -.. versionadded:: 5.3 - - The ``fromBinary()``, ``fromBase32()``, ``fromBase58()`` and ``fromRfc4122()`` - methods were introduced in Symfony 5.3. - You can also use the ``UuidFactory`` to generate UUIDs. First, you may configure the behavior of the factory using configuration files:: @@ -93,10 +162,10 @@ configure the behavior of the factory using configuration files:: # config/packages/uid.yaml framework: uid: - default_uuid_version: 6 + default_uuid_version: 7 name_based_uuid_version: 5 name_based_uuid_namespace: 6ba7b810-9dad-11d1-80b4-00c04fd430c8 - time_based_uuid_version: 6 + time_based_uuid_version: 7 time_based_uuid_node: 121212121212 .. code-block:: xml @@ -112,10 +181,10 @@ configure the behavior of the factory using configuration files:: @@ -134,10 +203,10 @@ configure the behavior of the factory using configuration files:: $container->extension('framework', [ 'uid' => [ - 'default_uuid_version' => 6, + 'default_uuid_version' => 7, 'name_based_uuid_version' => 5, 'name_based_uuid_namespace' => '6ba7b810-9dad-11d1-80b4-00c04fd430c8', - 'time_based_uuid_version' => 6, + 'time_based_uuid_version' => 7, 'time_based_uuid_node' => 121212121212, ], ]); @@ -152,16 +221,14 @@ on the configuration you defined:: class FooService { - private UuidFactory $uuidFactory; - - public function __construct(UuidFactory $uuidFactory) - { - $this->uuidFactory = $uuidFactory; + public function __construct( + private UuidFactory $uuidFactory, + ) { } public function generate(): void { - // This creates a UUID of the version given in the configuration file (v6 by default) + // This creates a UUID of the version given in the configuration file (v7 by default) $uuid = $this->uuidFactory->create(); $nameBasedUuid = $this->uuidFactory->nameBased(/** ... */); @@ -172,10 +239,6 @@ on the configuration you defined:: } } -.. versionadded:: 5.3 - - The ``UuidFactory`` was introduced in Symfony 5.3. - Converting UUIDs ~~~~~~~~~~~~~~~~ @@ -187,6 +250,32 @@ Use these methods to transform the UUID object into different bases:: $uuid->toBase32(); // string(26) "6SWYGR8QAV27NACAHMK5RG0RPG" $uuid->toBase58(); // string(22) "TuetYWNHhmuSQ3xPoVLv9M" $uuid->toRfc4122(); // string(36) "d9e7a184-5d5b-11ea-a62a-3499710062d0" + $uuid->toHex(); // string(34) "0xd9e7a1845d5b11eaa62a3499710062d0" + $uuid->toString(); // string(36) "d9e7a184-5d5b-11ea-a62a-3499710062d0" + +.. versionadded:: 7.1 + + The ``toString()`` method was introduced in Symfony 7.1. + +You can also convert some UUID versions to others:: + + // convert V1 to V6 or V7 + $uuid = Uuid::v1(); + + $uuid->toV6(); // returns a Symfony\Component\Uid\UuidV6 instance + $uuid->toV7(); // returns a Symfony\Component\Uid\UuidV7 instance + + // convert V6 to V7 + $uuid = Uuid::v6(); + + $uuid->toV7(); // returns a Symfony\Component\Uid\UuidV7 instance + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\Uid\\UuidV1::toV6`, + :method:`Symfony\\Component\\Uid\\UuidV1::toV7` and + :method:`Symfony\\Component\\Uid\\UuidV6::toV7` + methods were introduced in Symfony 7.1. Working with UUIDs ~~~~~~~~~~~~~~~~~~ @@ -225,11 +314,6 @@ UUID objects created with the ``Uuid`` class can use the following methods // * int < 0 if $uuid1 is less than $uuid4 $uuid1->compare($uuid4); // e.g. int(4) -.. versionadded:: 5.3 - - The ``getDateTime()`` method was introduced in Symfony 5.3. In previous - versions it was called ``getTime()``. - Storing UUIDs in Databases ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -240,41 +324,34 @@ type, which converts to/from UUID objects automatically:: namespace App\Entity; use Doctrine\ORM\Mapping as ORM; + use Symfony\Bridge\Doctrine\Types\UuidType; + use Symfony\Component\Uid\Uuid; - /** - * @ORM\Entity(repositoryClass="App\Repository\ProductRepository") - */ + #[ORM\Entity(repositoryClass: ProductRepository::class)] class Product { - /** - * @ORM\Column(type="uuid") - */ - private $someProperty; + #[ORM\Column(type: UuidType::NAME)] + private Uuid $someProperty; // ... } -.. versionadded:: 5.2 - - The UUID type was introduced in Symfony 5.2. - There's also a Doctrine generator to help auto-generate UUID values for the entity primary keys:: namespace App\Entity; use Doctrine\ORM\Mapping as ORM; + use Symfony\Bridge\Doctrine\Types\UuidType; use Symfony\Component\Uid\Uuid; class User implements UserInterface { - /** - * @ORM\Id - * @ORM\Column(type="uuid", unique=true) - * @ORM\GeneratedValue(strategy="CUSTOM") - * @ORM\CustomIdGenerator(class="doctrine.uuid_generator") - */ - private $id; + #[ORM\Id] + #[ORM\Column(type: UuidType::NAME, unique: true)] + #[ORM\GeneratedValue(strategy: 'CUSTOM')] + #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')] + private ?Uuid $id; public function getId(): ?Uuid { @@ -284,6 +361,14 @@ entity primary keys:: // ... } +.. caution:: + + Using UUIDs as primary keys is usually not recommended for performance reasons: + indexes are slower and take more space (because UUIDs in binary format take + 128 bits instead of 32/64 bits for auto-incremental integers) and the non-sequential + nature of UUIDs fragments indexes. :ref:`UUID v6 ` and :ref:`UUID v7 ` + are the only variants that solve the fragmentation issue (but the index size issue remains). + When using built-in Doctrine repository methods (e.g. ``findOneBy()``), Doctrine knows how to convert these UUID types to build the SQL query (e.g. ``->findOneBy(['user' => $user->getUuid()])``). However, when using DQL @@ -293,6 +378,9 @@ of the UUID parameters:: // src/Repository/ProductRepository.php // ... + use Doctrine\DBAL\ParameterType; + use Symfony\Bridge\Doctrine\Types\UuidType; + class ProductRepository extends ServiceEntityRepository { // ... @@ -301,12 +389,12 @@ of the UUID parameters:: { $qb = $this->createQueryBuilder('p') // ... - // add 'uuid' as the third argument to tell Doctrine that this is a UUID - ->setParameter('user', $user->getUuid(), 'uuid') + // add UuidType::NAME as the third argument to tell Doctrine that this is a UUID + ->setParameter('user', $user->getUuid(), UuidType::NAME) // alternatively, you can convert it to a value compatible with // the type inferred by Doctrine - ->setParameter('user', $user->getUuid()->toBinary()) + ->setParameter('user', $user->getUuid()->toBinary(), ParameterType::BINARY) ; // ... @@ -352,11 +440,6 @@ following methods to create a ``Ulid`` object from it:: $ulid = Ulid::fromBase58('1BKocMc5BnrVcuq2ti4Eqm'); $ulid = Ulid::fromRfc4122('0171069d-593d-97d3-8b3e-23d06de5b308'); -.. versionadded:: 5.3 - - The ``fromBinary()``, ``fromBase32()``, ``fromBase58()`` and ``fromRfc4122()`` - methods were introduced in Symfony 5.3. - Like UUIDs, ULIDs have their own factory, ``UlidFactory``, that can be used to generate them:: namespace App\Service; @@ -365,11 +448,9 @@ Like UUIDs, ULIDs have their own factory, ``UlidFactory``, that can be used to g class FooService { - private UlidFactory $ulidFactory; - - public function __construct(UlidFactory $ulidFactory) - { - $this->ulidFactory = $ulidFactory; + public function __construct( + private UlidFactory $ulidFactory, + ) { } public function generate(): void @@ -380,10 +461,6 @@ Like UUIDs, ULIDs have their own factory, ``UlidFactory``, that can be used to g } } -.. versionadded:: 5.3 - - The ``UlidFactory`` was introduced in Symfony 5.3. - There's also a special ``NilUlid`` class to represent ULID ``null`` values:: use Symfony\Component\Uid\NilUlid; @@ -391,10 +468,6 @@ There's also a special ``NilUlid`` class to represent ULID ``null`` values:: $ulid = new NilUlid(); // equivalent to $ulid = new Ulid('00000000000000000000000000'); -.. versionadded:: 5.4 - - The ``NilUlid`` class was introduced in Symfony 5.4. - Converting ULIDs ~~~~~~~~~~~~~~~~ @@ -406,6 +479,7 @@ Use these methods to transform the ULID object into different bases:: $ulid->toBase32(); // string(26) "01E439TP9XJZ9RPFH3T1PYBCR8" $ulid->toBase58(); // string(22) "1BKocMc5BnrVcuq2ti4Eqm" $ulid->toRfc4122(); // string(36) "0171069d-593d-97d3-8b3e-23d06de5b308" + $ulid->toHex(); // string(34) "0x0171069d593d97d38b3e23d06de5b308" Working with ULIDs ~~~~~~~~~~~~~~~~~~ @@ -428,11 +502,6 @@ ULID objects created with the ``Ulid`` class can use the following methods:: // this method returns $ulid1 <=> $ulid2 $ulid1->compare($ulid2); // e.g. int(-1) -.. versionadded:: 5.3 - - The ``getDateTime()`` method was introduced in Symfony 5.3. In previous - versions it was called ``getTime()``. - Storing ULIDs in Databases ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -443,16 +512,14 @@ type, which converts to/from ULID objects automatically:: namespace App\Entity; use Doctrine\ORM\Mapping as ORM; + use Symfony\Bridge\Doctrine\Types\UlidType; + use Symfony\Component\Uid\Ulid; - /** - * @ORM\Entity(repositoryClass="App\Repository\ProductRepository") - */ + #[ORM\Entity(repositoryClass: ProductRepository::class)] class Product { - /** - * @ORM\Column(type="ulid") - */ - private $someProperty; + #[ORM\Column(type: UlidType::NAME)] + private Ulid $someProperty; // ... } @@ -463,17 +530,16 @@ entity primary keys:: namespace App\Entity; use Doctrine\ORM\Mapping as ORM; + use Symfony\Bridge\Doctrine\Types\UlidType; use Symfony\Component\Uid\Ulid; class Product { - /** - * @ORM\Id - * @ORM\Column(type="ulid", unique=true) - * @ORM\GeneratedValue(strategy="CUSTOM") - * @ORM\CustomIdGenerator(class="doctrine.ulid_generator") - */ - private $id; + #[ORM\Id] + #[ORM\Column(type: UlidType::NAME, unique: true)] + #[ORM\GeneratedValue(strategy: 'CUSTOM')] + #[ORM\CustomIdGenerator(class: 'doctrine.ulid_generator')] + private ?Ulid $id; public function getId(): ?Ulid { @@ -481,12 +547,14 @@ entity primary keys:: } // ... - } -.. versionadded:: 5.2 +.. caution:: - The ULID type and generator were introduced in Symfony 5.2. + Using ULIDs as primary keys is usually not recommended for performance reasons. + Although ULIDs don't suffer from index fragmentation issues (because the values + are sequential), their indexes are slower and take more space (because ULIDs + in binary format take 128 bits instead of 32/64 bits for auto-incremental integers). When using built-in Doctrine repository methods (e.g. ``findOneBy()``), Doctrine knows how to convert these ULID types to build the SQL query @@ -497,6 +565,8 @@ of the ULID parameters:: // src/Repository/ProductRepository.php // ... + use Symfony\Bridge\Doctrine\Types\UlidType; + class ProductRepository extends ServiceEntityRepository { // ... @@ -505,8 +575,8 @@ of the ULID parameters:: { $qb = $this->createQueryBuilder('p') // ... - // add 'ulid' as the third argument to tell Doctrine that this is a ULID - ->setParameter('user', $user->getUlid(), 'ulid') + // add UlidType::NAME as the third argument to tell Doctrine that this is a ULID + ->setParameter('user', $user->getUlid(), UlidType::NAME) // alternatively, you can convert it to a value compatible with // the type inferred by Doctrine @@ -520,10 +590,6 @@ of the ULID parameters:: Generating and Inspecting UUIDs/ULIDs in the Console ---------------------------------------------------- -.. versionadded:: 5.3 - - The commands to inspect and generate UUIDs/ULIDs were introduced in Symfony 5.3. - This component provides several commands to generate and inspect UUIDs/ULIDs in the console. They are not enabled by default, so you must add the following configuration in your application before using these commands: @@ -598,7 +664,7 @@ commands to learn about all their options): # generate 1 ULID with a specific timestamp $ php bin/console ulid:generate --time="2021-02-02 14:00:00" - # generate 2 ULIDs and ouput them in RFC4122 format + # generate 2 ULIDs and output them in RFC4122 format $ php bin/console ulid:generate --count=2 --format=rfc4122 In addition to generating new UIDs, you can also inspect them with the following diff --git a/components/validator/metadata.rst b/components/validator/metadata.rst index 07ee9c52d79..e7df42413bc 100755 --- a/components/validator/metadata.rst +++ b/components/validator/metadata.rst @@ -17,9 +17,9 @@ the ``Author`` class has at least 3 characters:: class Author { - private $firstName; + private string $firstName; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', new Assert\NotBlank()); $metadata->addPropertyConstraint( @@ -40,7 +40,7 @@ Suppose that, for security reasons, you want to validate that a password field doesn't match the first name of the user. First, create a public method called ``isPasswordSafe()`` to define this custom validation logic:: - public function isPasswordSafe() + public function isPasswordSafe(): bool { return $this->firstName !== $this->password; } @@ -53,7 +53,7 @@ Then, add the Validator component configuration to the class:: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue([ 'message' => 'The password cannot match your first name', @@ -74,7 +74,7 @@ validation logic:: // ... use Symfony\Component\Validator\Context\ExecutionContextInterface; - public function validate(ExecutionContextInterface $context) + public function validate(ExecutionContextInterface $context): void { // ... } @@ -87,7 +87,7 @@ Then, add the Validator component configuration to the class:: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addConstraint(new Assert\Callback('validate')); } diff --git a/components/validator/resources.rst b/components/validator/resources.rst index 4baf4fbdd65..c1474c1710d 100644 --- a/components/validator/resources.rst +++ b/components/validator/resources.rst @@ -37,9 +37,9 @@ In this example, the validation metadata is retrieved executing the class User { - protected $name; + protected string $name; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('name', new Assert\NotBlank()); $metadata->addPropertyConstraint('name', new Assert\Length([ @@ -83,41 +83,27 @@ configure the locations of these files:: :method:`Symfony\\Component\\Validator\\ValidatorBuilder::addXmlMappings` to configure an array of file paths. -The AnnotationLoader --------------------- +The AttributeLoader +------------------- -At last, the component provides an -:class:`Symfony\\Component\\Validator\\Mapping\\Loader\\AnnotationLoader` to get -the metadata from the annotations of the class. Annotations are defined as ``@`` -prefixed classes included in doc block comments (``/** ... */``). For example:: +The component provides an +:class:`Symfony\\Component\\Validator\\Mapping\\Loader\\AttributeLoader` to get +the metadata from the attributes of the class. For example:: use Symfony\Component\Validator\Constraints as Assert; // ... class User { - /** - * @Assert\NotBlank - */ - protected $name; + #[Assert\NotBlank] + protected string $name; } -To enable the annotation loader, call the -:method:`Symfony\\Component\\Validator\\ValidatorBuilder::enableAnnotationMapping` method -and then call ``addDefaultDoctrineAnnotationReader()`` to use Doctrine's -annotation reader:: - - use Symfony\Component\Validator\Validation; - - $validator = Validation::createValidatorBuilder() - ->enableAnnotationMapping(true) - ->addDefaultDoctrineAnnotationReader() - ->getValidator(); +To enable the attribute loader, call the +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::enableAttributeMapping` method. To disable the annotation loader after it was enabled, call -:method:`Symfony\\Component\\Validator\\ValidatorBuilder::disableAnnotationMapping`. - -.. include:: /_includes/_annotation_loader_tip.rst.inc +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::disableAttributeMapping`. Using Multiple Loaders ---------------------- @@ -132,8 +118,7 @@ multiple mappings:: use Symfony\Component\Validator\Validation; $validator = Validation::createValidatorBuilder() - ->enableAnnotationMapping(true) - ->addDefaultDoctrineAnnotationReader() + ->enableAttributeMapping() ->addMethodMapping('loadValidatorMetadata') ->addXmlMapping('validator/validation.xml') ->getValidator(); diff --git a/components/var_dumper.rst b/components/var_dumper.rst index b6cb8c4b346..3f59ff1b796 100644 --- a/components/var_dumper.rst +++ b/components/var_dumper.rst @@ -144,7 +144,7 @@ the :ref:`dump_destination option ` of the // config/packages/debug.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->extension('debug', [ 'dump_destination' => 'tcp://%env(VAR_DUMPER_SERVER)%', ]); @@ -169,8 +169,8 @@ Outside a Symfony application, use the :class:`Symfony\\Component\\VarDumper\\Du 'source' => new SourceContextProvider(), ]); - VarDumper::setHandler(function ($var) use ($cloner, $dumper) { - $dumper->dump($cloner->cloneVar($var)); + VarDumper::setHandler(function (mixed $var) use ($cloner, $dumper): ?string { + return $dumper->dump($cloner->cloneVar($var)); }); .. note:: @@ -193,10 +193,6 @@ Then you can use the following command to start a server out-of-the-box: Configuring the Dump Server with Environment Variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.2 - - The ``VAR_DUMPER_FORMAT=server`` feature was introduced in Symfony 5.2. - If you prefer to not modify the application configuration (e.g. to quickly debug a project given to you) use the ``VAR_DUMPER_FORMAT`` env var. @@ -298,7 +294,7 @@ Example:: { use VarDumperTestTrait; - protected function setUp() + protected function setUp(): void { $casters = [ \DateTimeInterface::class => static function (\DateTimeInterface $date, array $a, Stub $stub): array { @@ -315,7 +311,7 @@ Example:: $this->setUpVarDumper($casters, $flags); } - public function testWithDumpEquals() + public function testWithDumpEquals(): void { $testedVar = [123, 'foo']; @@ -378,9 +374,9 @@ then its dump representation:: class PropertyExample { - public $publicProperty = 'The `+` prefix denotes public properties,'; - protected $protectedProperty = '`#` protected ones and `-` private ones.'; - private $privateProperty = 'Hovering a property shows a reminder.'; + public string $publicProperty = 'The `+` prefix denotes public properties,'; + protected string $protectedProperty = '`#` protected ones and `-` private ones.'; + private string $privateProperty = 'Hovering a property shows a reminder.'; } $var = new PropertyExample(); @@ -398,7 +394,7 @@ then its dump representation:: class DynamicPropertyExample { - public $declaredProperty = 'This property is declared in the class definition'; + public string $declaredProperty = 'This property is declared in the class definition'; } $var = new DynamicPropertyExample(); @@ -412,7 +408,7 @@ then its dump representation:: class ReferenceExample { - public $info = "Circular and sibling references are displayed as `#number`.\nHovering them highlights all instances in the same dump.\n"; + public string $info = "Circular and sibling references are displayed as `#number`.\nHovering them highlights all instances in the same dump.\n"; } $var = new ReferenceExample(); $var->aCircularReference = $var; @@ -474,6 +470,21 @@ then its dump representation:: .. image:: /_images/components/var_dumper/09-cut.png :alt: Dump output where the children of the Container object are hidden. +.. code-block:: php + + class Foo + { + // $foo is uninitialized, which is different from being null + private int|float $foo; + public ?string $baz = null; + } + + $var = new Foo(); + dump($var); + +.. image:: /_images/components/var_dumper/10-uninitialized.png + :alt: Dump output where the uninitialized property is represented by a question mark followed by the type definition. + .. _var-dumper-advanced: Advanced Usage @@ -494,11 +505,11 @@ like this:: use Symfony\Component\VarDumper\Dumper\HtmlDumper; use Symfony\Component\VarDumper\VarDumper; - VarDumper::setHandler(function ($var) { + VarDumper::setHandler(function (mixed $var): ?string { $cloner = new VarCloner(); $dumper = 'cli' === PHP_SAPI ? new CliDumper() : new HtmlDumper(); - $dumper->dump($cloner->cloneVar($var)); + return $dumper->dump($cloner->cloneVar($var)); }); Cloners @@ -612,7 +623,7 @@ For example, to get a dump as a string in a variable, you can do:: $dumper->dump( $cloner->cloneVar($variable), - function ($line, $depth) use (&$output) { + function (string $line, int $depth) use (&$output): void { // A negative depth means "end of dump" if ($depth >= 0) { // Adds a two spaces indentation to the line @@ -810,7 +821,7 @@ Here is a simple caster not doing anything:: use Symfony\Component\VarDumper\Cloner\Stub; - function myCaster($object, $array, Stub $stub, $isNested, $filter) + function myCaster(mixed $object, array $array, Stub $stub, bool $isNested, int $filter): array { // ... populate/alter $array to your needs @@ -874,7 +885,7 @@ that holds a file name or a URL, you can wrap them in a ``LinkStub`` to tell use Symfony\Component\VarDumper\Caster\LinkStub; use Symfony\Component\VarDumper\Cloner\Stub; - function ProductCaster(Product $object, $array, Stub $stub, $isNested, $filter = 0) + function ProductCaster(Product $object, array $array, Stub $stub, bool $isNested, int $filter = 0): array { $array['brochure'] = new LinkStub($array['brochure']); diff --git a/components/var_exporter.rst b/components/var_exporter.rst index 0b83b94dd76..6aa4279788e 100644 --- a/components/var_exporter.rst +++ b/components/var_exporter.rst @@ -50,10 +50,10 @@ following class hierarchy:: abstract class AbstractClass { - protected $foo; - private $bar; + protected int $foo; + private int $bar; - protected function setBar($bar) + protected function setBar($bar): void { $this->bar = $bar; } @@ -90,12 +90,16 @@ file looks like this:: [] ); -Instantiating PHP Classes -------------------------- +.. _instantiating-php-classes: -The other main feature provided by this component is an instantiator which can -create objects and set their properties without calling their constructors or -any other methods:: +Instantiating & Hydrating PHP Classes +------------------------------------- + +Instantiator +~~~~~~~~~~~~ + +This component provides an instantiator, which can create objects and set +their properties without calling their constructors or any other methods:: use Symfony\Component\VarExporter\Instantiator; @@ -105,6 +109,11 @@ any other methods:: // creates a Foo instance and sets one of its properties $fooObject = Instantiator::instantiate(Foo::class, ['propertyName' => $propertyValue]); +The instantiator can also populate the property of a parent class. Assuming ``Bar`` +is the parent class of ``Foo`` and defines a ``privateBarProperty`` attribute:: + + use Symfony\Component\VarExporter\Instantiator; + // creates a Foo instance and sets a private property defined on its parent Bar class $fooObject = Instantiator::instantiate(Foo::class, [], [ Bar::class => ['privateBarProperty' => $propertyValue], @@ -113,7 +122,9 @@ any other methods:: Instances of ``ArrayObject``, ``ArrayIterator`` and ``SplObjectHash`` can be created by using the special ``"\0"`` property name to define their internal value:: - // Creates an SplObjectHash where $info1 is associated with $object1, etc. + use Symfony\Component\VarExporter\Instantiator; + + // creates an SplObjectStorage where $info1 is associated with $object1, etc. $theObject = Instantiator::instantiate(SplObjectStorage::class, [ "\0" => [$object1, $info1, $object2, $info2...], ]); @@ -123,5 +134,215 @@ created by using the special ``"\0"`` property name to define their internal val "\0" => [$inputArray], ]); +Hydrator +~~~~~~~~ + +Instead of populating objects that don't exist yet (using the instantiator), +sometimes you want to populate properties of an already existing object. This is +the goal of the :class:`Symfony\\Component\\VarExporter\\Hydrator`. Here is a +basic usage of the hydrator populating a property of an object:: + + use Symfony\Component\VarExporter\Hydrator; + + $object = new Foo(); + Hydrator::hydrate($object, ['propertyName' => $propertyValue]); + +The hydrator can also populate the property of a parent class. Assuming ``Bar`` +is the parent class of ``Foo`` and defines a ``privateBarProperty`` attribute:: + + use Symfony\Component\VarExporter\Hydrator; + + $object = new Foo(); + Hydrator::hydrate($object, [], [ + Bar::class => ['privateBarProperty' => $propertyValue], + ]); + + // alternatively, you can use the special "\0" syntax + Hydrator::hydrate($object, ["\0Bar\0privateBarProperty" => $propertyValue]); + +Instances of ``ArrayObject``, ``ArrayIterator`` and ``SplObjectHash`` can be +populated by using the special ``"\0"`` property name to define their internal value:: + + use Symfony\Component\VarExporter\Hydrator; + + // creates an SplObjectHash where $info1 is associated with $object1, etc. + $storage = new SplObjectStorage(); + Hydrator::hydrate($storage, [ + "\0" => [$object1, $info1, $object2, $info2...], + ]); + + // creates an ArrayObject populated with $inputArray + $arrayObject = new ArrayObject(); + Hydrator::hydrate($arrayObject, [ + "\0" => [$inputArray], + ]); + +Creating Lazy Objects +--------------------- + +Lazy-objects are objects instantiated empty and populated on-demand. This is +particularly useful when you have for example properties in your classes that +requires some heavy computation to determine their value. In this case, you +may want to trigger the property's value processing only when you actually need +its value. Thanks to this, the heavy computation won't be done if you never use +this property. The VarExporter component is bundled with two traits helping +you implement such mechanism easily in your classes. + +.. _var-exporter_ghost-objects: + +LazyGhostTrait +~~~~~~~~~~~~~~ + +Ghost objects are empty objects, which see their properties populated the first +time any method is called. Thanks to :class:`Symfony\\Component\\VarExporter\\LazyGhostTrait`, +the implementation of the lazy mechanism is eased. The ``MyLazyObject::populateHash()`` +method will be called only when the object is actually used and needs to be +initialized:: + + namespace App\Hash; + + use Symfony\Component\VarExporter\LazyGhostTrait; + + class HashProcessor + { + use LazyGhostTrait; + + // This property may require a heavy computation to have its value + public readonly string $hash; + + public function __construct() + { + self::createLazyGhost(initializer: $this->populateHash(...), instance: $this); + } + + private function populateHash(array $data): void + { + // Compute $this->hash value with the passed data + } + } + +:class:`Symfony\\Component\\VarExporter\\LazyGhostTrait` also allows to +convert non-lazy classes to lazy ones:: + + namespace App\Hash; + + use Symfony\Component\VarExporter\LazyGhostTrait; + + class HashProcessor + { + public readonly string $hash; + + public function __construct(array $data) + { + $this->populateHash($data); + } + + private function populateHash(array $data): void + { + // ... + } + + public function validateHash(): bool + { + // ... + } + } + + class LazyHashProcessor extends HashProcessor + { + use LazyGhostTrait; + } + + $processor = LazyHashProcessor::createLazyGhost(initializer: function (HashProcessor $instance): void { + // Do any operation you need here: call setters, getters, methods to validate the hash, etc. + $data = /** Retrieve required data to compute the hash */; + $instance->__construct(...$data); + $instance->validateHash(); + }); + +While you never query ``$processor->hash`` value, heavy methods will never be +triggered. But still, the ``$processor`` object exists and can be used in your +code, passed to methods, functions, etc. + +Additionally and by adding two arguments to the initializer function, it is +possible to initialize properties one-by-one:: + + $processor = LazyHashProcessor::createLazyGhost(initializer: function (HashProcessor $instance, string $propertyName, ?string $propertyScope): mixed { + if (HashProcessor::class === $propertyScope && 'hash' === $propertyName) { + // Return $hash value + } + + // Then you can add more logic for the other properties + }); + +Ghost objects unfortunately can't work with abstract classes or internal PHP +classes. Nevertheless, the VarExporter component covers this need with the help +of :ref:`Virtual Proxies `. + +.. _var-exporter_virtual-proxies: + +LazyProxyTrait +~~~~~~~~~~~~~~ + +The purpose of virtual proxies in the same one as +:ref:`ghost objects `, but their internal behavior is +totally different. Where ghost objects requires to extend a base class, virtual +proxies take advantage of the **Liskov Substitution principle**. This principle +describes that if two objects are implementing the same interface, you can swap +between the different implementations without breaking your application. This is +what virtual proxies take advantage of. To use virtual proxies, you may use +:class:`Symfony\\Component\\VarExporter\\ProxyHelper` to generate proxy's class +code:: + + namespace App\Hash; + + use Symfony\Component\VarExporter\ProxyHelper; + + interface ProcessorInterface + { + public function getHash(): bool; + } + + abstract class AbstractProcessor implements ProcessorInterface + { + protected string $hash; + + public function getHash(): bool + { + return $this->hash; + } + } + + class HashProcessor extends AbstractProcessor + { + public function __construct(array $data) + { + $this->populateHash($data); + } + + private function populateHash(array $data): void + { + // ... + } + } + + $proxyCode = ProxyHelper::generateLazyProxy(new \ReflectionClass(AbstractProcessor::class)); + // $proxyCode contains the actual proxy and the reference to LazyProxyTrait. + // In production env, this should be dumped into a file to avoid calling eval(). + eval('class HashProcessorProxy'.$proxyCode); + + $processor = HashProcessorProxy::createLazyProxy(initializer: function (): ProcessorInterface { + $data = /** Retrieve required data to compute the hash */; + $instance = new HashProcessor(...$data); + + // Do any operation you need here: call setters, getters, methods to validate the hash, etc. + + return $instance; + }); + +Just like ghost objects, while you never query ``$processor->hash``, its value +will not be computed. The main difference with ghost objects is that this time, +a proxy of an abstract class was created. This also works with internal PHP class. + .. _`OPcache`: https://www.php.net/opcache .. _`PSR-2`: https://www.php-fig.org/psr/psr-2/ diff --git a/components/workflow.rst b/components/workflow.rst index 2e5e1eb0aa6..e3da25b3476 100644 --- a/components/workflow.rst +++ b/components/workflow.rst @@ -58,13 +58,11 @@ logic in one place and not spread all over your application. Usage ----- -When you have configured a ``Registry`` with your workflows, -you can retrieve a workflow from it and use it as follows:: +Here's an example of using the workflow defined above:: // ... // Consider that $blogPost is in place "draft" by default $blogPost = new BlogPost(); - $workflow = $registry->get($blogPost); $workflow->can($blogPost, 'publish'); // False $workflow->can($blogPost, 'to_review'); // True diff --git a/components/yaml.rst b/components/yaml.rst index 5d007738d09..ea1c1f4af3a 100644 --- a/components/yaml.rst +++ b/components/yaml.rst @@ -330,6 +330,51 @@ syntax to parse them as proper PHP constants:: $parameters = Yaml::parse($yaml, Yaml::PARSE_CONSTANT); // $parameters = ['foo' => 'PHP_INT_SIZE', 'bar' => 8]; +Parsing PHP Enumerations +~~~~~~~~~~~~~~~~~~~~~~~~ + +The YAML parser supports `PHP enumerations`_, both unit and backed enums. +By default, they are parsed as regular strings. Use the ``PARSE_CONSTANT`` flag +and the special ``!php/enum`` syntax to parse them as proper PHP enums:: + + enum FooEnum: string + { + case Foo = 'foo'; + case Bar = 'bar'; + } + + // ... + + $yaml = '{ foo: FooEnum::Foo, bar: !php/enum FooEnum::Foo }'; + $parameters = Yaml::parse($yaml, Yaml::PARSE_CONSTANT); + // the value of the 'foo' key is a string because it missed the `!php/enum` syntax + // $parameters = ['foo' => 'FooEnum::Foo', 'bar' => FooEnum::Foo]; + + $yaml = '{ foo: FooEnum::Foo, bar: !php/enum FooEnum::Foo->value }'; + $parameters = Yaml::parse($yaml, Yaml::PARSE_CONSTANT); + // the value of the 'foo' key is a string because it missed the `!php/enum` syntax + // $parameters = ['foo' => 'FooEnum::Foo', 'bar' => 'foo']; + +You can also use ``!php/enum`` to get all the enumeration cases by only +giving the enumeration FQCN:: + + enum FooEnum: string + { + case Foo = 'foo'; + case Bar = 'bar'; + } + + // ... + + $yaml = '{ bar: !php/enum FooEnum }'; + $parameters = Yaml::parse($yaml, Yaml::PARSE_CONSTANT); + // $parameters = ['bar' => ['foo', 'bar']]; + +.. versionadded:: 7.1 + + The support for using the enum FQCN without specifying a case + was introduced in Symfony 7.1. + Parsing and Dumping of Binary Data ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -381,6 +426,18 @@ you can dump them as ``~`` with the ``DUMP_NULL_AS_TILDE`` flag:: $dumped = Yaml::dump(['foo' => null], 2, 4, Yaml::DUMP_NULL_AS_TILDE); // foo: ~ +Dumping Numeric Keys as Strings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, digit-only array keys are dumped as integers. You can use the +``DUMP_NUMERIC_KEY_AS_STRING`` flag if you want to dump string-only keys:: + + $dumped = Yaml::dump([200 => 'foo']); + // 200: foo + + $dumped = Yaml::dump([200 => 'foo'], 2, 4, Yaml::DUMP_NUMERIC_KEY_AS_STRING); + // '200': foo + Syntax Validation ~~~~~~~~~~~~~~~~~ @@ -427,10 +484,6 @@ Then, execute the script for validating contents: # you can also exclude one or more files from linting $ php lint.php path/to/directory --exclude=path/to/directory/foo.yaml --exclude=path/to/directory/bar.yaml -.. versionadded:: 5.4 - - The ``--exclude`` option was introduced in Symfony 5.4. - The result is written to STDOUT and uses a plain text format by default. Add the ``--format`` option to get the output in JSON format: @@ -446,3 +499,4 @@ Add the ``--format`` option to get the output in JSON format: .. _`YAML`: https://yaml.org/ .. _`ISO-8601`: https://www.iso.org/iso-8601-date-and-time-format.html +.. _`PHP enumerations`: https://www.php.net/manual/en/language.types.enumerations.php diff --git a/configuration.rst b/configuration.rst index 56bc30fcf4c..36dceae1b71 100644 --- a/configuration.rst +++ b/configuration.rst @@ -57,36 +57,6 @@ configure your applications, but lets you choose between YAML, XML and PHP. Throughout the Symfony documentation, all configuration examples will be shown in these three formats. -.. note:: - - By default, Symfony only loads the configuration files defined in YAML - format. If you define configuration in XML and/or PHP formats, update the - ``src/Kernel.php`` file:: - - // src/Kernel.php - use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; - use Symfony\Component\HttpKernel\Kernel as BaseKernel; - - class Kernel extends BaseKernel - { - // ... - - private function configureContainer(ContainerConfigurator $container): void - { - $configDir = $this->getConfigDir(); - - $container->import($configDir.'/{packages}/*.{yaml,php}'); - $container->import($configDir.'/{packages}/'.$this->environment.'/*.{yaml,php}'); - - if (is_file($configDir.'/services.yaml')) { - $container->import($configDir.'/services.yaml'); - $container->import($configDir.'/{services}_'.$this->environment.'.yaml'); - } else { - $container->import($configDir.'/{services}.php'); - } - } - } - There isn't any practical difference between formats. In fact, Symfony transforms all of them into PHP and caches them before running the application, so there's not even any performance difference. @@ -101,6 +71,16 @@ readable. These are the main advantages and disadvantages of each format: * **PHP**: very powerful and it allows you to create dynamic configuration with arrays or a :ref:`ConfigBuilder `. +.. note:: + + By default Symfony loads the configuration files defined in YAML and PHP + formats. If you define configuration in XML format, update the + :method:`Symfony\\Bundle\\FrameworkBundle\\Kernel\\MicroKernelTrait::configureContainer` + and/or + :method:`Symfony\\Bundle\\FrameworkBundle\\Kernel\\MicroKernelTrait::configureRoutes` + methods in the ``src/Kernel.php`` file to add support for the ``.xml`` file + extension. + Importing Configuration Files ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -156,7 +136,7 @@ configuration files, even if they use a different format: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->import('legacy_config.php'); // glob expressions are also supported to load multiple files @@ -206,6 +186,9 @@ reusable configuration value. By convention, parameters are defined under the app.some_constant: !php/const GLOBAL_CONSTANT app.another_constant: !php/const App\Entity\BlogPost::MAX_ITEMS + # Enum case as parameter values + app.some_enum: !php/enum App\Enum\PostState::Published + # ... .. code-block:: xml @@ -243,6 +226,9 @@ reusable configuration value. By convention, parameters are defined under the GLOBAL_CONSTANT App\Entity\BlogPost::MAX_ITEMS + + + App\Enum\PostState::Published @@ -254,8 +240,9 @@ reusable configuration value. By convention, parameters are defined under the namespace Symfony\Component\DependencyInjection\Loader\Configurator; use App\Entity\BlogPost; + use App\Enum\PostState; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->parameters() // the parameter name is an arbitrary string (the 'app.' prefix is recommended // to better differentiate your parameters from Symfony parameters). @@ -272,15 +259,18 @@ reusable configuration value. By convention, parameters are defined under the // PHP constants as parameter values ->set('app.some_constant', GLOBAL_CONSTANT) - ->set('app.another_constant', BlogPost::MAX_ITEMS); + ->set('app.another_constant', BlogPost::MAX_ITEMS) + + // Enum case as parameter values + ->set('app.some_enum', PostState::Published); }; // ... .. caution:: - When using XML configuration, the values between ```` tags are - not trimmed. This means that the value of the following parameter will be + By default and when using XML configuration, the values between ```` + tags are not trimmed. This means that the value of the following parameter will be ``'\n something@example.com\n'``: .. code-block:: xml @@ -289,6 +279,15 @@ reusable configuration value. By convention, parameters are defined under the something@example.com + If you want to trim the value of your parameter, use the ``trim`` attribute. + When using it, the value of the following parameter will be ``something@example.com``: + + .. code-block:: xml + + + something@example.com + + Once defined, you can reference this parameter value from any other configuration file using a special syntax: wrap the parameter name in two ``%`` (e.g. ``%app.admin_email%``): @@ -326,7 +325,7 @@ configuration file using a special syntax: wrap the parameter name in two ``%`` namespace Symfony\Component\DependencyInjection\Loader\Configurator; use function Symfony\Component\DependencyInjection\Loader\Configurator\param; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->extension('some_package', [ // when using the param() function, you only have to pass the parameter name... 'email_address' => param('app.admin_email'), @@ -365,7 +364,7 @@ configuration file using a special syntax: wrap the parameter name in two ``%`` // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->parameters() ->set('url_pattern', 'http://symfony.com/?foo=%%s&bar=%%d'); }; @@ -376,6 +375,13 @@ Configuration parameters are very common in Symfony applications. Some packages even define their own parameters (e.g. when installing the translation package, a new ``locale`` parameter is added to the ``config/services.yaml`` file). +.. tip:: + + By convention, parameters whose names start with a dot ``.`` (for example, + ``.mailer.transport``), are available only during the container compilation. + They are useful when working with :ref:`Compiler Passes ` + to declare some temporary parameters that won't be available later in the application. + .. seealso:: Later in this article you can read how to @@ -432,11 +438,6 @@ files directly in the ``config/packages/`` directory. .. tip:: - .. versionadded:: 5.3 - - The ability to defined different environments in a single file was - introduced in Symfony 5.3. - You can also define options for different environments in a single configuration file using the special ``when`` keyword: @@ -501,7 +502,7 @@ files directly in the ``config/packages/`` directory. use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Config\WebpackEncoreConfig; - return static function (WebpackEncoreConfig $webpackEncore, ContainerConfigurator $container) { + return static function (WebpackEncoreConfig $webpackEncore, ContainerConfigurator $container): void { $webpackEncore ->outputPath('%kernel.project_dir%/public/build') ->strictMode(true) @@ -637,19 +638,13 @@ This example shows how you could configure the application secret using an env v // config/packages/framework.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->extension('framework', [ // by convention the env var names are always uppercase 'secret' => '%env(APP_SECRET)%', ]); }; -.. versionadded:: 5.3 - - The ``env()`` configurator syntax was introduced in 5.3. - In ``PHP`` configuration files, it will allow to autocomplete methods based - on processors name (i.e. ``env('SOME_VAR')->default('foo')``). - .. note:: Your env vars can also be accessed via the PHP super globals ``$_ENV`` and @@ -863,7 +858,10 @@ the right situation: but the overrides only apply to one environment. *Real* environment variables always win over env vars created by any of the -``.env`` files. +``.env`` files. Note that this behavior depends on the +`variables_order `_ +configuration, which must contain an ``E`` to expose the ``$_ENV`` superglobal. +This is the default configuration in PHP. The ``.env`` and ``.env.`` files should be committed to the repository because they are the same for all developers and machines. However, @@ -883,7 +881,7 @@ If you need to override an environment variable defined by the system, use the use Symfony\Component\Dotenv\Dotenv; $dotenv = new Dotenv(); - $dotenv->loadEnv(__DIR__.'/.env', null, 'dev', ['test'], true); + $dotenv->loadEnv(__DIR__.'/.env', overrideExistingVars: true); // ... @@ -913,17 +911,6 @@ To improve performance, you can optionally run the ``dump-env`` Composer command 1.2 or later). The command is not registered by default, so you must register first in your services: - .. code-block:: yaml - - # config/services.yaml - services: - Symfony\Component\Dotenv\Command\DotenvDumpCommand: - - '%kernel.project_dir%/.env' - - '%kernel.environment%' - - In PHP >= 8, you can remove the two arguments when autoconfiguration is enabled - (which is the default): - .. code-block:: yaml # config/services.yaml @@ -935,16 +922,59 @@ To improve performance, you can optionally run the ``dump-env`` Composer command .. code-block:: terminal # parses ALL .env files and dumps their final values to .env.local.php - $ php bin/console dotenv:dump prod + $ APP_ENV=prod APP_DEBUG=0 php bin/console dotenv:dump After running this command, Symfony will load the ``.env.local.php`` file to get the environment variables and will not spend time parsing the ``.env`` files. .. tip:: - Update your deployment tools/workflow to run the ``dump-env`` command after + Update your deployment tools/workflow to run the ``dotenv:dump`` command after each deploy to improve the application performance. +Storing Environment Variables In Other Files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the environment variables are stored in the ``.env`` file located +at the root of your project. However, you can store them in other files in +multiple ways. + +If you use the :doc:`Runtime component `, the dotenv +path is part of the options you can set in your ``composer.json`` file: + +.. code-block:: json + + { + // ... + "extra": { + // ... + "runtime": { + "dotenv_path": "my/custom/path/to/.env" + } + } + } + +As an alternate option, you can directly invoke the ``Dotenv`` class in your +``bootstrap.php`` file or any other file of your application:: + + use Symfony\Component\Dotenv\Dotenv; + + (new Dotenv())->bootEnv(dirname(__DIR__).'my/custom/path/to/.env'); + +Symfony will then look for the environment variables in that file, but also in +the local and environment-specific files (e.g. ``.*.local`` and +``.*..local``). Read +:ref:`how to override environment variables ` +to learn more about this. + +If you need to know the path to the ``.env`` file that Symfony is using, you can +read the ``SYMFONY_DOTENV_PATH`` environment variable in your application. + +.. versionadded:: 7.1 + + The ``SYMFONY_DOTENV_PATH`` environment variable was introduced in Symfony + 7.1. + .. _configuration-secrets: Encrypting Environment Variables (Secrets) @@ -986,24 +1016,25 @@ Use the ``debug:dotenv`` command to understand how Symfony parses the different ALICE BOB BOB bob ---------- ------- ---------- ------ -.. versionadded:: 5.4 - - The ``debug:dotenv`` command was introduced in Symfony 5.4. + # look for a specific variable passing its full or partial name as an argument + $ php bin/console debug:dotenv foo Additionally, and regardless of how you set environment variables, you can see all -environment variables, with their values, referenced in Symfony's container configuration: +environment variables, with their values, referenced in Symfony's container configuration, +you can also see the number of occurrences of each environment variable in the container: .. code-block:: terminal $ php bin/console debug:container --env-vars - ---------------- ----------------- --------------------------------------------- - Name Default value Real value - ---------------- ----------------- --------------------------------------------- - APP_SECRET n/a "471a62e2d601a8952deb186e44186cb3" - FOO "[1, "2.5", 3]" n/a - BAR null n/a - ---------------- ----------------- --------------------------------------------- + ------------ ----------------- ------------------------------------ ------------- + Name Default value Real value Usage count + ------------ ----------------- ------------------------------------ ------------- + APP_SECRET n/a "471a62e2d601a8952deb186e44186cb3" 2 + BAR n/a n/a 1 + BAZ n/a "value" 0 + FOO "[1, "2.5", 3]" n/a 1 + ------------ ----------------- ------------------------------------ ------------- # you can also filter the list of env vars by name: $ php bin/console debug:container --env-vars foo @@ -1021,7 +1052,7 @@ implements :class:`Symfony\\Component\\DependencyInjection\\EnvVarLoaderInterfac .. note:: If you're using the :ref:`default services.yaml configuration `, - the autoconfiguration feature will enable and tag thise service automatically. + the autoconfiguration feature will enable and tag this service automatically. Otherwise, you need to register and :doc:`tag your service ` with the ``container.env_var_loader`` tag. @@ -1160,7 +1191,7 @@ doesn't work for parameters: use App\Service\MessageGenerator; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->parameters() ->set('app.contents_dir', '...'); @@ -1215,7 +1246,7 @@ whenever a service/controller defines a ``$projectDir`` argument, use this: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->services() ->defaults() // pass this value to any $projectDir argument for any service @@ -1244,14 +1275,12 @@ parameters at once by type-hinting any of its constructor arguments with the class MessageGenerator { - private $params; - - public function __construct(ContainerBagInterface $params) - { - $this->params = $params; + public function __construct( + private ContainerBagInterface $params, + ) { } - public function someMethod() + public function someMethod(): void { // get any container parameter from $this->params, which stores all of them $sender = $this->params->get('mailer_sender'); @@ -1264,10 +1293,6 @@ parameters at once by type-hinting any of its constructor arguments with the Using PHP ConfigBuilders ------------------------ -.. versionadded:: 5.3 - - The "ConfigBuilders" feature was introduced in Symfony 5.3. - Writing PHP config is sometimes difficult because you end up with large nested arrays and you have no autocompletion help from your favorite IDE. A way to address this is to use "ConfigBuilders". They are objects that will help you @@ -1281,18 +1306,18 @@ namespace ``Symfony\Config``:: // config/packages/security.php use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { $security->firewall('main') ->pattern('^/*') ->lazy(true) - ->anonymous(); + ->security(false); $security ->roleHierarchy('ROLE_ADMIN', ['ROLE_USER']) ->roleHierarchy('ROLE_SUPER_ADMIN', ['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH']) ->accessControl() ->path('^/user') - ->role('ROLE_USER'); + ->roles('ROLE_USER'); $security->accessControl(['path' => '^/admin', 'roles' => 'ROLE_ADMIN']); }; diff --git a/configuration/env_var_processors.rst b/configuration/env_var_processors.rst index 0a76793cc2c..baf4037d05a 100644 --- a/configuration/env_var_processors.rst +++ b/configuration/env_var_processors.rst @@ -45,7 +45,7 @@ processor to turn the value of the ``HTTP_PORT`` env var into an integer: use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->router() ->httpPort('%env(int:HTTP_PORT)%') // or @@ -53,12 +53,6 @@ processor to turn the value of the ``HTTP_PORT`` env var into an integer: ; }; -.. versionadded:: 5.3 - - The ``env()`` configurator syntax was introduced in 5.3. - In ``PHP`` configuration files, it will allow to autocomplete methods based - on processors name (i.e. ``env('SOME_VAR')->default('foo')``). - Built-In Environment Variable Processors ---------------------------------------- @@ -104,14 +98,15 @@ Symfony provides the following env var processors: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Config\FrameworkConfig; - return static function (ContainerBuilder $container, FrameworkConfig $framework) { + return static function (ContainerBuilder $container, FrameworkConfig $framework): void { $container->setParameter('env(SECRET)', 'some_secret'); $framework->secret(env('SECRET')->string()); }; ``env(bool:FOO)`` - Casts ``FOO`` to a bool (``true`` values are ``'true'``, ``'on'``, ``'yes'`` - and all numbers except ``0`` and ``0.0``; everything else is ``false``): + Casts ``FOO`` to a bool (``true`` values are ``'true'``, ``'on'``, ``'yes'``, + all numbers except ``0`` and ``0.0`` and all numeric strings except ``'0'`` + and ``'0.0'``; everything else is ``false``): .. configuration-block:: @@ -150,17 +145,12 @@ Symfony provides the following env var processors: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Config\FrameworkConfig; - return static function (ContainerBuilder $container, FrameworkConfig $framework) { + return static function (ContainerBuilder $container, FrameworkConfig $framework): void { $container->setParameter('env(HTTP_METHOD_OVERRIDE)', 'true'); $framework->httpMethodOverride(env('HTTP_METHOD_OVERRIDE')->bool()); }; ``env(not:FOO)`` - - .. versionadded:: 5.3 - - The ``not:`` env var processor was introduced in Symfony 5.3. - Casts ``FOO`` to a bool (just as ``env(bool:...)`` does) except it returns the inverted value (falsy values are returned as ``true``, truthy values are returned as ``false``): @@ -242,7 +232,7 @@ Symfony provides the following env var processors: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Config\SecurityConfig; - return static function (ContainerBuilder $container, SecurityConfig $security) { + return static function (ContainerBuilder $container, SecurityConfig $security): void { $container->setParameter('env(HEALTH_CHECK_METHOD)', 'Symfony\Component\HttpFoundation\Request::METHOD_HEAD'); $security->accessControl() ->path('^/health-check$') @@ -291,7 +281,7 @@ Symfony provides the following env var processors: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Config\FrameworkConfig; - return static function (ContainerBuilder $container) { + return static function (ContainerBuilder $container): void { $container->setParameter('env(ALLOWED_LANGUAGES)', '["en","de","es"]'); $container->setParameter('app_allowed_languages', '%env(json:ALLOWED_LANGUAGES)%'); }; @@ -375,11 +365,60 @@ Symfony provides the following env var processors: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Config\FrameworkConfig; - return static function (ContainerBuilder $container) { + return static function (ContainerBuilder $container): void { $container->setParameter('env(ALLOWED_LANGUAGES)', 'en,de,es'); $container->setParameter('app_allowed_languages', '%env(csv:ALLOWED_LANGUAGES)%'); }; +``env(shuffle:FOO)`` + Randomly shuffles values of the ``FOO`` env var, which must be an array. + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + parameters: + env(REDIS_NODES): "127.0.0.1:6380,127.0.0.1:6381" + services: + RedisCluster: + class: RedisCluster + arguments: [null, "%env(shuffle:csv:REDIS_NODES)%"] + + .. code-block:: xml + + + + + + + redis://127.0.0.1:6380,redis://127.0.0.1:6381 + + + + + null + %env(shuffle:csv:REDIS_NODES)% + + + + + .. code-block:: php + + // config/services.php + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + return static function (ContainerConfigurator $containerConfigurator): void { + $container = $containerConfigurator->services() + ->set(\RedisCluster::class, \RedisCluster::class)->args([null, '%env(shuffle:csv:REDIS_NODES)%']); + }; + ``env(file:FOO)`` Returns the contents of a file whose path is the value of the ``FOO`` env var: @@ -699,6 +738,137 @@ Symfony provides the following env var processors: ], ]); +``env(enum:FooEnum:BAR)`` + Tries to convert an environment variable to an actual ``\BackedEnum`` value. + This processor takes the fully qualified name of the ``\BackedEnum`` as an argument:: + + // App\Enum\Suit.php + enum Suit: string + { + case Clubs = 'clubs'; + case Spades = 'spades'; + case Diamonds = 'diamonds'; + case Hearts = 'hearts'; + } + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + suit: '%env(enum:App\Enum\Suit:CARD_SUIT)%' + + .. code-block:: xml + + + + + + + %env(enum:App\Enum\Suit:CARD_SUIT)% + + + + .. code-block:: php + + // config/services.php + $container->setParameter('suit', '%env(enum:App\Enum\Suit:CARD_SUIT)%'); + + The value stored in the ``CARD_SUIT`` env var would be a string (e.g. ``'spades'``) + but the application will use the enum value (e.g. ``Suit::Spades``). + +``env(defined:NO_FOO)`` + Evaluates to ``true`` if the env var exists and its value is not ``''`` + (an empty string) or ``null``; it returns ``false`` otherwise. + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + typed_env: '%env(defined:FOO)%' + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/services.php + $container->setParameter('typed_env', '%env(defined:FOO)%'); + +.. _urlencode_environment_variable_processor: + +``env(urlencode:FOO)`` + Encodes the content of the ``FOO`` env var using the :phpfunction:`urlencode` + PHP function. This is especially useful when ``FOO`` value is not compatible + with DSN syntax. + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + parameters: + env(DATABASE_URL): 'mysql://db_user:foo@b$r@127.0.0.1:3306/db_name' + encoded_database_url: '%env(urlencode:DATABASE_URL)%' + + .. code-block:: xml + + + + + + + mysql://db_user:foo@b$r@127.0.0.1:3306/db_name + %env(urlencode:DATABASE_URL)% + + + + .. code-block:: php + + // config/packages/framework.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container): void { + $container->setParameter('env(DATABASE_URL)', 'mysql://db_user:foo@b$r@127.0.0.1:3306/db_name'); + $container->setParameter('encoded_database_url', '%env(urlencode:DATABASE_URL)%'); + }; + + .. versionadded:: 7.1 + + The ``env(urlencode:...)`` env var processor was introduced in Symfony 7.1. + It is also possible to combine any number of processors: .. configuration-block:: @@ -761,14 +931,14 @@ create a class that implements class LowercasingEnvVarProcessor implements EnvVarProcessorInterface { - public function getEnv(string $prefix, string $name, \Closure $getEnv) + public function getEnv(string $prefix, string $name, \Closure $getEnv): string { $env = $getEnv($name); return strtolower($env); } - public static function getProvidedTypes() + public static function getProvidedTypes(): array { return [ 'lowercase' => 'string', diff --git a/configuration/front_controllers_and_kernel.rst b/configuration/front_controllers_and_kernel.rst index e5319a8b063..b55f66afc33 100644 --- a/configuration/front_controllers_and_kernel.rst +++ b/configuration/front_controllers_and_kernel.rst @@ -186,7 +186,7 @@ parameter used, for example, to turn Twig's debug mode on: // config/packages/twig.php use Symfony\Config\TwigConfig; - return static function (TwigConfig $twig) { + return static function (TwigConfig $twig): void { // ... $twig->debug('%kernel.debug%'); }; @@ -237,7 +237,7 @@ the directory of the environment you're using (most commonly ``dev/`` while developing and debugging). While it can vary, the ``var/cache/dev/`` directory includes the following: -``srcApp_KernelDevDebugContainer.php`` +``App_KernelDevDebugContainer.php`` The cached "service container" that represents the cached application configuration. diff --git a/configuration/micro_kernel_trait.rst b/configuration/micro_kernel_trait.rst index 4d7494e72f8..b67335514a1 100644 --- a/configuration/micro_kernel_trait.rst +++ b/configuration/micro_kernel_trait.rst @@ -20,55 +20,105 @@ via Composer: symfony/http-foundation symfony/routing \ symfony/dependency-injection symfony/framework-bundle -Next, create an ``index.php`` file that defines the kernel class and runs it:: +Next, create an ``index.php`` file that defines the kernel class and runs it: - // index.php - use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; - use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; - use Symfony\Component\HttpFoundation\JsonResponse; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpKernel\Kernel as BaseKernel; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; +.. configuration-block:: - require __DIR__.'/vendor/autoload.php'; + .. code-block:: php-attributes - class Kernel extends BaseKernel - { - use MicroKernelTrait; + // index.php + use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpFoundation\JsonResponse; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpKernel\Kernel as BaseKernel; + use Symfony\Component\Routing\Attribute\Route; - public function registerBundles(): array - { - return [ - new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), - ]; - } + require __DIR__.'/vendor/autoload.php'; - protected function configureContainer(ContainerConfigurator $container): void + class Kernel extends BaseKernel { - // PHP equivalent of config/packages/framework.yaml - $container->extension('framework', [ - 'secret' => 'S0ME_SECRET' - ]); - } + use MicroKernelTrait; - protected function configureRoutes(RoutingConfigurator $routes): void - { - $routes->add('random_number', '/random/{limit}')->controller([$this, 'randomNumber']); + public function registerBundles(): array + { + return [ + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + ]; + } + + protected function configureContainer(ContainerConfigurator $container): void + { + // PHP equivalent of config/packages/framework.yaml + $container->extension('framework', [ + 'secret' => 'S0ME_SECRET' + ]); + } + + #[Route('/random/{limit}', name: 'random_number')] + public function randomNumber(int $limit): JsonResponse + { + return new JsonResponse([ + 'number' => random_int(0, $limit), + ]); + } } - public function randomNumber(int $limit): JsonResponse + $kernel = new Kernel('dev', true); + $request = Request::createFromGlobals(); + $response = $kernel->handle($request); + $response->send(); + $kernel->terminate($request, $response); + + .. code-block:: php + + // index.php + use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpFoundation\JsonResponse; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpKernel\Kernel as BaseKernel; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + require __DIR__.'/vendor/autoload.php'; + + class Kernel extends BaseKernel { - return new JsonResponse([ - 'number' => random_int(0, $limit), - ]); + use MicroKernelTrait; + + public function registerBundles(): array + { + return [ + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + ]; + } + + protected function configureContainer(ContainerConfigurator $container): void + { + // PHP equivalent of config/packages/framework.yaml + $container->extension('framework', [ + 'secret' => 'S0ME_SECRET' + ]); + } + + protected function configureRoutes(RoutingConfigurator $routes): void + { + $routes->add('random_number', '/random/{limit}')->controller([$this, 'randomNumber']); + } + + public function randomNumber(int $limit): JsonResponse + { + return new JsonResponse([ + 'number' => random_int(0, $limit), + ]); + } } - } - $kernel = new Kernel('dev', true); - $request = Request::createFromGlobals(); - $response = $kernel->handle($request); - $response->send(); - $kernel->terminate($request, $response); + $kernel = new Kernel('dev', true); + $request = Request::createFromGlobals(); + $response = $kernel->handle($request); + $response->send(); + $kernel->terminate($request, $response); .. note:: @@ -118,12 +168,6 @@ be automatically registered as an extension. You can learn more about it in the dedicated section about :ref:`managing configuration with extensions `. -.. versionadded:: 5.2 - - The automatic registration of the kernel as an extension when implementing the - :class:`Symfony\\Component\\DependencyInjection\\Extension\\ExtensionInterface` - was introduced in Symfony 5.2. - It is also possible to implement the ``EventSubscriberInterface`` to handle events directly from the kernel, again it will be registered automatically:: @@ -158,7 +202,7 @@ Advanced Example: Twig, Annotations and the Web Debug Toolbar ------------------------------------------------------------- The purpose of the ``MicroKernelTrait`` is *not* to have a single-file application. -Instead, its goal to give you the power to choose your bundles and structure. +Instead, its goal is to give you the power to choose your bundles and structure. First, you'll probably want to put your PHP classes in an ``src/`` directory. Configure your ``composer.json`` file to load from there: @@ -178,14 +222,17 @@ your ``composer.json`` file to load from there: Then, run ``composer dump-autoload`` to dump your new autoload config. -Now, suppose you want to use Twig and load routes via annotations. Instead of -putting *everything* in ``index.php``, create a new ``src/Kernel.php`` to -hold the kernel. Now it looks like this:: +Now, suppose you want to define a custom configuration for your app, +use Twig and load routes via annotations. Instead of putting *everything* +in ``index.php``, create a new ``src/Kernel.php`` to hold the kernel. +Now it looks like this:: // src/Kernel.php namespace App; + use App\DependencyInjection\AppExtension; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; + use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\HttpKernel\Kernel as BaseKernel; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; @@ -201,13 +248,18 @@ hold the kernel. Now it looks like this:: new \Symfony\Bundle\TwigBundle\TwigBundle(), ]; - if ($this->getEnvironment() == 'dev') { + if ('dev' === $this->getEnvironment()) { $bundles[] = new \Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); } return $bundles; } + protected function build(ContainerBuilder $containerBuilder): void + { + $containerBuilder->registerExtension(new AppExtension()); + } + protected function configureContainer(ContainerConfigurator $container): void { $container->import(__DIR__.'/../config/framework.yaml'); @@ -236,8 +288,9 @@ hold the kernel. Now it looks like this:: $routes->import('@WebProfilerBundle/Resources/config/routing/profiler.xml')->prefix('/_profiler'); } - // load the annotation routes - $routes->import(__DIR__.'/Controller/', 'annotation'); + // load the routes defined as PHP attributes + // (use 'annotation' as the second argument if you define routes as annotations) + $routes->import(__DIR__.'/Controller/', 'attribute'); } // optional, to use the standard Symfony cache directory @@ -257,7 +310,36 @@ Before continuing, run this command to add support for the new dependencies: .. code-block:: terminal - $ composer require symfony/yaml symfony/twig-bundle symfony/web-profiler-bundle doctrine/annotations + $ composer require symfony/yaml symfony/twig-bundle symfony/web-profiler-bundle + +Next, create a new extension class that defines your app configuration and +add a service conditionally based on the ``foo`` value:: + + // src/DependencyInjection/AppExtension.php + namespace App\DependencyInjection; + + use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Extension\AbstractExtension; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + class AppExtension extends AbstractExtension + { + public function configure(DefinitionConfigurator $definition): void + { + $definition->rootNode() + ->children() + ->booleanNode('foo')->defaultTrue()->end() + ->end(); + } + + public function loadExtension(array $config, ContainerConfigurator $containerConfigurator, ContainerBuilder $containerBuilder): void + { + if ($config['foo']) { + $containerBuilder->register('foo_service', \stdClass::class); + } + } + } Unlike the previous kernel, this loads an external ``config/framework.yaml`` file, because the configuration started to get bigger: @@ -291,7 +373,7 @@ because the configuration started to get bigger: // config/framework.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework ->secret('SOME_SECRET') ->profiler() @@ -299,7 +381,7 @@ because the configuration started to get bigger: ; }; -This also loads annotation routes from an ``src/Controller/`` directory, which +This also loads attribute routes from an ``src/Controller/`` directory, which has one file in it:: // src/Controller/MicroController.php @@ -307,13 +389,11 @@ has one file in it:: use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; class MicroController extends AbstractController { - /** - * @Route("/random/{limit}") - */ + #[Route('/random/{limit}')] public function randomNumber(int $limit): Response { $number = random_int(0, $limit); diff --git a/configuration/multiple_kernels.rst b/configuration/multiple_kernels.rst index 2ecee747e38..512ea57f24d 100644 --- a/configuration/multiple_kernels.rst +++ b/configuration/multiple_kernels.rst @@ -117,7 +117,9 @@ resources:: // src/Kernel.php namespace Shared; + use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Kernel as BaseKernel; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; class Kernel extends BaseKernel @@ -206,7 +208,7 @@ resources:: } if (false !== ($fileName = (new \ReflectionObject($this))->getFileName())) { - $routes->import($fileName, 'annotation'); + $routes->import($fileName, 'attribute'); } } } @@ -245,7 +247,7 @@ application:: use Shared\Kernel; // ... - return function (array $context) { + return function (array $context): Kernel { return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG'], $context['APP_ID']); }; @@ -258,10 +260,11 @@ the application ID to run under CLI context:: // bin/console use Shared\Kernel; + use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; - return function (InputInterface $input, array $context) { + return function (InputInterface $input, array $context): Application { $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG'], $input->getParameterOption(['--id', '-i'], $context['APP_ID'])); $application = new Application($kernel); diff --git a/configuration/override_dir_structure.rst b/configuration/override_dir_structure.rst index 41bf46d0e66..d17b67aedba 100644 --- a/configuration/override_dir_structure.rst +++ b/configuration/override_dir_structure.rst @@ -189,7 +189,7 @@ for multiple directories): // config/packages/twig.php use Symfony\Config\TwigConfig; - return static function (TwigConfig $twig) { + return static function (TwigConfig $twig): void { $twig->defaultPath('%kernel.project_dir%/resources/views'); }; @@ -235,7 +235,7 @@ configuration option to define your own translations directory (use :ref:`framew // config/packages/translation.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->translator() ->defaultPath('%kernel.project_dir%/i18n') ; diff --git a/configuration/secrets.rst b/configuration/secrets.rst index 863f575287d..f717456a22c 100644 --- a/configuration/secrets.rst +++ b/configuration/secrets.rst @@ -11,10 +11,7 @@ store them by using Symfony's secrets management system - sometimes called a .. note:: - The Secrets system requires the sodium PHP extension that is bundled - with PHP 7.2. If you're using an earlier PHP version, you can - install the `libsodium`_ PHP extension or use the - `paragonie/sodium_compat`_ package. + The Secrets system requires the Sodium PHP extension. .. _secrets-generate-keys: @@ -142,7 +139,7 @@ If you stored a ``DATABASE_PASSWORD`` secret, you can reference it by: // config/packages/doctrine.php use Symfony\Config\DoctrineConfig; - return static function (DoctrineConfig $doctrine) { + return static function (DoctrineConfig $doctrine): void { $doctrine->dbal() ->connection('default') ->password(env('DATABASE_PASSWORD')) @@ -169,6 +166,22 @@ secrets' values by passing the ``--reveal`` option: DATABASE_PASSWORD "my secret" ------------------- ------------ ------------- +Reveal Existing Secrets +----------------------- + +If you have the **decryption key**, the ``secrets:reveal`` command allows +you to reveal a single secret's value. + +.. code-block:: terminal + + $ php bin/console secrets:reveal DATABASE_PASSWORD + + my secret + +.. versionadded:: 7.1 + + The ``secrets:reveal`` command was introduced in Symfony 7.1. + Remove Secrets -------------- @@ -312,13 +325,10 @@ The secrets system is enabled by default and some of its behavior can be configu // config/packages/framework.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->secrets() // ->vaultDirectory('%kernel.project_dir%/config/secrets/%kernel.environment%') // ->localDotenvFile('%kernel.project_dir%/.env.%kernel.environment%.local') // ->decryptionEnvVar('base64:default::SYMFONY_DECRYPTION_SECRET') ; }; - -.. _`libsodium`: https://pecl.php.net/package/libsodium -.. _`paragonie/sodium_compat`: https://github.com/paragonie/sodium_compat diff --git a/configuration/using_parameters_in_dic.rst b/configuration/using_parameters_in_dic.rst index 05008114e01..3cac5d5049c 100644 --- a/configuration/using_parameters_in_dic.rst +++ b/configuration/using_parameters_in_dic.rst @@ -101,14 +101,13 @@ be injected with this parameter via the extension as follows:: class Configuration implements ConfigurationInterface { - private $debug; + private bool $debug; - public function __construct($debug) + public function __construct(private bool $debug) { - $this->debug = (bool) $debug; } - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('my_bundle'); @@ -135,7 +134,7 @@ And set it in the constructor of ``Configuration`` via the ``Extension`` class:: { // ... - public function getConfiguration(array $config, ContainerBuilder $container) + public function getConfiguration(array $config, ContainerBuilder $container): Configuration { return new Configuration($container->getParameter('kernel.debug')); } diff --git a/console.rst b/console.rst index 60d53d0c056..57f322c983d 100644 --- a/console.rst +++ b/console.rst @@ -67,31 +67,25 @@ command, for instance: Console Completion ~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.4 - - Console completion for Bash was introduced in Symfony 5.4. - -If you are using the Bash shell, you can install Symfony's completion -script to get auto completion when typing commands in the terminal. All -commands support name and option completion, and some can even complete -values. +If you are using the Bash, Zsh or Fish shell, you can install Symfony's +completion script to get auto completion when typing commands in the +terminal. All commands support name and option completion, and some can +even complete values. .. image:: /_images/components/console/completion.gif :alt: The terminal completes the command name "secrets:remove" and the argument "SOME_OTHER_SECRET". -First, make sure you installed and setup the "bash completion" package for -your OS (typically named ``bash-completion``). Then, install the Symfony -completion bash script *once* by running the ``completion`` command in a -Symfony app installed on your computer: +First, you have to install the completion script *once*. Run +``bin/console completion --help`` for the installation instructions for +your shell. -.. code-block:: terminal +.. note:: - $ php bin/console completion bash | sudo tee /etc/bash_completion.d/console-events-terminate - # after the installation, restart the shell + When using Bash, make sure you installed and setup the "bash completion" + package for your OS (typically named ``bash-completion``). -Now you are all set to use the auto completion for all Symfony Console -applications on your computer. By default, you can get a list of complete -options by pressing the Tab key. +After installing and restarting your terminal, you're all set to use +completion (by default, by pressing the Tab key). .. tip:: @@ -101,7 +95,8 @@ options by pressing the Tab key. .. code-block:: terminal - $ php vendor/bin/phpstan completion bash | sudo tee /etc/bash_completion.d/phpstan + $ php vendor/bin/phpstan completion --help + $ composer completion --help .. tip:: @@ -122,15 +117,15 @@ want a command to create a user:: // src/Command/CreateUserCommand.php namespace App\Command; + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; + // the name of the command is what users type after "php bin/console" + #[AsCommand(name: 'app:create-user')] class CreateUserCommand extends Command { - // the name of the command (the part after "bin/console") - protected static $defaultName = 'app:create-user'; - protected function execute(InputInterface $input, OutputInterface $output): int { // ... put here the code to create the user @@ -152,15 +147,6 @@ want a command to create a user:: } } -.. versionadded:: 5.1 - - The ``Command::SUCCESS`` and ``Command::FAILURE`` constants were introduced - in Symfony 5.1. - -.. versionadded:: 5.3 - - The ``Command::INVALID`` constant was introduced in Symfony 5.3 - Configuring the Command ~~~~~~~~~~~~~~~~~~~~~~~ @@ -173,13 +159,12 @@ You can optionally define a description, help message and the // ... class CreateUserCommand extends Command { - // the command description shown when running "php bin/console list" - protected static $defaultDescription = 'Creates a new user.'; - // ... protected function configure(): void { $this + // the command description shown when running "php bin/console list" + ->setDescription('Creates a new user.') // the command help shown when running the command with the "--help" option ->setHelp('This command allows you to create a user...') ; @@ -188,20 +173,16 @@ You can optionally define a description, help message and the .. tip:: - Defining the ``$defaultDescription`` static property instead of using the - ``setDescription()`` method allows to get the command description without + Using the ``#[AsCommand]`` attribute to define a description instead of + using the ``setDescription()`` method allows to get the command description without instantiating its class. This makes the ``php bin/console list`` command run much faster. If you want to always run the ``list`` command fast, add the ``--short`` option to it (``php bin/console list --short``). This will avoid instantiating command classes, but it won't show any description for commands that use the - ``setDescription()`` method instead of the static property. - -.. versionadded:: 5.3 - - The ``$defaultDescription`` static property and the ``--short`` option - were introduced in Symfony 5.3. + ``setDescription()`` method instead of the attribute to define the command + description. The ``configure()`` method is called automatically at the end of the command constructor. If your command defines its own constructor, set the properties @@ -240,8 +221,7 @@ available in the ``configure()`` method:: Registering the Command ~~~~~~~~~~~~~~~~~~~~~~~ -In PHP 8 and newer versions, you can register the command by adding the -``AsCommand`` attribute to it:: +You can register the command by adding the ``AsCommand`` attribute to it:: // src/Command/CreateUserCommand.php namespace App\Command; @@ -249,8 +229,6 @@ In PHP 8 and newer versions, you can register the command by adding the use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; - // the "name" and "description" arguments of AsCommand replace the - // static $defaultName and $defaultDescription properties #[AsCommand( name: 'app:create-user', description: 'Creates a new user.', @@ -262,11 +240,6 @@ In PHP 8 and newer versions, you can register the command by adding the // ... } -.. versionadded:: 5.3 - - The ability to use PHP attributes to configure commands was introduced in - Symfony 5.3. - If you can't use PHP attributes, register the command as a service and :doc:`tag it ` with the ``console.command`` tag. If you're using the :ref:`default services.yaml configuration `, @@ -374,6 +347,12 @@ method, which returns an instance of sleep(1); // Output is now completely empty! + // setting the max height of a section will make new lines replace the old ones + $section1->setMaxHeight(2); + $section1->writeln('Line1'); + $section1->writeln('Line2'); + $section1->writeln('Line3'); + return Command::SUCCESS; } } @@ -453,12 +432,9 @@ as a service, you can use normal dependency injection. Imagine you have a class CreateUserCommand extends Command { - private $userManager; - - public function __construct(UserManager $userManager) - { - $this->userManager = $userManager; - + public function __construct( + private UserManager $userManager, + ){ parent::__construct(); } @@ -520,7 +496,7 @@ console:: class CreateUserCommandTest extends KernelTestCase { - public function testExecute() + public function testExecute(): void { self::bootKernel(); $application = new Application(self::$kernel); @@ -550,15 +526,6 @@ console:: If you are using a :doc:`single-command application `, call ``setAutoExit(false)`` on it to get the command result in ``CommandTester``. -.. versionadded:: 5.2 - - The ``setAutoExit()`` method for single-command applications was introduced - in Symfony 5.2. - -.. versionadded:: 5.4 - - The ``assertCommandIsSuccessful()`` method was introduced in Symfony 5.4. - .. tip:: You can also test a whole console application by using @@ -582,11 +549,11 @@ call ``setAutoExit(false)`` on it to get the command result in ``CommandTester`` .. caution:: - When testing ``InputOption::VALUE_NONE`` command options, you must pass an - empty value to them:: + When testing ``InputOption::VALUE_NONE`` command options, you must pass ``true`` + to them:: $commandTester = new CommandTester($command); - $commandTester->execute(['--some-option' => '']); + $commandTester->execute(['--some-option' => true]); .. note:: @@ -595,8 +562,8 @@ call ``setAutoExit(false)`` on it to get the command result in ``CommandTester`` and extend the normal ``\PHPUnit\Framework\TestCase``. When testing your commands, it could be useful to understand how your command -reacts on different settings like the width and the height of the terminal. -You have access to such information thanks to the +reacts on different settings like the width and the height of the terminal, or +even the color mode being used. You have access to such information thanks to the :class:`Symfony\\Component\\Console\\Terminal` class:: use Symfony\Component\Console\Terminal; @@ -609,6 +576,12 @@ You have access to such information thanks to the // gets the number of columns available $width = $terminal->getWidth(); + // gets the color mode + $colorMode = $terminal->getColorMode(); + + // changes the color mode + $colorMode = $terminal->setColorMode(AnsiColorMode::Ansi24); + Logging Command Errors ---------------------- @@ -624,6 +597,35 @@ Using Events And Handling Signals When a command is running, many events are dispatched, one of them allows to react to signals, read more in :doc:`this section `. +Profiling Commands +------------------ + +Symfony allows to profile the execution of any command, including yours. First, +make sure that the :ref:`debug mode ` and the :doc:`profiler ` +are enabled. Then, add the ``--profile`` option when running the command: + +.. code-block:: terminal + + $ php bin/console --profile app:my-command + +Symfony will now collect data about the command execution, which is helpful to +debug errors or check other issues. When the command execution is over, the +profile is accessible through the web page of the profiler. + +.. tip:: + + If you run the command in verbose mode (adding the ``-v`` option), Symfony + will display in the output a clickable link to the command profile (if your + terminal supports links). If you run it in debug verbosity (``-vvv``) you'll + also see the time and memory consumed by the command. + +.. caution:: + + When profiling the ``messenger:consume`` command from the :doc:`Messenger ` + component, add the ``--no-reset`` option to the command or you won't get any + profile. Moreover, consider using the ``--limit`` option to only process a few + messages to make the profile more readable in the profiler. + Learn More ---------- diff --git a/console/calling_commands.rst b/console/calling_commands.rst index 35d388965ad..c5bfc6e5a72 100644 --- a/console/calling_commands.rst +++ b/console/calling_commands.rst @@ -66,6 +66,6 @@ method):: .. note:: - Most of the times, calling a command from code that is not executed on the + Most of the time, calling a command from code that is not executed on the command line is not a good idea. The main reason is that the command's output is optimized for the console and not to be passed to other commands. diff --git a/console/coloring.rst b/console/coloring.rst index 316665a0391..8b6655d6b71 100644 --- a/console/coloring.rst +++ b/console/coloring.rst @@ -1,8 +1,10 @@ How to Color and Style the Console Output ========================================= -By using colors in the command output, you can distinguish different types of -output (e.g. important messages, titles, comments, etc.). +Symfony provides an optional :doc:`console style ` to render the +input and output of commands in a consistent way. If you prefer to apply your +own style, use the utilities explained in this article to show colors in the command +output (e.g. to differentiate between important messages, titles, comments, etc.). .. note:: @@ -50,18 +52,11 @@ Any hex color is supported for foreground and background colors. Besides that, t ``gray``, ``bright-red``, ``bright-green``, ``bright-yellow``, ``bright-blue``, ``bright-magenta``, ``bright-cyan`` and ``bright-white``. -.. versionadded:: 5.2 - - True (hex) color support was introduced in Symfony 5.2 - -.. versionadded:: 5.3 - - Support for bright colors was introduced in Symfony 5.3. - .. note:: - If the terminal doesn't support true colors, the nearest named color is used. - E.g. ``#c0392b`` is degraded to ``red`` or ``#f1c40f`` is degraded to ``yellow``. + If the terminal doesn't support true colors, the given color is replaced by + the nearest color depending on the terminal capabilities. E.g. ``#c0392b`` is + degraded to ``#d75f5f`` in 256-color terminals and to ``red`` in 8-color terminals. And available options are: ``bold``, ``underscore``, ``blink``, ``reverse`` (enables the "reverse video" mode where the background and foreground colors diff --git a/console/commands_as_services.rst b/console/commands_as_services.rst index 9b57560e42c..75aa13d5be8 100644 --- a/console/commands_as_services.rst +++ b/console/commands_as_services.rst @@ -15,19 +15,17 @@ For example, suppose you want to log something from within your command:: namespace App\Command; use Psr\Log\LoggerInterface; + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; + #[AsCommand(name: 'app:sunshine')] class SunshineCommand extends Command { - protected static $defaultName = 'app:sunshine'; - private $logger; - - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; - + public function __construct( + private LoggerInterface $logger, + ) { // you *must* call the parent constructor parent::__construct(); } @@ -65,12 +63,15 @@ command and start logging. Lazy Loading ------------ -To make your command lazily loaded, either define its ``$defaultName`` static property:: +To make your command lazily loaded, either define its name using the PHP +``AsCommand`` attribute:: + use Symfony\Component\Console\Attribute\AsCommand; + // ... + + #[AsCommand(name: 'app:sunshine')] class SunshineCommand extends Command { - protected static $defaultName = 'app:sunshine'; - // ... } @@ -132,3 +133,5 @@ only when the ``app:sunshine`` command is actually called. .. caution:: Calling the ``list`` command will instantiate all commands, including lazy commands. + However, if the command is a ``Symfony\Component\Console\Command\LazyCommand``, then + the underlying command factory will not be executed. diff --git a/console/hide_commands.rst b/console/hide_commands.rst index 2f9d2819873..44a69d09289 100644 --- a/console/hide_commands.rst +++ b/console/hide_commands.rst @@ -8,25 +8,19 @@ However, sometimes commands are not intended to be run by end-users; for example, commands for the legacy parts of the application, commands exclusively run through scheduled tasks, etc. -In those cases, you can define the command as **hidden** by setting the -``setHidden()`` method to ``true`` in the command configuration:: +In those cases, you can define the command as **hidden** by setting to ``true`` +the ``hidden`` property of the ``AsCommand`` attribute:: // src/Command/LegacyCommand.php namespace App\Command; + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; + #[AsCommand(name: 'app:legacy', hidden: true)] class LegacyCommand extends Command { - protected static $defaultName = 'app:legacy'; - - protected function configure(): void - { - $this - ->setHidden(true) - // ... - ; - } + // ... } Hidden commands behave the same as normal commands but they are no longer displayed diff --git a/console/input.rst b/console/input.rst index 3abf3a37b9b..6e7fc85a055 100644 --- a/console/input.rst +++ b/console/input.rst @@ -228,10 +228,6 @@ There are five option variants you can use: Accept either the flag (e.g. ``--yell``) or its negation (e.g. ``--no-yell``). -.. versionadded:: 5.3 - - The ``InputOption::VALUE_NEGATABLE`` constant was introduced in Symfony 5.3. - You need to combine ``VALUE_IS_ARRAY`` with ``VALUE_REQUIRED`` or ``VALUE_OPTIONAL`` like this:: @@ -315,12 +311,44 @@ The above code can be simplified as follows because ``false !== null``:: $yell = ($optionValue !== false); $yellLouder = ($optionValue === 'louder'); -Adding Argument/Option Value Completion ---------------------------------------- +Fetching The Raw Command Input +------------------------------ + +Symfony provides a :method:`Symfony\\Component\\Console\\Input\\ArgvInput::getRawTokens` +method to fetch the raw input that was passed to the command. This is useful if +you want to parse the input yourself or when you need to pass the input to another +command without having to worry about the number of arguments or options:: + + // ... + use Symfony\Component\Process\Process; + + protected function execute(InputInterface $input, OutputInterface $output): int + { + // if this command was run as: + // php bin/console app:my-command foo --bar --baz=3 --qux=value1 --qux=value2 + + $tokens = $input->getRawTokens(); + // $tokens = ['app:my-command', 'foo', '--bar', '--baz=3', '--qux=value1', '--qux=value2']; + + // pass true as argument to not include the original command name + $tokens = $input->getRawTokens(true); + // $tokens = ['foo', '--bar', '--baz=3', '--qux=value1', '--qux=value2']; + + // pass the raw input to any other command (from Symfony or the operating system) + $process = new Process(['app:other-command', ...$input->getRawTokens(true)]); + $process->setTty(true); + $process->mustRun(); + + // ... + } -.. versionadded:: 5.4 +.. versionadded:: 7.1 - Console completion was introduced in Symfony 5.4. + The :method:`Symfony\\Component\\Console\\Input\\ArgvInput::getRawTokens` + method was introduced in Symfony 7.1. + +Adding Argument/Option Value Completion +--------------------------------------- If :ref:`Console completion is installed `, command and option names will be auto completed by the shell. However, you @@ -328,7 +356,7 @@ can also implement value completion for the input in your commands. For instance, you may want to complete all usernames from the database in the ``name`` argument of your greet command. -To achieve this, override the ``complete()`` method in the command:: +To achieve this, use the 5th argument of ``addArgument()``/``addOption``:: // ... use Symfony\Component\Console\Completion\CompletionInput; @@ -337,23 +365,28 @@ To achieve this, override the ``complete()`` method in the command:: class GreetCommand extends Command { // ... - - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + protected function configure(): void { - if ($input->mustSuggestArgumentValuesFor('names')) { - // the user asks for completion input for the "names" option - - // the value the user already typed, e.g. when typing "app:greet Fa" before - // pressing Tab, this will contain "Fa" - $currentValue = $input->getCompletionValue(); - - // get the list of username names from somewhere (e.g. the database) - // you may use $currentValue to filter down the names - $availableUsernames = ...; - - // then add the retrieved names as suggested values - $suggestions->suggestValues($availableUsernames); - } + $this + ->addArgument( + 'names', + InputArgument::IS_ARRAY, + 'Who do you want to greet (separate multiple names with a space)?', + null, + function (CompletionInput $input): array { + // the value the user already typed, e.g. when typing "app:greet Fa" before + // pressing Tab, this will contain "Fa" + $currentValue = $input->getCompletionValue(); + + // get the list of username names from somewhere (e.g. the database) + // you may use $currentValue to filter down the names + $availableUsernames = ...; + + // then suggested the usernames as values + return $availableUsernames; + } + ) + ; } } @@ -362,7 +395,7 @@ tab after typing ``app:greet Fa`` will give you these names as a suggestion. .. tip:: - The bash shell is able to handle huge amounts of suggestions and will + The shell script is able to handle huge amounts of suggestions and will automatically filter the suggested values based on the existing input from the user. You do not have to implement any filter logic in the command. @@ -383,7 +416,7 @@ to help you unit test the completion logic:: class GreetCommandTest extends TestCase { - public function testComplete() + public function testComplete(): void { $application = new Application(); $application->add(new GreetCommand()); diff --git a/console/lazy_commands.rst b/console/lazy_commands.rst index 6d1f245eb75..487ef32955f 100644 --- a/console/lazy_commands.rst +++ b/console/lazy_commands.rst @@ -10,15 +10,25 @@ The traditional way of adding commands to your application is to use :method:`Symfony\\Component\\Console\\Application::add`, which expects a ``Command`` instance as an argument. +This approach can have downsides as some commands might be expensive to +instantiate in which case you may want to lazy-load them. Note however that lazy-loading +is not absolute. Indeed a few commands such as ``list``, ``help`` or ``_complete`` can +require to instantiate other commands although they are lazy. For example ``list`` needs +to get the name and description of all commands, which might require the command to be +instantiated to get. + In order to lazy-load commands, you need to register an intermediate loader which will be responsible for returning ``Command`` instances:: use App\Command\HeavyCommand; use Symfony\Component\Console\Application; + use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\CommandLoader\FactoryCommandLoader; $commandLoader = new FactoryCommandLoader([ - 'app:heavy' => function () { return new HeavyCommand(); }, + // Note that the `list` command will still instantiate that command + // in this example. + 'app:heavy' => static fn(): Command => new HeavyCommand(), ]); $application = new Application(); @@ -35,6 +45,28 @@ method accepts any :class:`Symfony\\Component\\Console\\CommandLoader\\CommandLoaderInterface` instance so you can use your own implementation. +Another way to do so is to take advantage of ``Symfony\Component\Console\Command\LazyCommand``:: + + use App\Command\HeavyCommand; + use Symfony\Component\Console\Application; + use Symfony\Component\Console\Command\Command; + use Symfony\Component\Console\CommandLoader\FactoryCommandLoader; + + // In this case although the command is instantiated, the underlying command factory + // will not be executed unless the command is actually executed or one tries to access + // its input definition to know its argument or option inputs. + $lazyCommand = new LazyCommand( + 'app:heavy', + [], + 'This is another more complete form of lazy command.', + false, + static fn (): Command => new HeavyCommand(), + ); + + $application = new Application(); + $application->add($lazyCommand); + $application->run(); + Built-in Command Loaders ------------------------ @@ -45,10 +77,11 @@ The :class:`Symfony\\Component\\Console\\CommandLoader\\FactoryCommandLoader` class provides a way of getting commands lazily loaded as it takes an array of ``Command`` factories as its only constructor argument:: + use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\CommandLoader\FactoryCommandLoader; $commandLoader = new FactoryCommandLoader([ - 'app:foo' => function () { return new FooCommand(); }, + 'app:foo' => function (): Command { return new FooCommand(); }, 'app:bar' => [BarCommand::class, 'create'], ]); diff --git a/console/lockable_trait.rst b/console/lockable_trait.rst index e3c26372cfe..0f4a4900e17 100644 --- a/console/lockable_trait.rst +++ b/console/lockable_trait.rst @@ -43,8 +43,28 @@ that adds two convenient methods to lock and release commands:: } } -.. versionadded:: 5.1 +The LockableTrait will use the ``SemaphoreStore`` if available and will default +to ``FlockStore`` otherwise. You can override this behavior by setting +a ``$lockFactory`` property with your own lock factory:: - The ``Command::SUCCESS`` constant was introduced in Symfony 5.1. + // ... + use Symfony\Component\Console\Command\Command; + use Symfony\Component\Console\Command\LockableTrait; + use Symfony\Component\Lock\LockFactory; + + class UpdateContentsCommand extends Command + { + use LockableTrait; + + public function __construct(private LockFactory $lockFactory) + { + } + + // ... + } + +.. versionadded:: 7.1 + + The ``$lockFactory`` property was introduced in Symfony 7.1. .. _`locks`: https://en.wikipedia.org/wiki/Lock_(computer_science) diff --git a/console/style.rst b/console/style.rst index 98ab5d66b38..0aaaa3f675e 100644 --- a/console/style.rst +++ b/console/style.rst @@ -169,10 +169,6 @@ Content Methods styled according to the Symfony Style Guide, which allows you to use features such as dynamically appending rows. -.. versionadded:: 5.4 - - The ``createTable()`` method was introduced in Symfony 5.4. - :method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::newLine` It displays a blank line in the command output. Although it may seem useful, most of the times you won't need it at all. The reason is that every helper @@ -263,10 +259,6 @@ Progress Bar Methods // ... do some work } -.. versionadded:: 5.4 - - The ``progressIterate`` method was introduced in Symfony 5.4. - :method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::createProgressBar` Creates an instance of :class:`Symfony\\Component\\Console\\Helper\\ProgressBar` styled according to the Symfony Style Guide. @@ -289,7 +281,7 @@ User Input Methods In case you need to validate the given value, pass a callback validator as the third argument:: - $io->ask('Number of workers to start', '1', function ($number) { + $io->ask('Number of workers to start', '1', function (string $number): int { if (!is_numeric($number)) { throw new \RuntimeException('You must type a number.'); } @@ -306,7 +298,7 @@ User Input Methods In case you need to validate the given value, pass a callback validator as the second argument:: - $io->askHidden('What is your password?', function ($password) { + $io->askHidden('What is your password?', function (string $password): string { if (empty($password)) { throw new \RuntimeException('Password cannot be empty.'); } @@ -335,11 +327,24 @@ User Input Methods $io->choice('Select the queue to analyze', ['queue1', 'queue2', 'queue3'], 'queue1'); + Finally, you can allow users to select multiple choices. To do so, users must + separate each choice with a comma (e.g. typing ``1, 2`` will select choice 1 + and 2):: + + $io->choice('Select the queue to analyze', ['queue1', 'queue2', 'queue3'], multiSelect: true); + .. _symfony-style-blocks: Result Methods ~~~~~~~~~~~~~~ +.. note:: + + If you print any URL it won't be broken/cut, it will be clickable - if the terminal provides it. If the "well + formatted output" is more important, you can switch it off:: + + $io->getOutputWrapper()->setAllowCutUrls(true); + :method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::success` It displays the given string or array of strings highlighted as a successful message (with a green background and the ``[OK]`` label). It's meant to be @@ -374,10 +379,6 @@ Result Methods 'Consectetur adipiscing elit', ]); -.. versionadded:: 5.2 - - The ``info()`` method was introduced in Symfony 5.2. - :method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::warning` It displays the given string or array of strings highlighted as a warning message (with a red background and the ``[WARNING]`` label). It's meant to be @@ -412,6 +413,34 @@ Result Methods 'Consectetur adipiscing elit', ]); +Configuring the Default Styles +------------------------------ + +By default, Symfony Styles wrap all contents to avoid having lines of text that +are too long. The only exception is URLs, which are not wrapped, no matter how +long they are. This is done to enable clickable URLs in terminals that support them. + +If you prefer to wrap all contents, including URLs, use this method:: + + // src/Command/GreetCommand.php + namespace App\Command; + + // ... + use Symfony\Component\Console\Style\SymfonyStyle; + + class GreetCommand extends Command + { + // ... + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $io->getOutputWrapper()->setAllowCutUrls(true); + + // ... + } + } + Defining your Own Styles ------------------------ diff --git a/contributing/code/bc.rst b/contributing/code/bc.rst index 3a4f16c5208..889a605b422 100644 --- a/contributing/code/bc.rst +++ b/contributing/code/bc.rst @@ -12,11 +12,6 @@ that release branch (5.x in the previous example). We also provide deprecation message triggered in the code base to help you with the migration process across major releases. -.. caution:: - - This promise was introduced with Symfony 2.3 and does not apply to previous - versions of Symfony. - However, backward compatibility comes in many different flavors. In fact, almost every change that we make to the framework can potentially break an application. For example, if we add a new method to a class, this will break an application diff --git a/contributing/code/pull_requests.rst b/contributing/code/pull_requests.rst index e9e8470bb96..7c9ab2579a5 100644 --- a/contributing/code/pull_requests.rst +++ b/contributing/code/pull_requests.rst @@ -31,7 +31,7 @@ Before working on Symfony, setup a friendly environment with the following software: * Git; -* PHP version 7.2.5 or above. +* PHP version 8.2 or above. Configure Git ~~~~~~~~~~~~~ @@ -147,6 +147,12 @@ work: for the ``5.4`` branch, the PR will also be applied by the core team on all the ``6.x`` branches that are still maintained. +During the :ref:`stabilization phase `, the development branch is in +feature freeze. Please help the community prepare for the new version release. If you want to submit a +new feature pull request, you should target the next version. For example, if ``6.3`` reached feature +freeze, new features should target ``6.4``. If the ``6.4`` branch does not yet exist, target ``6.3`` +and rebase your pull requests once the branch is created. + Create a Topic Branch ~~~~~~~~~~~~~~~~~~~~~ @@ -155,7 +161,7 @@ topic branch: .. code-block:: terminal - $ git checkout -b BRANCH_NAME 5.x + $ git checkout -b BRANCH_NAME 6.1 Or, if you want to provide a bug fix for the ``5.4`` branch, first track the remote ``5.4`` branch locally: diff --git a/contributing/code/standards.rst b/contributing/code/standards.rst index 2668269dfcc..39d96d9e247 100644 --- a/contributing/code/standards.rst +++ b/contributing/code/standards.rst @@ -49,19 +49,16 @@ short example containing most features described below:: { public const SOME_CONST = 42; - /** - * @var string - */ - private $fooBar; - private $qux; + private string $fooBar; /** * @param $dummy some argument description */ - public function __construct(string $dummy, Qux $qux) - { + public function __construct( + string $dummy, + private Qux $qux, + ) { $this->fooBar = $this->transformText($dummy); - $this->qux = $qux; } /** @@ -114,7 +111,7 @@ short example containing most features described below:: /** * Performs some basic operations for a given value. */ - private function performOperations(mixed $value = null, bool $theSwitch = false) + private function performOperations(mixed $value = null, bool $theSwitch = false): void { if (!$theSwitch) { return; diff --git a/contributing/code/tests.rst b/contributing/code/tests.rst index 8bffc4aa4bc..08f6bc5df12 100644 --- a/contributing/code/tests.rst +++ b/contributing/code/tests.rst @@ -32,7 +32,7 @@ tests, such as Doctrine, Twig and Monolog. To do so, .. code-block:: terminal - $ COMPOSER_ROOT_VERSION=5.4.x-dev composer update + $ COMPOSER_ROOT_VERSION=7.1.x-dev composer update .. _running: diff --git a/contributing/community/releases.rst b/contributing/community/releases.rst index fa452b67dfc..8126496bfef 100644 --- a/contributing/community/releases.rst +++ b/contributing/community/releases.rst @@ -19,7 +19,7 @@ published through a *time-based model*: .. tip:: - `Subscribe to Symfony Roadmap notifications`_ to receive an email when a new + `Subscribe to Symfony Release notifications`_ to receive an email when a new Symfony version is published or when a Symfony version reaches its end of life. .. _contributing-release-development: @@ -27,6 +27,13 @@ published through a *time-based model*: Development ----------- +.. note:: + + The Symfony project is an open-source community-driven development framework. + There is no roadmap written or defined in advance. Every feature request + may or may not be developed in future versions based on the community. + Symfony Core Team members can help move things forward if there's enough interest. + The full development period for any major or minor version lasts six months and is divided into two phases: @@ -43,7 +50,7 @@ final release. .. tip:: - Check out the `Symfony Roadmap`_ to learn more about any specific version. + Check out the `Symfony Release`_ to learn more about any specific version. .. _contributing-release-maintenance: .. _symfony-versions: @@ -155,6 +162,6 @@ period to upgrade. Companies wanting more stability use the LTS versions: a new version is published every two years and there is a year to upgrade. .. _`semantic versioning`: https://semver.org/ -.. _`Subscribe to Symfony Roadmap notifications`: https://symfony.com/account/notifications -.. _`Symfony Roadmap`: https://symfony.com/releases +.. _`Subscribe to Symfony Release notifications`: https://symfony.com/account/notifications +.. _`Symfony Release`: https://symfony.com/releases .. _`professional Symfony support`: https://sensiolabs.com/ diff --git a/contributing/community/reviews.rst b/contributing/community/reviews.rst index 94c37643988..06426c03985 100644 --- a/contributing/community/reviews.rst +++ b/contributing/community/reviews.rst @@ -59,15 +59,15 @@ The steps for the review are: #. **Is the Report Complete?** Good bug reports contain a link to a project (the "reproduction project") - created with the `Symfony skeleton`_ or the `Symfony website skeleton`_ - that reproduces the bug. If it doesn't, the report should at least contain - enough information and code samples to reproduce the bug. + created with the `Symfony skeleton`_ that reproduces the bug. If it + doesn't, the report should at least contain enough information and code + samples to reproduce the bug. #. **Reproduce the Bug** Download the reproduction project and test whether the bug can be reproduced on your system. If the reporter did not provide a reproduction project, - create one based on one `Symfony skeleton`_ (or the `Symfony website skeleton`_). + create one based on one `Symfony skeleton`_. #. **Update the Issue Status** @@ -134,9 +134,9 @@ Pick a pull request from the `PRs in need of review`_ and follow these steps: #. **Reproduce the Problem** Read the issue that the pull request is supposed to fix. Reproduce the - problem on a new project created with the `Symfony skeleton`_ (or the - `Symfony website skeleton`_) and try to understand why it exists. If the - linked issue already contains such a project, install it and run it on your system. + problem on a new project created with the `Symfony skeleton`_ and try to + understand why it exists. If the linked issue already contains such a + project, install it and run it on your system. #. **Review the Code** @@ -212,7 +212,6 @@ Pick a pull request from the `PRs in need of review`_ and follow these steps: .. _GitHub: https://github.com .. _Symfony issue tracker: https://github.com/symfony/symfony/issues .. _`Symfony skeleton`: https://github.com/symfony/skeleton -.. _`Symfony website skeleton`: https://github.com/symfony/website-skeleton .. _create a GitHub account: https://help.github.com/github/getting-started-with-github/signing-up-for-a-new-github-account .. _bug reports in need of review: https://github.com/symfony/symfony/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3A%22Bug%22+label%3A%22Status%3A+Needs+Review%22+ .. _PRs in need of review: https://github.com/symfony/symfony/pulls?q=is%3Aopen+is%3Apr+label%3A%22Status%3A+Needs+Review%22 diff --git a/contributing/documentation/format.rst b/contributing/documentation/format.rst index 1ff8b8e56c1..d933f3bcead 100644 --- a/contributing/documentation/format.rst +++ b/contributing/documentation/format.rst @@ -246,39 +246,39 @@ If you are documenting a brand new feature, a change or a deprecation that's been made in Symfony, you should precede your description of the change with the corresponding directive and a short description: -For a new feature or a behavior change use the ``.. versionadded:: 5.x`` +For a new feature or a behavior change use the ``.. versionadded:: 7.x`` directive: .. code-block:: rst - .. versionadded:: 5.2 + .. versionadded:: 7.2 - ... ... ... was introduced in Symfony 5.2. + ... ... ... was introduced in Symfony 7.2. If you are documenting a behavior change, it may be helpful to *briefly* describe how the behavior has changed: .. code-block:: rst - .. versionadded:: 5.2 + .. versionadded:: 7.2 - ... ... ... was introduced in Symfony 5.2. Prior to this, + ... ... ... was introduced in Symfony 7.2. Prior to this, ... ... ... ... ... ... ... ... . -For a deprecation use the ``.. deprecated:: 5.x`` directive: +For a deprecation use the ``.. deprecated:: 7.x`` directive: .. code-block:: rst - .. deprecated:: 5.2 + .. deprecated:: 7.2 - ... ... ... was deprecated in Symfony 5.2. + ... ... ... was deprecated in Symfony 7.2. -Whenever a new major version of Symfony is released (e.g. 6.0, 7.0, etc), a new +Whenever a new major version of Symfony is released (e.g. 8.0, 9.0, etc), a new branch of the documentation is created from the ``x.4`` branch of the previous major version. At this point, all the ``versionadded`` and ``deprecated`` tags for Symfony versions that have a lower major version will be removed. For -example, if Symfony 6.0 were released today, 5.0 to 5.4 ``versionadded`` and -``deprecated`` tags would be removed from the new ``6.0`` branch. +example, if Symfony 8.0 were released today, 7.0 to 7.4 ``versionadded`` and +``deprecated`` tags would be removed from the new ``8.0`` branch. .. _reStructuredText: https://docutils.sourceforge.io/rst.html .. _Sphinx: https://www.sphinx-doc.org/ diff --git a/contributing/documentation/standards.rst b/contributing/documentation/standards.rst index 0184fef36fc..420780d25f5 100644 --- a/contributing/documentation/standards.rst +++ b/contributing/documentation/standards.rst @@ -88,9 +88,9 @@ Configuration examples should show all supported formats using (and their orders) are: * **Configuration** (including services): YAML, XML, PHP -* **Routing**: Attributes, Annotations, YAML, XML, PHP -* **Validation**: Attributes, Annotations, YAML, XML, PHP -* **Doctrine Mapping**: Attributes, Annotations, YAML, XML, PHP +* **Routing**: Attributes, YAML, XML, PHP +* **Validation**: Attributes, YAML, XML, PHP +* **Doctrine Mapping**: Attributes, YAML, XML, PHP * **Translation**: XML, YAML, PHP * **Code Examples** (if applicable): PHP Symfony, PHP Standalone @@ -109,7 +109,7 @@ Example { // ... - public function foo($bar) + public function foo($bar): mixed { // set foo with a value of bar $foo = ...; diff --git a/controller.rst b/controller.rst index d4f7f99d43d..17cf30e40ef 100644 --- a/controller.rst +++ b/controller.rst @@ -23,13 +23,11 @@ class:: namespace App\Controller; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; class LuckyController { - /** - * @Route("/lucky/number/{max}", name="app_lucky_number") - */ + #[Route('/lucky/number/{max}', name: 'app_lucky_number')] public function number(int $max): Response { $number = random_int(0, $max); @@ -55,17 +53,17 @@ This controller is pretty straightforward: * *line 7*: The class can technically be called anything, but it's suffixed with ``Controller`` by convention. -* *line 12*: The action method is allowed to have a ``$max`` argument thanks to the +* *line 10*: The action method is allowed to have a ``$max`` argument thanks to the ``{max}`` :doc:`wildcard in the route `. -* *line 16*: The controller creates and returns a ``Response`` object. +* *line 14*: The controller creates and returns a ``Response`` object. Mapping a URL to a Controller ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In order to *view* the result of this controller, you need to map a URL to it via -a route. This was done above with the ``@Route("/lucky/number/{max}")`` -:ref:`route annotation `. +a route. This was done above with the ``#[Route('/lucky/number/{max}')]`` +:ref:`route attribute `. To see your page, go to this URL in your browser: http://localhost:8000/lucky/number/100 @@ -185,9 +183,7 @@ your :doc:`controller to be registered as a service `:: use Symfony\Component\HttpFoundation\Response; // ... - /** - * @Route("/lucky/number/{max}") - */ + #[Route('/lucky/number/{max}')] public function number(int $max, LoggerInterface $logger): Response { $logger->info('We are logging!'); @@ -203,66 +199,40 @@ command: $ php bin/console debug:autowiring -If you need control over the *exact* value of an argument, you can :ref:`bind ` -the argument by its name: +.. tip:: + + If you need control over the *exact* value of an argument, or require a parameter, + you can use the ``#[Autowire]`` attribute:: -.. configuration-block:: + // ... + use Psr\Log\LoggerInterface; + use Symfony\Component\DependencyInjection\Attribute\Autowire; + use Symfony\Component\HttpFoundation\Response; + + class LuckyController extends AbstractController + { + public function number( + int $max, + + // inject a specific logger service + #[Autowire(service: 'monolog.logger.request')] + LoggerInterface $logger, + + // or inject parameter values + #[Autowire('%kernel.project_dir%')] + string $projectDir + ): Response + { + $logger->info('We are logging!'); + // ... + } + } + + You can read more about this attribute in :ref:`autowire-attribute`. - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - # explicitly configure the service - App\Controller\LuckyController: - tags: [controller.service_arguments] - bind: - # for any $logger argument, pass this specific service - $logger: '@monolog.logger.doctrine' - # for any $projectDir argument, pass this parameter value - $projectDir: '%kernel.project_dir%' - - .. code-block:: xml - - - - - - - - - - - - - %kernel.project_dir% - - - - - .. code-block:: php - - // config/services.php - use App\Controller\LuckyController; - use Symfony\Component\DependencyInjection\Reference; - - $container->register(LuckyController::class) - ->addTag('controller.service_arguments') - ->setBindings([ - '$logger' => new Reference('monolog.logger.doctrine'), - '$projectDir' => '%kernel.project_dir%', - ]) - ; - -Like with all services, you can also use regular :ref:`constructor injection ` -in your controllers. +Like with all services, you can also use regular +:ref:`constructor injection ` in your +controllers. For more information about services, see the :doc:`/service_container` article. @@ -363,6 +333,361 @@ object. To access it in your controller, add it as an argument and :ref:`Keep reading ` for more information about using the Request object. +.. _controller_map-request: + +Automatic Mapping Of The Request +-------------------------------- + +It is possible to automatically map request's payload and/or query parameters to +your controller's action arguments with attributes. + +Mapping Query Parameters Individually +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Let's say a user sends you a request with the following query string: +``https://example.com/dashboard?firstName=John&lastName=Smith&age=27``. +Thanks to the :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapQueryParameter` +attribute, arguments of your controller's action can be automatically fulfilled:: + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; + + // ... + + public function dashboard( + #[MapQueryParameter] string $firstName, + #[MapQueryParameter] string $lastName, + #[MapQueryParameter] int $age, + ): Response + { + // ... + } + +``#[MapQueryParameter]`` can take an optional argument called ``filter``. You can use the +`Validate Filters`_ constants defined in PHP:: + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; + + // ... + + public function dashboard( + #[MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\w+$/'])] string $firstName, + #[MapQueryParameter] string $lastName, + #[MapQueryParameter(filter: \FILTER_VALIDATE_INT)] int $age, + ): Response + { + // ... + } + +.. _controller-mapping-query-string: + +Mapping The Whole Query String +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Another possibility is to map the entire query string into an object that will hold +available query parameters. Let's say you declare the following DTO with its +optional validation constraints:: + + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + + class UserDto + { + public function __construct( + #[Assert\NotBlank] + public string $firstName, + + #[Assert\NotBlank] + public string $lastName, + + #[Assert\GreaterThan(18)] + public int $age, + ) { + } + } + +You can then use the :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapQueryString` +attribute in your controller:: + + use App\Model\UserDto; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapQueryString; + + // ... + + public function dashboard( + #[MapQueryString] UserDto $userDto + ): Response + { + // ... + } + +You can customize the validation groups used during the mapping and also the +HTTP status to return if the validation fails:: + + use Symfony\Component\HttpFoundation\Response; + + // ... + + public function dashboard( + #[MapQueryString( + validationGroups: ['strict', 'edit'], + validationFailedStatusCode: Response::HTTP_UNPROCESSABLE_ENTITY + )] UserDto $userDto + ): Response + { + // ... + } + +The default status code returned if the validation fails is 404. + +If you need a valid DTO even when the request query string is empty, set a +default value for your controller arguments:: + + use App\Model\UserDto; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapQueryString; + + // ... + + public function dashboard( + #[MapQueryString] UserDto $userDto = new UserDto() + ): Response + { + // ... + } + +.. _controller-mapping-request-payload: + +Mapping Request Payload +~~~~~~~~~~~~~~~~~~~~~~~ + +When creating an API and dealing with other HTTP methods than ``GET`` (like +``POST`` or ``PUT``), user's data are not stored in the query string +but directly in the request payload, like this: + +.. code-block:: json + + { + "firstName": "John", + "lastName": "Smith", + "age": 28 + } + +In this case, it is also possible to directly map this payload to your DTO by +using the :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapRequestPayload` +attribute:: + + use App\Model\UserDto; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; + + // ... + + public function dashboard( + #[MapRequestPayload] UserDto $userDto + ): Response + { + // ... + } + +This attribute allows you to customize the serialization context as well +as the class responsible of doing the mapping between the request and +your DTO:: + + public function dashboard( + #[MapRequestPayload( + serializationContext: ['...'], + resolver: App\Resolver\UserDtoResolver + )] + UserDto $userDto + ): Response + { + // ... + } + +You can also customize the validation groups used, the status code to return if +the validation fails as well as supported payload formats:: + + use Symfony\Component\HttpFoundation\Response; + + // ... + + public function dashboard( + #[MapRequestPayload( + acceptFormat: 'json', + validationGroups: ['strict', 'read'], + validationFailedStatusCode: Response::HTTP_NOT_FOUND + )] UserDto $userDto + ): Response + { + // ... + } + +The default status code returned if the validation fails is 422. + +.. tip:: + + If you build a JSON API, make sure to declare your route as using the JSON + :ref:`format `. This will make the error handling + output a JSON response in case of validation errors, rather than an HTML page:: + + #[Route('/dashboard', name: 'dashboard', format: 'json')] + +Make sure to install `phpstan/phpdoc-parser`_ and `phpdocumentor/type-resolver`_ +if you want to map a nested array of specific DTOs:: + + public function dashboard( + #[MapRequestPayload] EmployeesDto $employeesDto + ): Response + { + // ... + } + + final class EmployeesDto + { + /** + * @param UserDto[] $users + */ + public function __construct( + public readonly array $users = [] + ) {} + } + +Instead of returning an array of DTO objects, you can tell Symfony to transform +each DTO object into an array and return something like this: + +.. code-block:: json + + [ + { + "firstName": "John", + "lastName": "Smith", + "age": 28 + }, + { + "firstName": "Jane", + "lastName": "Doe", + "age": 30 + } + ] + +To do so, map the parameter as an array and configure the type of each element +using the ``type`` option of the attribute:: + + public function dashboard( + #[MapRequestPayload(type: UserDTO::class)] array $users + ): Response + { + // ... + } + +.. versionadded:: 7.1 + + The ``type`` option of ``#[MapRequestPayload]`` was introduced in Symfony 7.1. + +.. _controller_map-uploaded-file: + +Mapping Uploaded Files +~~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides an attribute called ``#[MapUploadedFile]`` to map one or more +``UploadedFile`` objects to controller arguments:: + + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\File\UploadedFile; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapUploadedFile; + use Symfony\Component\Routing\Attribute\Route; + + class UserController extends AbstractController + { + #[Route('/user/picture', methods: ['PUT'])] + public function changePicture( + #[MapUploadedFile] UploadedFile $picture, + ): Response { + // ... + } + } + +In this example, the associated :doc:`argument resolver ` +fetches the ``UploadedFile`` based on the argument name (``$picture``). If no file +is submitted, an ``HttpException`` is thrown. You can change this by making the +controller argument nullable: + +.. code-block:: php-attributes + + #[MapUploadedFile] + ?UploadedFile $document + +The ``#[MapUploadedFile]`` attribute also allows to pass a list of constraints +to apply to the uploaded file:: + + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\File\UploadedFile; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapUploadedFile; + use Symfony\Component\Routing\Attribute\Route; + use Symfony\Component\Validator\Constraints as Assert; + + class UserController extends AbstractController + { + #[Route('/user/picture', methods: ['PUT'])] + public function changePicture( + #[MapUploadedFile([ + new Assert\File(mimeTypes: ['image/png', 'image/jpeg']), + new Assert\Image(maxWidth: 3840, maxHeight: 2160), + ])] + UploadedFile $picture, + ): Response { + // ... + } + } + +The validation constraints are checked before injecting the ``UploadedFile`` into +the controller argument. If there's a constraint violation, an ``HttpException`` +is thrown and the controller's action is not executed. + +If you need to upload a collection of files, map them to an array or a variadic +argument. The given constraint will be applied to all files and if any of them +fails, an ``HttpException`` is thrown: + +.. code-block:: php-attributes + + #[MapUploadedFile(new Assert\File(mimeTypes: ['application/pdf']))] + array $documents + + #[MapUploadedFile(new Assert\File(mimeTypes: ['application/pdf']))] + UploadedFile ...$documents + +Use the ``name`` option to rename the uploaded file to a custom value: + +.. code-block:: php-attributes + + #[MapUploadedFile(name: 'something-else')] + UploadedFile $document + +In addition, you can change the status code of the HTTP exception thrown when +there are constraint violations: + +.. code-block:: php-attributes + + #[MapUploadedFile( + constraints: new Assert\File(maxSize: '2M'), + validationFailedStatusCode: Response::HTTP_REQUEST_ENTITY_TOO_LARGE + )] + UploadedFile $document + +.. versionadded:: 7.1 + + The ``#[MapUploadedFile]`` attribute was introduced in Symfony 7.1. + Managing the Session -------------------- @@ -422,7 +747,7 @@ the ``Request`` class:: // retrieves GET and POST variables respectively $request->query->get('page'); - $request->request->get('page'); + $request->getPayload()->get('page'); // retrieves SERVER variables $request->server->get('HTTP_HOST'); @@ -533,6 +858,57 @@ The ``file()`` helper provides some arguments to configure its behavior:: return $this->file('invoice_3241.pdf', 'my_invoice.pdf', ResponseHeaderBag::DISPOSITION_INLINE); } +Sending Early Hints +~~~~~~~~~~~~~~~~~~~ + +`Early hints`_ tell the browser to start downloading some assets even before the +application sends the response content. This improves perceived performance +because the browser can prefetch resources that will be needed once the full +response is finally sent. These resources are commonly Javascript or CSS files, +but they can be any type of resource. + +.. note:: + + In order to work, the `SAPI`_ you're using must support this feature, like + `FrankenPHP`_. + +You can send early hints from your controller action thanks to the +:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::sendEarlyHints` +method:: + + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + use Symfony\Component\WebLink\Link; + + class HomepageController extends AbstractController + { + #[Route("/", name: "homepage")] + public function index(): Response + { + $response = $this->sendEarlyHints([ + new Link(rel: 'preconnect', href: 'https://fonts.google.com'), + (new Link(href: '/style.css'))->withAttribute('as', 'stylesheet'), + (new Link(href: '/script.js'))->withAttribute('as', 'script'), + ]); + + // prepare the contents of the response... + + return $this->render('homepage/index.html.twig', response: $response); + } + } + +Technically, Early Hints are an informational HTTP response with the status code +``103``. The ``sendEarlyHints()`` method creates a ``Response`` object with that +status code and sends its headers immediately. + +This way, browsers can start downloading the assets immediately; like the +``style.css`` and ``script.js`` files in the above example. The +``sendEarlyHints()`` method also returns the ``Response`` object, which you +must use to create the full response sent from the controller action. + Final Thoughts -------------- @@ -567,3 +943,9 @@ Learn more about Controllers .. _`Symfony Maker`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html .. _`unvalidated redirects security vulnerability`: https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html +.. _`Early hints`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103 +.. _`SAPI`: https://www.php.net/manual/en/function.php-sapi-name.php +.. _`FrankenPHP`: https://frankenphp.dev +.. _`Validate Filters`: https://www.php.net/manual/en/filter.filters.validate.php +.. _`phpstan/phpdoc-parser`: https://packagist.org/packages/phpstan/phpdoc-parser +.. _`phpdocumentor/type-resolver`: https://packagist.org/packages/phpdocumentor/type-resolver diff --git a/controller/argument_value_resolver.rst b/controller/argument_value_resolver.rst deleted file mode 100644 index 1cddcede0bf..00000000000 --- a/controller/argument_value_resolver.rst +++ /dev/null @@ -1,268 +0,0 @@ -Extending Action Argument Resolving -=================================== - -In the :doc:`controller guide `, you've learned that you can get the -:class:`Symfony\\Component\\HttpFoundation\\Request` object via an argument in -your controller. This argument has to be type-hinted by the ``Request`` class -in order to be recognized. This is done via the -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver`. By -creating and registering custom argument value resolvers, you can extend this -functionality. - -.. _functionality-shipped-with-the-httpkernel: - -Built-In Value Resolvers ------------------------- - -Symfony ships with the following value resolvers in the -:doc:`HttpKernel component `: - -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\RequestAttributeValueResolver` - Attempts to find a request attribute that matches the name of the argument. - -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\RequestValueResolver` - Injects the current ``Request`` if type-hinted with ``Request`` or a class - extending ``Request``. - -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\ServiceValueResolver` - Injects a service if type-hinted with a valid service class or interface. This - works like :doc:`autowiring `. - -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\SessionValueResolver` - Injects the configured session class implementing ``SessionInterface`` if - type-hinted with ``SessionInterface`` or a class implementing - ``SessionInterface``. - -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\DefaultValueResolver` - Will set the default value of the argument if present and the argument - is optional. - -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\VariadicValueResolver` - Verifies if the request data is an array and will add all of them to the - argument list. When the action is called, the last (variadic) argument will - contain all the values of this array. - -In addition, some components and official bundles provide other value resolvers: - -:class:`Symfony\\Component\\Security\\Http\\Controller\\UserValueResolver` - Injects the object that represents the current logged in user if type-hinted - with ``UserInterface``. You can also type-hint your own ``User`` class but you - must then add the ``#[CurrentUser]`` attribute to the argument. Default value - can be set to ``null`` in case the controller can be accessed by anonymous - users. It requires installing the :doc:`SecurityBundle `. - -PSR-7 Objects Resolver: - Injects a Symfony HttpFoundation ``Request`` object created from a PSR-7 object - of type ``Psr\Http\Message\ServerRequestInterface``, - ``Psr\Http\Message\RequestInterface`` or ``Psr\Http\Message\MessageInterface``. - It requires installing :doc:`the PSR-7 Bridge ` component. - -Adding a Custom Value Resolver ------------------------------- - -In the next example, you'll create a value resolver to inject the object that -represents the current user whenever a controller method type-hints an argument -with the ``User`` class:: - - // src/Controller/UserController.php - namespace App\Controller; - - use App\Entity\User; - use Symfony\Component\HttpFoundation\Response; - - class UserController - { - public function index(User $user) - { - return new Response('Hello '.$user->getUserIdentifier().'!'); - } - } - -Beware that this feature is already provided by the `@ParamConverter`_ -annotation from the SensioFrameworkExtraBundle. If you have that bundle -installed in your project, add this config to disable the auto-conversion of -type-hinted method arguments: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/sensio_framework_extra.yaml - sensio_framework_extra: - request: - converters: true - auto_convert: false - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // config/packages/sensio_framework_extra.php - $container->loadFromExtension('sensio_framework_extra', [ - 'request' => [ - 'converters' => true, - 'auto_convert' => false, - ], - ]); - -Adding a new value resolver requires creating a class that implements -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentValueResolverInterface` -and defining a service for it. The interface defines two methods: - -``supports()`` - This method is used to check whether the value resolver supports the - given argument. ``resolve()`` will only be called when this returns ``true``. -``resolve()`` - This method will resolve the actual value for the argument. Once the value - is resolved, you must `yield`_ the value to the ``ArgumentResolver``. - -Both methods get the ``Request`` object, which is the current request, and an -:class:`Symfony\\Component\\HttpKernel\\ControllerMetadata\\ArgumentMetadata` -instance. This object contains all information retrieved from the method signature -for the current argument. - -Now that you know what to do, you can implement this interface. To get the -current ``User``, you need the current security token. This token can be -retrieved from the token storage:: - - // src/ArgumentResolver/UserValueResolver.php - namespace App\ArgumentResolver; - - use App\Entity\User; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; - use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; - use Symfony\Component\Security\Core\Security; - - class UserValueResolver implements ArgumentValueResolverInterface - { - private $security; - - public function __construct(Security $security) - { - $this->security = $security; - } - - public function supports(Request $request, ArgumentMetadata $argument): bool - { - if (User::class !== $argument->getType()) { - return false; - } - - return $this->security->getUser() instanceof User; - } - - public function resolve(Request $request, ArgumentMetadata $argument): iterable - { - yield $this->security->getUser(); - } - } - -In order to get the actual ``User`` object in your argument, the given value -must fulfill the following requirements: - -* An argument must be type-hinted as ``User`` in your action method signature; -* The value must be an instance of the ``User`` class. - -When all those requirements are met and ``true`` is returned, the -``ArgumentResolver`` calls ``resolve()`` with the same values as it called -``supports()``. - -That's it! Now all you have to do is add the configuration for the service -container. This can be done by tagging the service with ``controller.argument_value_resolver`` -and adding a priority. - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - _defaults: - # ... be sure autowiring is enabled - autowire: true - # ... - - App\ArgumentResolver\UserValueResolver: - tags: - - { name: controller.argument_value_resolver, priority: 50 } - - .. code-block:: xml - - - - - - - - - - - - - - - - - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - use App\ArgumentResolver\UserValueResolver; - - return static function (ContainerConfigurator $container) { - $services = $container->services(); - - $services->set(UserValueResolver::class) - ->tag('controller.argument_value_resolver', ['priority' => 50]) - ; - }; - -While adding a priority is optional, it's recommended to add one to make sure -the expected value is injected. The built-in ``RequestAttributeValueResolver``, -which fetches attributes from the ``Request``, has a priority of ``100``. If your -resolver also fetches ``Request`` attributes, set a priority of ``100`` or more. -Otherwise, set a priority lower than ``100`` to make sure the argument resolver -is not triggered when the ``Request`` attribute is present (for example, when -passing the user along sub-requests). - -To ensure your resolvers are added in the right position you can run the following -command to see which argument resolvers are present and in which order they run. - -.. code-block:: terminal - - $ php bin/console debug:container debug.argument_resolver.inner --show-arguments - -.. tip:: - - As you can see in the ``UserValueResolver::supports()`` method, the user - may not be available (e.g. when the controller is not behind a firewall). - In these cases, the resolver will not be executed. If no argument value - is resolved, an exception will be thrown. - - To prevent this, you can add a default value in the controller (e.g. ``User - $user = null``). The ``DefaultValueResolver`` is executed as the last - resolver and will use the default value if no value was already resolved. - -.. _`@ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html -.. _`yield`: https://www.php.net/manual/en/language.generators.syntax.php diff --git a/controller/error_pages.rst b/controller/error_pages.rst index 6a8b343ceca..001e637c03e 100644 --- a/controller/error_pages.rst +++ b/controller/error_pages.rst @@ -176,7 +176,7 @@ automatically when installing ``symfony/framework-bundle``): // config/routes/framework.php use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return function (RoutingConfigurator $routes): void { if ('dev' === $routes->env()) { $routes->import('@FrameworkBundle/Resources/config/routing/errors.xml') ->prefix('/_error') @@ -216,7 +216,7 @@ contents, create a new Normalizer that supports the ``FlattenException`` input:: class MyCustomProblemNormalizer implements NormalizerInterface { - public function normalize($exception, ?string $format = null, array $context = []) + public function normalize($exception, ?string $format = null, array $context = []): array { return [ 'content' => 'This is my custom problem normalizer.', @@ -227,7 +227,7 @@ contents, create a new Normalizer that supports the ``FlattenException`` input:: ]; } - public function supportsNormalization($data, ?string $format = null) + public function supportsNormalization($data, ?string $format = null, array $context = []): bool { return $data instanceof FlattenException; } @@ -275,7 +275,7 @@ configuration option to point to it: // config/packages/framework.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { // ... $framework->errorController('App\Controller\ErrorController::show'); }; diff --git a/controller/service.rst b/controller/service.rst index d7a263e7206..88af093ff29 100644 --- a/controller/service.rst +++ b/controller/service.rst @@ -66,7 +66,7 @@ apply the ``controller.service_arguments`` tag to your controller services:: use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\AsController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; #[AsController] class HelloController @@ -78,10 +78,6 @@ apply the ``controller.service_arguments`` tag to your controller services:: } } -.. versionadded:: 5.3 - - The ``#[AsController]`` attribute was introduced in Symfony 5.3. - Registering your controller as a service is the first step, but you also need to update your routing config to reference the service properly, so that Symfony knows to use it. @@ -93,31 +89,13 @@ a service like: ``App\Controller\HelloController::index``: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/HelloController.php - namespace App\Controller; - - use Symfony\Component\Routing\Annotation\Route; - - class HelloController - { - /** - * @Route("/hello", name="hello", methods={"GET"}) - */ - public function index(): Response - { - // ... - } - } - .. code-block:: php-attributes // src/Controller/HelloController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; class HelloController { @@ -155,7 +133,7 @@ a service like: ``App\Controller\HelloController::index``: use App\Controller\HelloController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return function (RoutingConfigurator $routes): void { $routes->add('hello', '/hello') ->controller([HelloController::class, 'index']) ->methods(['GET']) @@ -173,32 +151,13 @@ which is a common practice when following the `ADR pattern`_ .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/Hello.php - namespace App\Controller; - - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; - - /** - * @Route("/hello/{name}", name="hello") - */ - class Hello - { - public function __invoke(string $name = 'World'): Response - { - return new Response(sprintf('Hello %s!', $name)); - } - } - .. code-block:: php-attributes // src/Controller/Hello.php namespace App\Controller; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; #[Route('/hello/{name}', name: 'hello')] class Hello @@ -263,11 +222,9 @@ service and use it directly:: class HelloController { - private Environment $twig; - - public function __construct(Environment $twig) - { - $this->twig = $twig; + public function __construct( + private Environment $twig, + ) { } public function index(string $name): Response diff --git a/controller/upload_file.rst b/controller/upload_file.rst index b122b76c71a..b3dc2d6ffd0 100644 --- a/controller/upload_file.rst +++ b/controller/upload_file.rst @@ -21,10 +21,8 @@ add a PDF brochure for each product. To do so, add a new property called { // ... - /** - * @ORM\Column(type="string") - */ - private $brochureFilename; + #[ORM\Column(type: 'string')] + private string $brochureFilename; public function getBrochureFilename(): string { @@ -74,7 +72,7 @@ so Symfony doesn't try to get/set its value from the related entity:: // every time you edit the Product details 'required' => false, - // unmapped fields can't define their validation using annotations + // unmapped fields can't define their validation using attributes // in the associated entity, so you can use the PHP constraint classes 'constraints' => [ new File([ @@ -122,19 +120,22 @@ Finally, you need to update the code of the controller that handles the form:: use App\Entity\Product; use App\Form\ProductType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\String\Slugger\SluggerInterface; class ProductController extends AbstractController { - /** - * @Route("/product/new", name="app_product_new") - */ - public function new(Request $request, SluggerInterface $slugger, string $brochuresDirectory): Response + #[Route('/product/new', name: 'app_product_new')] + public function new( + Request $request, + SluggerInterface $slugger, + #[Autowire('%kernel.project_dir%/public/uploads/brochures')] string $brochuresDirectory + ): Response { $product = new Product(); $form = $this->createForm(ProductType::class, $product); @@ -169,24 +170,12 @@ Finally, you need to update the code of the controller that handles the form:: return $this->redirectToRoute('app_product_list'); } - return $this->renderForm('product/new.html.twig', [ + return $this->render('product/new.html.twig', [ 'form' => $form, ]); } } -Now, bind the ``$brochuresDirectory`` controller argument to its actual value -using the service configuration: - -.. code-block:: yaml - - # config/services.yaml - services: - _defaults: - # ... - bind: - string $brochuresDirectory: '%kernel.project_dir%/public/uploads/brochures' - There are some important things to consider in the code of the above controller: #. In Symfony applications, uploaded files are objects of the @@ -196,13 +185,24 @@ There are some important things to consider in the code of the above controller: users. This also applies to the files uploaded by your visitors. The ``UploadedFile`` class provides methods to get the original file extension (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalExtension`), - the original file size (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getSize`) - and the original file name (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalName`). + the original file size (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getSize`), + the original file name (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalName`) + and the original file path (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalPath`). However, they are considered *not safe* because a malicious user could tamper that information. That's why it's always better to generate a unique name and use the :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::guessExtension` method to let Symfony guess the right extension according to the file MIME type; +.. note:: + + If a directory was uploaded, ``getClientOriginalPath()`` will contain + the **webkitRelativePath** as provided by the browser. Otherwise this + value will be identical to ``getClientOriginalName()``. + +.. versionadded:: 7.1 + + The ``getClientOriginalPath()`` method was introduced in Symfony 7.1. + You can use the following code to link to the PDF brochure of a product: .. code-block:: html+twig @@ -239,13 +239,10 @@ logic to a separate service:: class FileUploader { - private $targetDirectory; - private $slugger; - - public function __construct($targetDirectory, SluggerInterface $slugger) - { - $this->targetDirectory = $targetDirectory; - $this->slugger = $slugger; + public function __construct( + private string $targetDirectory, + private SluggerInterface $slugger, + ) { } public function upload(UploadedFile $file): string @@ -317,7 +314,7 @@ Then, define a service for this class: use App\Service\FileUploader; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $services = $container->services(); $services->set(FileUploader::class) @@ -332,9 +329,10 @@ Now you're ready to use this service in the controller:: use App\Service\FileUploader; use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; // ... - public function new(Request $request, FileUploader $fileUploader) + public function new(Request $request, FileUploader $fileUploader): Response { // ... diff --git a/controller/value_resolver.rst b/controller/value_resolver.rst new file mode 100644 index 00000000000..dbbea7bcc87 --- /dev/null +++ b/controller/value_resolver.rst @@ -0,0 +1,444 @@ +Extending Action Argument Resolving +=================================== + +In the :doc:`controller guide `, you've learned that you can get the +:class:`Symfony\\Component\\HttpFoundation\\Request` object via an argument in +your controller. This argument has to be type-hinted by the ``Request`` class +in order to be recognized. This is done via the +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver`. By +creating and registering custom value resolvers, you can extend this +functionality. + +.. _functionality-shipped-with-the-httpkernel: + +Built-In Value Resolvers +------------------------ + +Symfony ships with the following value resolvers in the +:doc:`HttpKernel component `: + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\BackedEnumValueResolver` + Attempts to resolve a backed enum case from a route path parameter that matches the name of the argument. + Leads to a 404 Not Found response if the value isn't a valid backing value for the enum type. + + For example, if your backed enum is:: + + namespace App\Model; + + enum Suit: string + { + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'S'; + } + + And your controller contains the following:: + + class CardController + { + #[Route('/cards/{suit}')] + public function list(Suit $suit): Response + { + // ... + } + + // ... + } + + When requesting the ``/cards/H`` URL, the ``$suit`` variable will store the + ``Suit::Hearts`` case. + + Furthermore, you can limit route parameter's allowed values to + only one (or more) with ``EnumRequirement``:: + + use Symfony\Component\Routing\Requirement\EnumRequirement; + + // ... + + class CardController + { + #[Route('/cards/{suit}', requirements: [ + // this allows all values defined in the Enum + 'suit' => new EnumRequirement(Suit::class), + // this restricts the possible values to the Enum values listed here + 'suit' => new EnumRequirement([Suit::Diamonds, Suit::Spades]), + ])] + public function list(Suit $suit): Response + { + // ... + } + + // ... + } + + The example above allows requesting only ``/cards/D`` and ``/cards/S`` + URLs and leads to 404 Not Found response in two other cases. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\RequestPayloadValueResolver` + Maps the request payload or the query string into the type-hinted object. + + Because this is a :ref:`targeted value resolver `, + you'll have to use either the :ref:`MapRequestPayload ` + or the :ref:`MapQueryString ` attribute + in order to use this resolver. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\RequestAttributeValueResolver` + Attempts to find a request attribute that matches the name of the argument. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\DateTimeValueResolver` + Attempts to find a request attribute that matches the name of the argument + and injects a ``DateTimeInterface`` object if type-hinted with a class + extending ``DateTimeInterface``. + + By default any input that can be parsed as a date string by PHP is accepted. + You can restrict how the input can be formatted with the + :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapDateTime` attribute. + + .. tip:: + + The ``DateTimeInterface`` object is generated with the :doc:`Clock component `. + This gives you full control over the date and time values the controller + receives when testing your application and using the + :class:`Symfony\\Component\\Clock\\MockClock` implementation. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\RequestValueResolver` + Injects the current ``Request`` if type-hinted with ``Request`` or a class + extending ``Request``. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\ServiceValueResolver` + Injects a service if type-hinted with a valid service class or interface. This + works like :doc:`autowiring `. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\SessionValueResolver` + Injects the configured session class implementing ``SessionInterface`` if + type-hinted with ``SessionInterface`` or a class implementing + ``SessionInterface``. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\DefaultValueResolver` + Will set the default value of the argument if present and the argument + is optional. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\UidValueResolver` + Attempts to convert any UID values from a route path parameter into UID objects. + Leads to a 404 Not Found response if the value isn't a valid UID. + + For example, the following will convert the token parameter into a ``UuidV4`` object:: + + // src/Controller/DefaultController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + use Symfony\Component\Uid\UuidV4; + + class DefaultController + { + #[Route('/share/{token}')] + public function share(UuidV4 $token): Response + { + // ... + } + } + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\VariadicValueResolver` + Verifies if the request data is an array and will add all of them to the + argument list. When the action is called, the last (variadic) argument will + contain all the values of this array. + +In addition, some components, bridges and official bundles provide other value resolvers: + +:class:`Symfony\\Component\\Security\\Http\\Controller\\UserValueResolver` + Injects the object that represents the current logged in user if type-hinted + with ``UserInterface``. You can also type-hint your own ``User`` class but you + must then add the ``#[CurrentUser]`` attribute to the argument. Default value + can be set to ``null`` in case the controller can be accessed by anonymous + users. It requires installing the :doc:`SecurityBundle `. + + If the argument is not nullable and there is no logged in user or the logged in + user has a user class not matching the type-hinted class, an ``AccessDeniedException`` + is thrown by the resolver to prevent access to the controller. + +:class:`Symfony\\Component\\Security\\Http\\Controller\\SecurityTokenValueResolver` + Injects the object that represents the current logged in token if type-hinted + with ``TokenInterface`` or a class extending it. + + If the argument is not nullable and there is no logged in token, an ``HttpException`` + with status code 401 is thrown by the resolver to prevent access to the controller. + +:class:`Symfony\\Bridge\\Doctrine\\ArgumentResolver\\EntityValueResolver` + Automatically query for an entity and pass it as an argument to your controller. + + For example, the following will query the ``Product`` entity which has ``{id}`` as primary key:: + + // src/Controller/DefaultController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + + class DefaultController + { + #[Route('/product/{id}')] + public function share(Product $product): Response + { + // ... + } + } + + To learn more about the use of the ``EntityValueResolver``, see the dedicated + section :ref:`Automatically Fetching Objects `. + +PSR-7 Objects Resolver: + Injects a Symfony HttpFoundation ``Request`` object created from a PSR-7 object + of type ``Psr\Http\Message\ServerRequestInterface``, + ``Psr\Http\Message\RequestInterface`` or ``Psr\Http\Message\MessageInterface``. + It requires installing :doc:`the PSR-7 Bridge ` component. + +Managing Value Resolvers +------------------------ + +For each argument, every resolver tagged with ``controller.argument_value_resolver`` +will be called until one provides a value. The order in which they are called depends +on their priority. For example, the ``SessionValueResolver`` will be called before the +``DefaultValueResolver`` because its priority is higher. This allows to write e.g. +``SessionInterface $session = null`` to get the session if there is one, or ``null`` +if there is none. + +In that specific case, you don't need any resolver running before +``SessionValueResolver``, so skipping them would not only improve performance, +but also prevent one of them providing a value before ``SessionValueResolver`` +has a chance to. + +The :class:`Symfony\\Component\\HttpKernel\\Attribute\\ValueResolver` attribute +lets you do this by "targeting" the resolver you want:: + + // src/Controller/SessionController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpFoundation\Session\SessionInterface; + use Symfony\Component\HttpKernel\Attribute\ValueResolver; + use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver; + use Symfony\Component\Routing\Attribute\Route; + + class SessionController + { + #[Route('/')] + public function __invoke( + #[ValueResolver(SessionValueResolver::class)] + SessionInterface $session = null + ): Response + { + // ... + } + } + +In the example above, the ``SessionValueResolver`` will be called first because +it is targeted. The ``DefaultValueResolver`` will be called next if no value has +been provided; that's why you can assign ``null`` as ``$session``'s default value. + +You can target a resolver by passing its name as ``ValueResolver``'s first argument. +For convenience, built-in resolvers' name are their FQCN. + +A targeted resolver can also be disabled by passing ``ValueResolver``'s ``$disabled`` +argument to ``true``; this is how :ref:`MapEntity allows to disable the +EntityValueResolver for a specific controller `. +Yes, ``MapEntity`` extends ``ValueResolver``! + +Adding a Custom Value Resolver +------------------------------ + +In the next example, you'll create a value resolver to inject an ID value +object whenever a controller argument has a type implementing +``IdentifierInterface`` (e.g. ``BookingId``):: + + // src/Controller/BookingController.php + namespace App\Controller; + + use App\Reservation\BookingId; + use Symfony\Component\HttpFoundation\Response; + + class BookingController + { + public function index(BookingId $id): Response + { + // ... do something with $id + } + } + +Adding a new value resolver requires creating a class that implements +:class:`Symfony\\Component\\HttpKernel\\Controller\\ValueResolverInterface` +and defining a service for it. + +This interface contains a ``resolve()`` method, which is called for each +argument of the controller. It receives the current ``Request`` object and an +:class:`Symfony\\Component\\HttpKernel\\ControllerMetadata\\ArgumentMetadata` +instance, which contains all information from the method signature. + +The ``resolve()`` method should return either an empty array (if it cannot resolve +this argument) or an array with the resolved value(s). Usually arguments are +resolved as a single value, but variadic arguments require resolving multiple +values. That's why you must always return an array, even for single values:: + + // src/ValueResolver/IdentifierValueResolver.php + namespace App\ValueResolver; + + use App\IdentifierInterface; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; + use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; + + class BookingIdValueResolver implements ValueResolverInterface + { + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + // get the argument type (e.g. BookingId) + $argumentType = $argument->getType(); + if ( + !$argumentType + || !is_subclass_of($argumentType, IdentifierInterface::class, true) + ) { + return []; + } + + // get the value from the request, based on the argument name + $value = $request->attributes->get($argument->getName()); + if (!is_string($value)) { + return []; + } + + // create and return the value object + return [$argumentType::fromString($value)]; + } + } + +This method first checks whether it can resolve the value: + +* The argument must be type-hinted with a class implementing a custom ``IdentifierInterface``; +* The argument name (e.g. ``$id``) must match the name of a request + attribute (e.g. using a ``/booking/{id}`` route placeholder). + +When those requirements are met, the method creates a new instance of the +custom value object and returns it as the value for this argument. + +That's it! Now all you have to do is add the configuration for the service +container. This can be done by adding one of the following tags to your value resolver. + +``controller.argument_value_resolver`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This tag is automatically added to every service implementing ``ValueResolverInterface``, +but you can set it yourself to change its ``priority`` or ``name`` attributes. + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + _defaults: + # ... be sure autowiring is enabled + autowire: true + # ... + + App\ValueResolver\BookingIdValueResolver: + tags: + - controller.argument_value_resolver: + name: booking_id + priority: 150 + + .. code-block:: xml + + + + + + + + + + + + controller.argument_value_resolver + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\ValueResolver\BookingIdValueResolver; + + return static function (ContainerConfigurator $containerConfigurator): void { + $services = $containerConfigurator->services(); + + $services->set(BookingIdValueResolver::class) + ->tag('controller.argument_value_resolver', ['name' => 'booking_id', 'priority' => 150]) + ; + }; + +While adding a priority is optional, it's recommended to add one to make sure +the expected value is injected. The built-in ``RequestAttributeValueResolver``, +which fetches attributes from the ``Request``, has a priority of ``100``. If your +resolver also fetches ``Request`` attributes, set a priority of ``100`` or more. +Otherwise, set a priority lower than ``100`` to make sure the argument resolver +is not triggered when the ``Request`` attribute is present. + +To ensure your resolvers are added in the right position you can run the following +command to see which argument resolvers are present and in which order they run: + +.. code-block:: terminal + + $ php bin/console debug:container debug.argument_resolver.inner --show-arguments + +You can also configure the name passed to the ``ValueResolver`` attribute to target +your resolver. Otherwise it will default to the service's id. + +.. _value-resolver-targeted: + +``controller.targeted_value_resolver`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Set this tag if you want your resolver to be called only if it is targeted by a +``ValueResolver`` attribute. Like ``controller.argument_value_resolver``, you +can customize the name by which your resolver can be targeted. + +As an alternative, you can add the +:class:`Symfony\\Component\\HttpKernel\\Attribute\\AsTargetedValueResolver` attribute +to your resolver and pass your custom name as its first argument:: + + // src/ValueResolver/IdentifierValueResolver.php + namespace App\ValueResolver; + + use Symfony\Component\HttpKernel\Attribute\AsTargetedValueResolver; + use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; + + #[AsTargetedValueResolver('booking_id')] + class BookingIdValueResolver implements ValueResolverInterface + { + // ... + } + +You can then pass this name as ``ValueResolver``'s first argument to target your resolver:: + + // src/Controller/BookingController.php + namespace App\Controller; + + use App\Reservation\BookingId; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\ValueResolver; + + class BookingController + { + public function index(#[ValueResolver('booking_id')] BookingId $id): Response + { + // ... do something with $id + } + } diff --git a/create_framework/event_dispatcher.rst b/create_framework/event_dispatcher.rst index 181c75b00d2..650e4c7554e 100644 --- a/create_framework/event_dispatcher.rst +++ b/create_framework/event_dispatcher.rst @@ -45,20 +45,15 @@ the Response instance:: class Framework { - private $dispatcher; - private $matcher; - private $controllerResolver; - private $argumentResolver; - - public function __construct(EventDispatcher $dispatcher, UrlMatcherInterface $matcher, ControllerResolverInterface $controllerResolver, ArgumentResolverInterface $argumentResolver) - { - $this->dispatcher = $dispatcher; - $this->matcher = $matcher; - $this->controllerResolver = $controllerResolver; - $this->argumentResolver = $argumentResolver; + public function __construct( + private EventDispatcher $dispatcher, + private UrlMatcherInterface $matcher, + private ControllerResolverInterface $controllerResolver, + private ArgumentResolverInterface $argumentResolver, + ) { } - public function handle(Request $request) + public function handle(Request $request): Response { $this->matcher->getContext()->fromRequest($request); @@ -94,21 +89,18 @@ now dispatched:: class ResponseEvent extends Event { - private $request; - private $response; - - public function __construct(Response $response, Request $request) - { - $this->response = $response; - $this->request = $request; + public function __construct( + private Response $response, + private Request $request, + ) { } - public function getResponse() + public function getResponse(): Response { return $this->response; } - public function getRequest() + public function getRequest(): Request { return $this->request; } @@ -125,7 +117,7 @@ the registration of a listener for the ``response`` event:: use Symfony\Component\EventDispatcher\EventDispatcher; $dispatcher = new EventDispatcher(); - $dispatcher->addListener('response', function (Simplex\ResponseEvent $event) { + $dispatcher->addListener('response', function (Simplex\ResponseEvent $event): void { $response = $event->getResponse(); if ($response->isRedirection() @@ -164,7 +156,7 @@ So far so good, but let's add another listener on the same event. Let's say that we want to set the ``Content-Length`` of the Response if it is not already set:: - $dispatcher->addListener('response', function (Simplex\ResponseEvent $event) { + $dispatcher->addListener('response', function (Simplex\ResponseEvent $event): void { $response = $event->getResponse(); $headers = $response->headers; @@ -182,7 +174,7 @@ a positive number; negative numbers can be used for low priority listeners. Here, we want the ``Content-Length`` listener to be executed last, so change the priority to ``-255``:: - $dispatcher->addListener('response', function (Simplex\ResponseEvent $event) { + $dispatcher->addListener('response', function (Simplex\ResponseEvent $event): void { $response = $event->getResponse(); $headers = $response->headers; @@ -203,7 +195,7 @@ Let's refactor the code a bit by moving the Google listener to its own class:: class GoogleListener { - public function onResponse(ResponseEvent $event) + public function onResponse(ResponseEvent $event): void { $response = $event->getResponse(); @@ -225,7 +217,7 @@ And do the same with the other listener:: class ContentLengthListener { - public function onResponse(ResponseEvent $event) + public function onResponse(ResponseEvent $event): void { $response = $event->getResponse(); $headers = $response->headers; @@ -267,7 +259,7 @@ look at the new version of the ``GoogleListener``:: { // ... - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return ['response' => 'onResponse']; } @@ -284,7 +276,7 @@ And here is the new version of ``ContentLengthListener``:: { // ... - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return ['response' => ['onResponse', -255]]; } diff --git a/create_framework/http_foundation.rst b/create_framework/http_foundation.rst index 4406dde64a0..219119164b4 100644 --- a/create_framework/http_foundation.rst +++ b/create_framework/http_foundation.rst @@ -61,7 +61,7 @@ unit test for the above code:: class IndexTest extends TestCase { - public function testHello() + public function testHello(): void { $_GET['name'] = 'Fabien'; @@ -176,20 +176,20 @@ fingertips thanks to a nice and simple API:: // the URI being requested (e.g. /about) minus any query parameters $request->getPathInfo(); - // retrieve GET and POST variables respectively + // retrieves GET and POST variables respectively $request->query->get('foo'); - $request->request->get('bar', 'default value if bar does not exist'); + $request->getPayload()->get('bar', 'default value if bar does not exist'); - // retrieve SERVER variables + // retrieves SERVER variables $request->server->get('HTTP_HOST'); // retrieves an instance of UploadedFile identified by foo $request->files->get('foo'); - // retrieve a COOKIE value + // retrieves a COOKIE value $request->cookies->get('PHPSESSID'); - // retrieve an HTTP request header, with normalized, lowercase keys + // retrieves a HTTP request header, with normalized, lowercase keys $request->headers->get('host'); $request->headers->get('content-type'); diff --git a/create_framework/http_kernel_controller_resolver.rst b/create_framework/http_kernel_controller_resolver.rst index 12d9efead6e..1c2857c9ed9 100644 --- a/create_framework/http_kernel_controller_resolver.rst +++ b/create_framework/http_kernel_controller_resolver.rst @@ -10,7 +10,7 @@ class:: class LeapYearController { - public function index($request) + public function index($request): Response { if (is_leap_year($request->attributes->get('year'))) { return new Response('Yep, this is a leap year!'); @@ -112,26 +112,26 @@ More interesting, ``getArguments()`` is also able to inject any Request attribute; if the argument has the same name as the corresponding attribute:: - public function index($year) + public function index(int $year) You can also inject the Request and some attributes at the same time (as the matching is done on the argument name or a type hint, the arguments order does not matter):: - public function index(Request $request, $year) + public function index(Request $request, int $year) - public function index($year, Request $request) + public function index(int $year, Request $request) Finally, you can also define default values for any argument that matches an optional attribute of the Request:: - public function index($year = 2012) + public function index(int $year = 2012) Let's inject the ``$year`` request attribute for our controller:: class LeapYearController { - public function index($year) + public function index(int $year): Response { if (is_leap_year($year)) { return new Response('Yep, this is a leap year!'); @@ -165,7 +165,7 @@ Let's conclude with the new version of our framework:: use Symfony\Component\HttpKernel; use Symfony\Component\Routing; - function render_template(Request $request) + function render_template(Request $request): Response { extract($request->attributes->all(), EXTR_SKIP); ob_start(); diff --git a/create_framework/http_kernel_httpkernel_class.rst b/create_framework/http_kernel_httpkernel_class.rst index 0f4e565b084..ecf9d4c7879 100644 --- a/create_framework/http_kernel_httpkernel_class.rst +++ b/create_framework/http_kernel_httpkernel_class.rst @@ -69,7 +69,7 @@ Our code is now much more concise and surprisingly more robust and more powerful than ever. For instance, use the built-in ``ErrorListener`` to make your error management configurable:: - $errorHandler = function (Symfony\Component\ErrorHandler\Exception\FlattenException $exception) { + $errorHandler = function (Symfony\Component\ErrorHandler\Exception\FlattenException $exception): Response { $msg = 'Something went wrong! ('.$exception->getMessage().')'; return new Response($msg, $exception->getStatusCode()); @@ -96,7 +96,7 @@ The error controller reads as follows:: class ErrorController { - public function exception(FlattenException $exception) + public function exception(FlattenException $exception): Response { $msg = 'Something went wrong! ('.$exception->getMessage().')'; @@ -114,11 +114,6 @@ client; that's what the ``ResponseListener`` does:: $dispatcher->addSubscriber(new HttpKernel\EventListener\ResponseListener('UTF-8')); -If you want out of the box support for streamed responses, subscribe -to ``StreamedResponseListener``:: - - $dispatcher->addSubscriber(new HttpKernel\EventListener\StreamedResponseListener()); - And in your controller, return a ``StreamedResponse`` instance instead of a ``Response`` instance. @@ -133,7 +128,7 @@ instead of a full Response object:: class LeapYearController { - public function index($year) + public function index(int $year): string { $leapYear = new LeapYear(); if ($leapYear->isLeapYear($year)) { @@ -158,7 +153,7 @@ only if needed:: class StringResponseListener implements EventSubscriberInterface { - public function onView(ViewEvent $event) + public function onView(ViewEvent $event): void { $response = $event->getControllerResult(); @@ -167,7 +162,7 @@ only if needed:: } } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return ['kernel.view' => 'onView']; } diff --git a/create_framework/http_kernel_httpkernelinterface.rst b/create_framework/http_kernel_httpkernelinterface.rst index f883b4a2e1d..8d28fc9d24b 100644 --- a/create_framework/http_kernel_httpkernelinterface.rst +++ b/create_framework/http_kernel_httpkernelinterface.rst @@ -16,9 +16,9 @@ goal by making our framework implement ``HttpKernelInterface``:: */ public function handle( Request $request, - $type = self::MAIN_REQUEST, - $catch = true - ); + int $type = self::MAIN_REQUEST, + bool $catch = true + ): Response; } ``HttpKernelInterface`` is probably the most important piece of code in the @@ -39,8 +39,8 @@ Update your framework so that it implements this interface:: public function handle( Request $request, - $type = HttpKernelInterface::MAIN_REQUEST, - $catch = true + int $type = HttpKernelInterface::MAIN_REQUEST, + bool $catch = true ) { // ... } @@ -76,7 +76,7 @@ to cache a response for 10 seconds, use the ``Response::setTtl()`` method:: // example.com/src/Calendar/Controller/LeapYearController.php // ... - public function index(Request $request, $year) + public function index(Request $request, int $year): Response { $leapYear = new LeapYear(); if ($leapYear->isLeapYear($year)) { diff --git a/create_framework/routing.rst b/create_framework/routing.rst index f76167ec2fb..71e3a8250e1 100644 --- a/create_framework/routing.rst +++ b/create_framework/routing.rst @@ -35,7 +35,7 @@ template as follows: .. code-block:: html+php - Hello + Hello Now, we are in good shape to add new features. diff --git a/create_framework/separation_of_concerns.rst b/create_framework/separation_of_concerns.rst index 24d34f0e82b..5238b3aac42 100644 --- a/create_framework/separation_of_concerns.rst +++ b/create_framework/separation_of_concerns.rst @@ -27,18 +27,14 @@ request handling logic into its own ``Simplex\Framework`` class:: class Framework { - private $matcher; - private $controllerResolver; - private $argumentResolver; - - public function __construct(UrlMatcher $matcher, ControllerResolver $controllerResolver, ArgumentResolver $argumentResolver) - { - $this->matcher = $matcher; - $this->controllerResolver = $controllerResolver; - $this->argumentResolver = $argumentResolver; + public function __construct( + private UrlMatcher $matcher, + private ControllerResolver $controllerResolver, + private ArgumentResolver $argumentResolver, + ) { } - public function handle(Request $request) + public function handle(Request $request): Response { $this->matcher->getContext()->fromRequest($request); @@ -106,7 +102,7 @@ Move the controller to ``Calendar\Controller\LeapYearController``:: class LeapYearController { - public function index(Request $request, $year) + public function index(Request $request, int $year): Response { $leapYear = new LeapYear(); if ($leapYear->isLeapYear($year)) { @@ -124,7 +120,7 @@ And move the ``is_leap_year()`` function to its own class too:: class LeapYear { - public function isLeapYear($year = null) + public function isLeapYear(?int $year = null): bool { if (null === $year) { $year = date('Y'); diff --git a/create_framework/templating.rst b/create_framework/templating.rst index f7ff66fa9f8..282e75cbc94 100644 --- a/create_framework/templating.rst +++ b/create_framework/templating.rst @@ -38,7 +38,7 @@ that renders a template when there is no specific logic. To keep the same template as before, request attributes are extracted before the template is rendered:: - function render_template($request) + function render_template(Request $request): Response { extract($request->attributes->all(), EXTR_SKIP); ob_start(); @@ -74,7 +74,7 @@ can still use the ``render_template()`` to render a template:: $routes->add('hello', new Routing\Route('/hello/{name}', [ 'name' => 'World', - '_controller' => function ($request) { + '_controller' => function (Request $request): string { return render_template($request); } ])); @@ -84,7 +84,7 @@ you can even pass additional arguments to the template:: $routes->add('hello', new Routing\Route('/hello/{name}', [ 'name' => 'World', - '_controller' => function ($request) { + '_controller' => function (Request $request): Response { // $foo will be available in the template $request->attributes->set('foo', 'bar'); @@ -106,7 +106,7 @@ Here is the updated and improved version of our framework:: use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing; - function render_template($request) + function render_template(Request $request): Response { extract($request->attributes->all(), EXTR_SKIP); ob_start(); @@ -146,7 +146,7 @@ framework does not need to be modified in any way, create a new use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing; - function is_leap_year($year = null) + function is_leap_year(?int $year = null): bool { if (null === $year) { $year = (int)date('Y'); @@ -158,7 +158,7 @@ framework does not need to be modified in any way, create a new $routes = new Routing\RouteCollection(); $routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', [ 'year' => null, - '_controller' => function ($request) { + '_controller' => function (Request $request): Response { if (is_leap_year($request->attributes->get('year'))) { return new Response('Yep, this is a leap year!'); } diff --git a/create_framework/unit_testing.rst b/create_framework/unit_testing.rst index e39c96b9035..32c97a03846 100644 --- a/create_framework/unit_testing.rst +++ b/create_framework/unit_testing.rst @@ -62,15 +62,11 @@ resolver. Modify the framework to make use of them:: class Framework { - protected $matcher; - protected $controllerResolver; - protected $argumentResolver; - - public function __construct(UrlMatcherInterface $matcher, ControllerResolverInterface $resolver, ArgumentResolverInterface $argumentResolver) - { - $this->matcher = $matcher; - $this->controllerResolver = $resolver; - $this->argumentResolver = $argumentResolver; + public function __construct( + private UrlMatcherInterface $matcher, + private ControllerResolverInterface $resolver, + private ArgumentResolverInterface $argumentResolver, + ) { } // ... @@ -91,7 +87,7 @@ We are now ready to write our first test:: class FrameworkTest extends TestCase { - public function testNotFoundHandling() + public function testNotFoundHandling(): void { $framework = $this->getFrameworkForException(new ResourceNotFoundException()); @@ -100,11 +96,9 @@ We are now ready to write our first test:: $this->assertEquals(404, $response->getStatusCode()); } - private function getFrameworkForException($exception) + private function getFrameworkForException($exception): Framework { $matcher = $this->createMock(Routing\Matcher\UrlMatcherInterface::class); - // use getMock() on PHPUnit 5.3 or below - // $matcher = $this->getMock(Routing\Matcher\UrlMatcherInterface::class); $matcher ->expects($this->once()) @@ -143,7 +137,7 @@ either in the test or in the framework code! Adding a unit test for any exception thrown in a controller:: - public function testErrorHandling() + public function testErrorHandling(): void { $framework = $this->getFrameworkForException(new \RuntimeException()); @@ -160,11 +154,9 @@ Response:: use Symfony\Component\HttpKernel\Controller\ControllerResolver; // ... - public function testControllerResponse() + public function testControllerResponse(): void { $matcher = $this->createMock(Routing\Matcher\UrlMatcherInterface::class); - // use getMock() on PHPUnit 5.3 or below - // $matcher = $this->getMock(Routing\Matcher\UrlMatcherInterface::class); $matcher ->expects($this->once()) diff --git a/deployment.rst b/deployment.rst index da05990b5ef..3edbc34dd6b 100644 --- a/deployment.rst +++ b/deployment.rst @@ -62,7 +62,7 @@ Using Platforms as a Service Using a Platform as a Service (PaaS) can be a great way to deploy your Symfony app quickly. There are many PaaS, but we recommend `Platform.sh`_ as it -provides a dedicated Symfony integration and help fund the Symfony development. +provides a dedicated Symfony integration and helps fund the Symfony development. Using Build Scripts and other Tools ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -211,6 +211,7 @@ setup: * Add/edit CRON jobs * Restarting your workers * :ref:`Building and minifying your assets ` with Webpack Encore +* :ref:`Compile your assets ` if you're using the AssetMapper component * Pushing assets to a CDN * On a shared hosting platform using the Apache web server, you may need to install the `symfony/apache-pack`_ package diff --git a/deployment/proxies.rst b/deployment/proxies.rst index 3d5bab95474..40c2550ee2c 100644 --- a/deployment/proxies.rst +++ b/deployment/proxies.rst @@ -33,6 +33,8 @@ and what headers your reverse proxy uses to send information: # ... # the IP address (or range) of your proxy trusted_proxies: '192.0.0.1,10.0.0.0/8' + # shortcut for private IP address ranges of your proxy + trusted_proxies: 'private_ranges' # trust *all* "X-Forwarded-*" headers trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix'] # or, if your proxy instead uses the "Forwarded" header @@ -53,6 +55,8 @@ and what headers your reverse proxy uses to send information: 192.0.0.1,10.0.0.0/8 + + private_ranges x-forwarded-for @@ -71,10 +75,12 @@ and what headers your reverse proxy uses to send information: // config/packages/framework.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework // the IP address (or range) of your proxy ->trustedProxies('192.0.0.1,10.0.0.0/8') + // shortcut for private IP address ranges of your proxy + ->trustedProxies('private_ranges') // trust *all* "X-Forwarded-*" headers (the ! prefix means to not trust those headers) ->trustedHeaders(['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix']) // or, if your proxy instead uses the "Forwarded" header @@ -82,11 +88,20 @@ and what headers your reverse proxy uses to send information: ; }; -.. deprecated:: 5.2 +.. versionadded:: 7.1 - In previous Symfony versions, the above example used ``HEADER_X_FORWARDED_ALL`` - to trust all "X-Forwarded-" headers, but that constant is deprecated since - Symfony 5.2 in favor of the individual ``HEADER_X_FORWARDED_*`` constants. + ``private_ranges`` as a shortcut for private IP address ranges for the + ``trusted_proxies`` option was introduced in Symfony 7.1. + +.. caution:: + + Enabling the ``Request::HEADER_X_FORWARDED_HOST`` option exposes the + application to `HTTP Host header attacks`_. Make sure the proxy really + sends an ``x-forwarded-host`` header. + +The Request object has several ``Request::HEADER_*`` constants that control exactly +*which* headers from your reverse proxy are trusted. The argument is a bit field, +so you can also pass your own value (e.g. ``0b00110``). .. tip:: @@ -106,23 +121,6 @@ and what headers your reverse proxy uses to send information: .. danger:: - Enabling the ``Request::HEADER_X_FORWARDED_HOST`` option exposes the - application to `HTTP Host header attacks`_. Make sure the proxy really - sends an ``x-forwarded-host`` header. - -The Request object has several ``Request::HEADER_*`` constants that control exactly -*which* headers from your reverse proxy are trusted. The argument is a bit field, -so you can also pass your own value (e.g. ``0b00110``). - -.. versionadded:: 5.2 - - The feature to configure trusted proxies and headers with ``trusted_proxies`` - and ``trusted_headers`` options was introduced in Symfony 5.2. In earlier - Symfony versions you needed to use the ``Request::setTrustedProxies()`` - method in the ``public/index.php`` file. - -.. caution:: - The "trusted proxies" feature does not work as expected when using the `nginx realip module`_. Disable that module when serving Symfony applications. @@ -206,8 +204,31 @@ handling the request:: // ... $response = $kernel->handle($request); +Overriding Configuration Behind Hidden SSL Termination +------------------------------------------------------ + +Some cloud setups (like running a Docker container with the "Web App for Containers" +in `Microsoft Azure`_) do SSL termination and contact your web server over HTTP, but +do not change the remote address nor set the ``X-Forwarded-*`` headers. This means +the trusted proxy feature of Symfony can't help you. + +Once you made sure your server is only reachable through the cloud proxy over HTTPS +and not through HTTP, you can override the information your web server sends to PHP. +For Nginx, this could look like this: + +.. code-block:: nginx + + location ~ ^/index\.php$ { + fastcgi_pass 127.0.0.1:9000; + include fastcgi.conf; + # Lie to Symfony about the protocol and port so that it generates the correct HTTPS URLs + fastcgi_param SERVER_PORT "443"; + fastcgi_param HTTPS "on"; + } + .. _`security groups`: https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-groups.html .. _`CloudFront`: https://en.wikipedia.org/wiki/Amazon_CloudFront .. _`CloudFront IP ranges`: https://ip-ranges.amazonaws.com/ip-ranges.json .. _`HTTP Host header attacks`: https://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html .. _`nginx realip module`: https://nginx.org/en/docs/http/ngx_http_realip_module.html +.. _`Microsoft Azure`: https://en.wikipedia.org/wiki/Microsoft_Azure diff --git a/doctrine.rst b/doctrine.rst index 5c881e31429..ca1ed25b7b5 100644 --- a/doctrine.rst +++ b/doctrine.rst @@ -62,10 +62,11 @@ The database connection information is stored as an environment variable called If the username, password, host or database name contain any character considered special in a URI (such as ``: / ? # [ ] @ ! $ & ' ( ) * + , ; =``), - you must encode them. See `RFC 3986`_ for the full list of reserved characters or - use the :phpfunction:`urlencode` function to encode them. In this case you need to - remove the ``resolve:`` prefix in ``config/packages/doctrine.yaml`` to avoid errors: - ``url: '%env(DATABASE_URL)%'`` + you must encode them. See `RFC 3986`_ for the full list of reserved characters. + You can use the :phpfunction:`urlencode` function to encode them or + the :ref:`urlencode environment variable processor `. + In this case you need to remove the ``resolve:`` prefix in ``config/packages/doctrine.yaml`` + to avoid errors: ``url: '%env(DATABASE_URL)%'`` Now that your connection parameters are setup, Doctrine can create the ``db_name`` database for you: @@ -173,13 +174,6 @@ Whoa! You now have a new ``src/Entity/Product.php`` file:: Confused why the price is an integer? Don't worry: this is just an example. But, storing prices as integers (e.g. 100 = $1 USD) can avoid rounding issues. -.. note:: - - If you are using an SQLite database, you'll see the following error: - *PDOException: SQLSTATE[HY000]: General error: 1 Cannot add a NOT NULL - column with default value NULL*. Add a ``nullable=true`` option to the - ``description`` property to fix the problem. - .. caution:: There is a `limit of 767 bytes for the index key prefix`_ when using @@ -206,7 +200,7 @@ add/remove fields, add/remove methods or update configuration. Doctrine supports a wide variety of field types, each with their own options. Check out the `list of Doctrine mapping types`_ in the Doctrine documentation. -If you want to use XML instead of annotations, add ``type: xml`` and +If you want to use XML instead of attributes, add ``type: xml`` and ``dir: '%kernel.project_dir%/config/doctrine'`` to the entity mappings in your ``config/packages/doctrine.yaml`` file. @@ -215,8 +209,8 @@ If you want to use XML instead of annotations, add ``type: xml`` and Be careful not to use reserved SQL keywords as your table or column names (e.g. ``GROUP`` or ``USER``). See Doctrine's `Reserved SQL keywords documentation`_ for details on how to escape these. Or, change the table name with - ``#[ORM\Table(name: "groups")]`` above the class or configure the column name with - the ``name: "group_name"`` option. + ``#[ORM\Table(name: 'groups')]`` above the class or configure the column name with + the ``name: 'group_name'`` option. .. _doctrine-creating-the-database-tables-schema: @@ -293,13 +287,14 @@ methods: // src/Entity/Product.php // ... + + use Doctrine\DBAL\Types\Types; class Product { // ... - + #[ORM\Column(type: 'text')] - + private $description; + + #[ORM\Column(type: Types::TEXT)] + + private string $description; // getDescription() & setDescription() were also added } @@ -325,6 +320,13 @@ before, execute your migrations: $ php bin/console doctrine:migrations:migrate +.. caution:: + + If you are using an SQLite database, you'll see the following error: + *PDOException: SQLSTATE[HY000]: General error: 1 Cannot add a NOT NULL + column with default value NULL*. Add a ``nullable=true`` option to the + ``description`` property to fix the problem. + This will only execute the *one* new migration file, because DoctrineMigrationsBundle knows that the first migration was already executed earlier. Behind the scenes, it manages a ``migration_versions`` table to track this. @@ -365,17 +367,15 @@ and save it:: // ... use App\Entity\Product; - use Doctrine\Persistence\ManagerRegistry; + use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; class ProductController extends AbstractController { #[Route('/product', name: 'create_product')] - public function createProduct(ManagerRegistry $doctrine): Response + public function createProduct(EntityManagerInterface $entityManager): Response { - $entityManager = $doctrine->getManager(); - $product = new Product(); $product->setName('Keyboard'); $product->setPrice(1999); @@ -409,21 +409,18 @@ Take a look at the previous example in more detail: .. _doctrine-entity-manager: -* **line 13** The ``ManagerRegistry $doctrine`` argument tells Symfony to - :ref:`inject the Doctrine service ` into the - controller method. +* **line 13** The ``EntityManagerInterface $entityManager`` argument tells Symfony + to :ref:`inject the Entity Manager service ` into + the controller method. This object is responsible for saving objects to, and + fetching objects from, the database. -* **line 15** The ``$doctrine->getManager()`` method gets Doctrine's - *entity manager* object, which is the most important object in Doctrine. It's - responsible for saving objects to, and fetching objects from, the database. - -* **lines 17-20** In this section, you instantiate and work with the ``$product`` +* **lines 15-18** In this section, you instantiate and work with the ``$product`` object like any other normal PHP object. -* **line 23** The ``persist($product)`` call tells Doctrine to "manage" the +* **line 21** The ``persist($product)`` call tells Doctrine to "manage" the ``$product`` object. This does **not** cause a query to be made to the database. -* **line 26** When the ``flush()`` method is called, Doctrine looks through +* **line 24** When the ``flush()`` method is called, Doctrine looks through all of the objects that it's managing to see if they need to be persisted to the database. In this example, the ``$product`` object's data doesn't exist in the database, so the entity manager executes an ``INSERT`` query, @@ -442,15 +439,19 @@ is smart enough to know if it should INSERT or UPDATE your entity. Validating Objects ------------------ -:doc:`The Symfony validator ` reuses Doctrine metadata to perform -some basic validation tasks:: +:doc:`The Symfony validator ` can reuse Doctrine metadata to perform +some basic validation tasks. First, add or configure the +:ref:`auto_mapping option ` to define which +entities should be introspected by Symfony to add automatic validation constraints. + +Consider the following controller code:: // src/Controller/ProductController.php namespace App\Controller; use App\Entity\Product; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Validator\Validator\ValidatorInterface; // ... @@ -460,12 +461,8 @@ some basic validation tasks:: public function createProduct(ValidatorInterface $validator): Response { $product = new Product(); - // This will trigger an error: the column isn't nullable in the database - $product->setName(null); - // This will trigger a type mismatch error: an integer is expected - $product->setPrice('1999'); - // ... + // ... update the product data somehow (e.g. with a form) ... $errors = $validator->validate($product); if (count($errors) > 0) { @@ -477,9 +474,11 @@ some basic validation tasks:: } Although the ``Product`` entity doesn't define any explicit -:doc:`validation configuration `, Symfony introspects the Doctrine -mapping configuration to infer some validation rules. For example, given that -the ``name`` property can't be ``null`` in the database, a +:doc:`validation configuration `, if the ``auto_mapping`` option +includes it in the list of entities to introspect, Symfony will infer some +validation rules for it and will apply them. + +For example, given that the ``name`` property can't be ``null`` in the database, a :doc:`NotNull constraint ` is added automatically to the property (if it doesn't contain that constraint already). @@ -514,16 +513,17 @@ be able to go to ``/product/1`` to see your new product:: namespace App\Controller; use App\Entity\Product; + use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; // ... class ProductController extends AbstractController { #[Route('/product/{id}', name: 'product_show')] - public function show(ManagerRegistry $doctrine, int $id): Response + public function show(EntityManagerInterface $entityManager, int $id): Response { - $product = $doctrine->getRepository(Product::class)->find($id); + $product = $entityManager->getRepository(Product::class)->find($id); if (!$product) { throw $this->createNotFoundException( @@ -548,7 +548,7 @@ and injected by the dependency injection container:: use App\Entity\Product; use App\Repository\ProductRepository; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; // ... class ProductController extends AbstractController @@ -573,7 +573,7 @@ job is to help you fetch entities of a certain class. Once you have a repository object, you have many helper methods:: - $repository = $doctrine->getRepository(Product::class); + $repository = $entityManager->getRepository(Product::class); // look for a single Product by its primary key (usually "id") $product = $repository->find($id); @@ -615,19 +615,17 @@ the :ref:`doctrine-queries` section. For more information, read the :doc:`Symfony profiler documentation `. -Automatically Fetching Objects (ParamConverter) ------------------------------------------------ - .. _doctrine-entity-value-resolver: -In many cases, you can use the `SensioFrameworkExtraBundle`_ to do the query -for you automatically! First, install the bundle in case you don't have it: +Automatically Fetching Objects (EntityValueResolver) +---------------------------------------------------- -.. code-block:: terminal +.. versionadded:: 2.7.1 - $ composer require sensio/framework-extra-bundle + Autowiring of the ``EntityValueResolver`` was introduced in DoctrineBundle 2.7.1. -Now, simplify your controller:: +In many cases, you can use the ``EntityValueResolver`` to do the query for you +automatically! You can simplify the controller to:: // src/Controller/ProductController.php namespace App\Controller; @@ -635,12 +633,12 @@ Now, simplify your controller:: use App\Entity\Product; use App\Repository\ProductRepository; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; // ... class ProductController extends AbstractController { - #[Route('/product/{id}', name: 'product_show')] + #[Route('/product/{id}')] public function show(Product $product): Response { // use the Product! @@ -651,7 +649,217 @@ Now, simplify your controller:: That's it! The bundle uses the ``{id}`` from the route to query for the ``Product`` by the ``id`` column. If it's not found, a 404 page is generated. -There are many more options you can use. Read more about the `ParamConverter`_. +.. tip:: + + When enabled globally, it's possible to disable the behavior on a specific + controller, by using the ``MapEntity`` set to ``disabled``:: + + public function show( + #[CurrentUser] + #[MapEntity(disabled: true)] + User $user + ): Response { + // User is not resolved by the EntityValueResolver + // ... + } + +Fetch Automatically +~~~~~~~~~~~~~~~~~~~ + +If your route wildcards match properties on your entity, then the resolver +will automatically fetch them:: + + /** + * Fetch via primary key because {id} is in the route. + */ + #[Route('/product/{id}')] + public function showByPk(Product $product): Response + { + } + + /** + * Perform a findOneBy() where the slug property matches {slug}. + */ + #[Route('/product/{slug}')] + public function showBySlug(Product $product): Response + { + } + +Automatic fetching works in these situations: + +* If ``{id}`` is in your route, then this is used to fetch by + primary key via the ``find()`` method. + +* The resolver will attempt to do a ``findOneBy()`` fetch by using + *all* of the wildcards in your route that are actually properties + on your entity (non-properties are ignored). + +This behavior is enabled by default on all controllers. If you prefer, you can +restrict this feature to only work on route wildcards called ``id`` to look for +entities by primary key. To do so, set the option +``doctrine.orm.controller_resolver.auto_mapping`` to ``false``. + +When ``auto_mapping`` is disabled, you can configure the mapping explicitly for +any controller argument with the ``MapEntity`` attribute. You can even control +the ``EntityValueResolver`` behavior by using the `MapEntity options`_ :: + + // src/Controller/ProductController.php + namespace App\Controller; + + use App\Entity\Product; + use Symfony\Bridge\Doctrine\Attribute\MapEntity; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + // ... + + class ProductController extends AbstractController + { + #[Route('/product/{slug}')] + public function show( + #[MapEntity(mapping: ['slug' => 'slug'])] + Product $product + ): Response { + // use the Product! + // ... + } + } + +Fetch via an Expression +~~~~~~~~~~~~~~~~~~~~~~~ + +If automatic fetching doesn't work for your use case, you can write an expression +using the :doc:`ExpressionLanguage component `:: + + #[Route('/product/{product_id}')] + public function show( + #[MapEntity(expr: 'repository.find(product_id)')] + Product $product + ): Response { + } + +In the expression, the ``repository`` variable will be your entity's +Repository class and any route wildcards - like ``{product_id}`` are +available as variables. + +The repository method called in the expression can also return a list of entities. +In that case, update the type of your controller argument:: + + #[Route('/posts_by/{author_id}')] + public function authorPosts( + #[MapEntity(class: Post::class, expr: 'repository.findBy({"author": author_id}, {}, 10)')] + iterable $posts + ): Response { + } + +.. versionadded:: 7.1 + + The mapping of the lists of entities was introduced in Symfony 7.1. + +This can also be used to help resolve multiple arguments:: + + #[Route('/product/{id}/comments/{comment_id}')] + public function show( + Product $product, + #[MapEntity(expr: 'repository.find(comment_id)')] + Comment $comment + ): Response { + } + +In the example above, the ``$product`` argument is handled automatically, +but ``$comment`` is configured with the attribute since they cannot both follow +the default convention. + +If you need to get other information from the request to query the database, you +can also access the request in your expression thanks to the ``request`` +variable. Let's say you want the first or the last comment of a product depending on a query parameter named ``sort``:: + + #[Route('/product/{id}/comments')] + public function show( + Product $product, + #[MapEntity(expr: 'repository.findOneBy({"product": id}, {"createdAt": request.query.get("sort", "DESC")})')] + Comment $comment + ): Response { + } + +MapEntity Options +~~~~~~~~~~~~~~~~~ + +A number of options are available on the ``MapEntity`` attribute to +control behavior: + +``id`` + If an ``id`` option is configured and matches a route parameter, then + the resolver will find by the primary key:: + + #[Route('/product/{product_id}')] + public function show( + #[MapEntity(id: 'product_id')] + Product $product + ): Response { + } + +``mapping`` + Configures the properties and values to use with the ``findOneBy()`` + method: the key is the route placeholder name and the value is the Doctrine + property name:: + + #[Route('/product/{category}/{slug}/comments/{comment_slug}')] + public function show( + #[MapEntity(mapping: ['category' => 'category', 'slug' => 'slug'])] + Product $product, + #[MapEntity(mapping: ['comment_slug' => 'slug'])] + Comment $comment + ): Response { + } + +``exclude`` + Configures the properties that should be used in the ``findOneBy()`` + method by *excluding* one or more properties so that not *all* are used:: + + #[Route('/product/{slug}/{date}')] + public function show( + #[MapEntity(exclude: ['date'])] + Product $product, + \DateTime $date + ): Response { + } + +``stripNull`` + If true, then when ``findOneBy()`` is used, any values that are + ``null`` will not be used for the query. + +``objectManager`` + By default, the ``EntityValueResolver`` uses the *default* + object manager, but you can configure this:: + + #[Route('/product/{id}')] + public function show( + #[MapEntity(objectManager: 'foo')] + Product $product + ): Response { + } + +``evictCache`` + If true, forces Doctrine to always fetch the entity from the database + instead of cache. + +``disabled`` + If true, the ``EntityValueResolver`` will not try to replace the argument. + +``message`` + An optional custom message displayed when there's a :class:`Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException`, + but **only in the development environment** (you won't see this message in production):: + + #[Route('/product/{product_id}')] + public function show( + #[MapEntity(id: 'product_id', message: 'The product does not exist')] + Product $product + ): Response { + } + +.. versionadded:: 7.1 + + The ``message`` option was introduced in Symfony 7.1. Updating an Object ------------------ @@ -664,16 +872,16 @@ with any PHP model:: use App\Entity\Product; use App\Repository\ProductRepository; + use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; // ... class ProductController extends AbstractController { #[Route('/product/edit/{id}', name: 'product_edit')] - public function update(ManagerRegistry $doctrine, int $id): Response + public function update(EntityManagerInterface $entityManager, int $id): Response { - $entityManager = $doctrine->getManager(); $product = $entityManager->getRepository(Product::class)->find($id); if (!$product) { @@ -722,7 +930,7 @@ You've already seen how the repository object allows you to run basic queries without any work:: // from inside a controller - $repository = $doctrine->getRepository(Product::class); + $repository = $entityManager->getRepository(Product::class); $product = $repository->find($id); But what if you need a more complex query? When you generated your entity with @@ -789,7 +997,7 @@ Now, you can call this method on the repository:: // from inside a controller $minPrice = 1000; - $products = $doctrine->getRepository(Product::class)->findAllGreaterThanPrice($minPrice); + $products = $entityManager->getRepository(Product::class)->findAllGreaterThanPrice($minPrice); // ... @@ -849,8 +1057,8 @@ In addition, you can query directly with SQL if you need to:: WHERE p.price > :price ORDER BY p.price ASC '; - $stmt = $conn->prepare($sql); - $resultSet = $stmt->executeQuery(['price' => $price]); + + $resultSet = $conn->executeQuery($sql, ['price' => $price]); // returns an array of arrays (i.e. a raw data set) return $resultSet->fetchAllAssociative(); @@ -895,7 +1103,6 @@ Learn more doctrine/associations doctrine/events - doctrine/registration_form doctrine/custom_dql_functions doctrine/dbal doctrine/multiple_entity_managers @@ -913,8 +1120,6 @@ Learn more .. _`Transactions and Concurrency`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/transactions-and-concurrency.html .. _`DoctrineMigrationsBundle`: https://github.com/doctrine/DoctrineMigrationsBundle .. _`NativeQuery`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/native-sql.html -.. _`SensioFrameworkExtraBundle`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html -.. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html .. _`limit of 767 bytes for the index key prefix`: https://dev.mysql.com/doc/refman/5.6/en/innodb-limits.html .. _`Doctrine screencast series`: https://symfonycasts.com/screencast/symfony-doctrine .. _`API Platform`: https://api-platform.com/docs/core/validation/ diff --git a/doctrine/associations.rst b/doctrine/associations.rst index 5cd1ff1e07f..8dd9aa7f36b 100644 --- a/doctrine/associations.rst +++ b/doctrine/associations.rst @@ -98,7 +98,7 @@ From the perspective of the ``Product`` entity, this is a many-to-one relationsh From the perspective of the ``Category`` entity, this is a one-to-many relationship. To map this, first create a ``category`` property on the ``Product`` class with -the ``ManyToOne`` annotation. You can do this by hand, or by using the ``make:entity`` +the ``ManyToOne`` attribute. You can do this by hand, or by using the ``make:entity`` command, which will ask you several questions about your relationship. If you're not sure of the answer, don't worry! You can always change the settings later: @@ -144,34 +144,6 @@ the ``Product`` entity (and getter & setter methods): .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/Product.php - namespace App\Entity; - - // ... - class Product - { - // ... - - /** - * @ORM\ManyToOne(targetEntity="App\Entity\Category", inversedBy="products") - */ - private $category; - - public function getCategory(): ?Category - { - return $this->category; - } - - public function setCategory(?Category $category): self - { - $this->category = $category; - - return $this; - } - } - .. code-block:: php-attributes // src/Entity/Product.php @@ -182,8 +154,8 @@ the ``Product`` entity (and getter & setter methods): { // ... - #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: "products")] - private $category; + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'products')] + private Category $category; public function getCategory(): ?Category { @@ -241,40 +213,6 @@ class that will hold these objects: .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/Category.php - namespace App\Entity; - - // ... - use Doctrine\Common\Collections\ArrayCollection; - use Doctrine\Common\Collections\Collection; - - class Category - { - // ... - - /** - * @ORM\OneToMany(targetEntity="App\Entity\Product", mappedBy="category") - */ - private $products; - - public function __construct() - { - $this->products = new ArrayCollection(); - } - - /** - * @return Collection|Product[] - */ - public function getProducts(): Collection - { - return $this->products; - } - - // addProduct() and removeProduct() were also added - } - .. code-block:: php-attributes // src/Entity/Category.php @@ -288,8 +226,8 @@ class that will hold these objects: { // ... - #[ORM\OneToMany(targetEntity: Product::class, mappedBy: "category")] - private $products; + #[ORM\OneToMany(targetEntity: Product::class, mappedBy: 'category')] + private Collection $products; public function __construct() { @@ -297,7 +235,7 @@ class that will hold these objects: } /** - * @return Collection|Product[] + * @return Collection */ public function getProducts(): Collection { @@ -379,14 +317,14 @@ Now you can see this new code in action! Imagine you're inside a controller:: // ... use App\Entity\Category; use App\Entity\Product; - use Doctrine\Persistence\ManagerRegistry; + use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; class ProductController extends AbstractController { #[Route('/product', name: 'product')] - public function index(ManagerRegistry $doctrine): Response + public function index(EntityManagerInterface $entityManager): Response { $category = new Category(); $category->setName('Computer Peripherals'); @@ -399,7 +337,6 @@ Now you can see this new code in action! Imagine you're inside a controller:: // relates this product to the category $product->setCategory($category); - $entityManager = $doctrine->getManager(); $entityManager->persist($category); $entityManager->persist($product); $entityManager->flush(); @@ -448,9 +385,9 @@ before. First, fetch a ``$product`` object and then access its related class ProductController extends AbstractController { - public function show(ManagerRegistry $doctrine, int $id): Response + public function show(ProductRepository $productRepository, int $id): Response { - $product = $doctrine->getRepository(Product::class)->find($id); + $product = $productRepository->find($id); // ... $categoryName = $product->getCategory()->getName(); @@ -484,9 +421,9 @@ direction:: // ... class ProductController extends AbstractController { - public function showProducts(ManagerRegistry $doctrine, int $id): Response + public function showProducts(CategoryRepository $categoryRepository, int $id): Response { - $category = $doctrine->getRepository(Category::class)->find($id); + $category = $categoryRepository->find($id); $products = $category->getProducts(); @@ -505,7 +442,7 @@ by adding JOINs. a "proxy" object in place of the true object. Look again at the above example:: - $product = $doctrine->getRepository(Product::class)->find($id); + $product = $productRepository->find($id); $category = $product->getCategory(); @@ -575,9 +512,9 @@ object and its related ``Category`` in one query:: // ... class ProductController extends AbstractController { - public function show(ManagerRegistry $doctrine, int $id): Response + public function show(ProductRepository $productRepository, int $id): Response { - $product = $doctrine->getRepository(Product::class)->findOneByIdJoinedToCategory($id); + $product = $productRepository->findOneByIdJoinedToCategory($id); $category = $product->getCategory(); @@ -598,7 +535,7 @@ To update a relationship in the database, you *must* set the relationship on the *owning* side. The owning side is always where the ``ManyToOne`` mapping is set (for a ``ManyToMany`` relation, you can choose which side is the owning side). -Does this means it's not possible to call ``$category->addProduct()`` or +Does this mean it's not possible to call ``$category->addProduct()`` or ``$category->removeProduct()`` to update the database? Actually, it *is* possible, thanks to some clever code that the ``make:entity`` command generated:: @@ -666,25 +603,14 @@ that behavior, use the `orphanRemoval`_ option inside ``Category``: .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/Category.php - - // ... - - /** - * @ORM\OneToMany(targetEntity="App\Entity\Product", mappedBy="category", orphanRemoval=true) - */ - private $products; - .. code-block:: php-attributes // src/Entity/Category.php // ... - #[ORM\OneToMany(targetEntity: Product::class, mappedBy: "category", orphanRemoval: true)] - private $products; + #[ORM\OneToMany(targetEntity: Product::class, mappedBy: 'category', orphanRemoval: true)] + private array $products; Thanks to this, if the ``Product`` is removed from the ``Category``, it will be removed from the database entirely. @@ -699,8 +625,8 @@ Doctrine's `Association Mapping Documentation`_. .. note:: - If you're using annotations, you'll need to prepend all annotations with - ``@ORM\`` (e.g. ``@ORM\OneToMany``), which is not reflected in Doctrine's + If you're using attributes, you'll need to prepend all attributes with + ``#[ORM\]`` (e.g. ``#[ORM\OneToMany]``), which is not reflected in Doctrine's documentation. .. _`Association Mapping Documentation`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/association-mapping.html diff --git a/doctrine/custom_dql_functions.rst b/doctrine/custom_dql_functions.rst index f615ad1fcd5..1b3aa4aa185 100644 --- a/doctrine/custom_dql_functions.rst +++ b/doctrine/custom_dql_functions.rst @@ -56,7 +56,7 @@ In Symfony, you can register your custom DQL functions as follows: use App\DQL\StringFunction; use Symfony\Config\DoctrineConfig; - return static function (DoctrineConfig $doctrine) { + return static function (DoctrineConfig $doctrine): void { $defaultDql = $doctrine->orm() ->entityManager('default') // ... @@ -123,7 +123,7 @@ In Symfony, you can register your custom DQL functions as follows: use App\DQL\DatetimeFunction; use Symfony\Config\DoctrineConfig; - return static function (DoctrineConfig $doctrine) { + return static function (DoctrineConfig $doctrine): void { $doctrine->orm() // ... ->entityManager('example_manager') diff --git a/doctrine/dbal.rst b/doctrine/dbal.rst index a0e0286d53e..4f47b61eb61 100644 --- a/doctrine/dbal.rst +++ b/doctrine/dbal.rst @@ -104,7 +104,7 @@ mapping types, read Doctrine's `Custom Mapping Types`_ section of their document use App\Type\CustomSecond; use Symfony\Config\DoctrineConfig; - return static function (DoctrineConfig $doctrine) { + return static function (DoctrineConfig $doctrine): void { $dbal = $doctrine->dbal(); $dbal->type('custom_first')->class(CustomFirst::class); $dbal->type('custom_second')->class(CustomSecond::class); @@ -153,7 +153,7 @@ mapping type: // config/packages/doctrine.php use Symfony\Config\DoctrineConfig; - return static function (DoctrineConfig $doctrine) { + return static function (DoctrineConfig $doctrine): void { $dbalDefault = $doctrine->dbal() ->connection('default'); $dbalDefault->mappingType('enum', 'string'); diff --git a/doctrine/events.rst b/doctrine/events.rst index 80506081fbe..dcd97126b7c 100644 --- a/doctrine/events.rst +++ b/doctrine/events.rst @@ -13,23 +13,20 @@ on other common tasks (e.g. ``loadClassMetadata``, ``onClear``). There are different ways to listen to these Doctrine events: -* **Lifecycle callbacks**, they are defined as public methods on the entity classes and - they are called when the events are triggered; -* **Lifecycle listeners and subscribers**, they are classes with callback - methods for one or more events and they are called for all entities; -* **Entity listeners**, they are similar to lifecycle listeners, but they are - called only for the entities of a certain class. - -These are the **drawbacks and advantages** of each one: - -* Callbacks have better performance because they only apply to a single entity - class, but you can't reuse the logic for different entities and they don't - have access to :doc:`Symfony services `; -* Lifecycle listeners and subscribers can reuse logic among different entities - and can access Symfony services but their performance is worse because they - are called for all entities; -* Entity listeners have the same advantages of lifecycle listeners and they have - better performance because they only apply to a single entity class. +* **Lifecycle callbacks**, they are defined as public methods on the entity classes. + They can't use services, so they are intended for **very simple logic** related + to a single entity; +* **Entity listeners**, they are defined as classes with callback methods for the + events you want to respond to. They can use services, but they are only called + for the entities of a certain class, so they are ideal for **complex event logic + related to a single entity**; +* **Lifecycle listeners**, they are similar to entity listeners but their event + methods are called for all entities, not only those of a certain type. They are + ideal to **share event logic between entities**. + +The performance of each type of listener depends on how many entities applies to: +lifecycle callbacks are faster than entity listeners, which in turn are faster +than lifecycle listeners. This article only explains the basics about Doctrine events when using them inside a Symfony application. Read the `official docs about Doctrine events`_ @@ -37,7 +34,7 @@ to learn everything about them. .. seealso:: - This article covers listeners and subscribers for Doctrine ORM. If you are + This article covers listeners for Doctrine ORM. If you are using ODM for MongoDB, read the `DoctrineMongoDBBundle documentation`_. Doctrine Lifecycle Callbacks @@ -50,33 +47,6 @@ define a callback for the ``prePersist`` Doctrine event: .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/Product.php - namespace App\Entity; - - use Doctrine\ORM\Mapping as ORM; - - // When using annotations, don't forget to add @ORM\HasLifecycleCallbacks() - // to the class of the entity where you define the callback - - /** - * @ORM\Entity() - * @ORM\HasLifecycleCallbacks() - */ - class Product - { - // ... - - /** - * @ORM\PrePersist - */ - public function setCreatedAtValue(): void - { - $this->createdAt = new \DateTimeImmutable(); - } - } - .. code-block:: php-attributes // src/Entity/Product.php @@ -132,160 +102,51 @@ define a callback for the ``prePersist`` Doctrine event: useful information such as the current entity manager (e.g. the ``preUpdate`` callback receives a ``PreUpdateEventArgs $event`` argument). -.. _doctrine-lifecycle-listener: - -Doctrine Lifecycle Listeners ----------------------------- - -Lifecycle listeners are defined as PHP classes that listen to a single Doctrine -event on all the application entities. For example, suppose that you want to -update some search index whenever a new entity is persisted in the database. To -do so, define a listener for the ``postPersist`` Doctrine event:: - - // src/EventListener/SearchIndexer.php - namespace App\EventListener; - - use App\Entity\Product; - use Doctrine\Persistence\Event\LifecycleEventArgs; - - class SearchIndexer - { - // the listener methods receive an argument which gives you access to - // both the entity object of the event and the entity manager itself - public function postPersist(LifecycleEventArgs $args): void - { - $entity = $args->getObject(); - - // if this listener only applies to certain entity types, - // add some code to check the entity type as early as possible - if (!$entity instanceof Product) { - return; - } - - $entityManager = $args->getObjectManager(); - // ... do something with the Product entity - } - } - -The next step is to enable the Doctrine listener in the Symfony application by -creating a new service for it and :doc:`tagging it ` -with the ``doctrine.event_listener`` tag: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - App\EventListener\SearchIndexer: - tags: - - - name: 'doctrine.event_listener' - # this is the only required option for the lifecycle listener tag - event: 'postPersist' - - # listeners can define their priority in case multiple subscribers or listeners are associated - # to the same event (default priority = 0; higher numbers = listener is run earlier) - priority: 500 - - # you can also restrict listeners to a specific Doctrine connection - connection: 'default' - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - use App\EventListener\SearchIndexer; - - return static function (ContainerConfigurator $container) { - $services = $container->services(); - - // listeners are applied by default to all Doctrine connections - $services->set(SearchIndexer::class) - ->tag('doctrine.event_listener', [ - // this is the only required option for the lifecycle listener tag - 'event' => 'postPersist', - - // listeners can define their priority in case multiple subscribers or listeners are associated - // to the same event (default priority = 0; higher numbers = listener is run earlier) - 'priority' => 500, - - # you can also restrict listeners to a specific Doctrine connection - 'connection' => 'default', - ]) - ; - }; - -.. tip:: - - Symfony loads (and instantiates) Doctrine listeners only when the related - Doctrine event is actually fired; whereas Doctrine subscribers are always - loaded (and instantiated) by Symfony, making them less performant. - -.. tip:: - - The value of the ``connection`` option can also be a - :ref:`configuration parameter `. - - .. versionadded:: 5.4 - - The feature to allow using configuration parameters in ``connection`` - was introduced in Symfony 5.4. - Doctrine Entity Listeners ------------------------- Entity listeners are defined as PHP classes that listen to a single Doctrine event on a single entity class. For example, suppose that you want to send some -notifications whenever a ``User`` entity is modified in the database. To do so, -define a listener for the ``postUpdate`` Doctrine event:: +notifications whenever a ``User`` entity is modified in the database. + +First, define a PHP class that handles the ``postUpdate`` Doctrine event:: // src/EventListener/UserChangedNotifier.php namespace App\EventListener; use App\Entity\User; - use Doctrine\Persistence\Event\LifecycleEventArgs; + use Doctrine\ORM\Event\PostUpdateEventArgs; class UserChangedNotifier { // the entity listener methods receive two arguments: // the entity instance and the lifecycle event - public function postUpdate(User $user, LifecycleEventArgs $event): void + public function postUpdate(User $user, PostUpdateEventArgs $event): void { // ... do something to notify the changes } } -The next step is to enable the Doctrine listener in the Symfony application by -creating a new service for it and :doc:`tagging it ` -with the ``doctrine.orm.entity_listener`` tag: +Then, add the ``#[AsEntityListener]`` attribute to the class to enable it as +a Doctrine entity listener in your application:: + + // src/EventListener/UserChangedNotifier.php + namespace App\EventListener; + + // ... + use App\Entity\User; + use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; + use Doctrine\ORM\Events; + + #[AsEntityListener(event: Events::postUpdate, method: 'postUpdate', entity: User::class)] + class UserChangedNotifier + { + // ... + } + +Alternatively, if you prefer to not use PHP attributes, you must +configure a service for the entity listener and :doc:`tag it ` +with the ``doctrine.orm.entity_listener`` tag as follows: .. configuration-block:: @@ -357,7 +218,7 @@ with the ``doctrine.orm.entity_listener`` tag: use App\Entity\User; use App\EventListener\UserChangedNotifier; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $services = $container->services(); $services->set(UserChangedNotifier::class) @@ -382,94 +243,103 @@ with the ``doctrine.orm.entity_listener`` tag: ; }; -Doctrine Lifecycle Subscribers ------------------------------- +.. _doctrine-lifecycle-listener: + +Doctrine Lifecycle Listeners +---------------------------- -Lifecycle subscribers are defined as PHP classes that implement the -``Doctrine\Common\EventSubscriber`` interface and which listen to one or more -Doctrine events on all the application entities. For example, suppose that you -want to log all the database activity. To do so, define a subscriber for the -``postPersist``, ``postRemove`` and ``postUpdate`` Doctrine events:: +Lifecycle listeners are defined as PHP classes that listen to a single Doctrine +event on all the application entities. For example, suppose that you want to +update some search index whenever a new entity is persisted in the database. To +do so, define a listener for the ``postPersist`` Doctrine event:: - // src/EventListener/DatabaseActivitySubscriber.php + // src/EventListener/SearchIndexer.php namespace App\EventListener; use App\Entity\Product; - use Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface; - use Doctrine\ORM\Events; - use Doctrine\Persistence\Event\LifecycleEventArgs; + use Doctrine\ORM\Event\PostPersistEventArgs; - class DatabaseActivitySubscriber implements EventSubscriberInterface + class SearchIndexer { - // this method can only return the event names; you cannot define a - // custom method name to execute when each event triggers - public function getSubscribedEvents(): array - { - return [ - Events::postPersist, - Events::postRemove, - Events::postUpdate, - ]; - } - - // callback methods must be called exactly like the events they listen to; - // they receive an argument of type LifecycleEventArgs, which gives you access - // to both the entity object of the event and the entity manager itself - public function postPersist(LifecycleEventArgs $args): void - { - $this->logActivity('persist', $args); - } - - public function postRemove(LifecycleEventArgs $args): void - { - $this->logActivity('remove', $args); - } - - public function postUpdate(LifecycleEventArgs $args): void - { - $this->logActivity('update', $args); - } - - private function logActivity(string $action, LifecycleEventArgs $args): void + // the listener methods receive an argument which gives you access to + // both the entity object of the event and the entity manager itself + public function postPersist(PostPersistEventArgs $args): void { $entity = $args->getObject(); - // if this subscriber only applies to certain entity types, + // if this listener only applies to certain entity types, // add some code to check the entity type as early as possible if (!$entity instanceof Product) { return; } - // ... get the entity information and log it somehow + $entityManager = $args->getObjectManager(); + // ... do something with the Product entity } } -If you're using the :ref:`default services.yaml configuration ` -and DoctrineBundle 2.1 (released May 25, 2020) or newer, this example will already -work! Otherwise, :ref:`create a service ` for this -subscriber and :doc:`tag it ` with ``doctrine.event_subscriber``. +.. note:: + + In previous Doctrine versions, instead of ``PostPersistEventArgs``, you had + to use ``LifecycleEventArgs``, which was deprecated in Doctrine ORM 2.14. + +Then, add the ``#[AsDoctrineListener]`` attribute to the class to enable it as +a Doctrine listener in your application:: + + // src/EventListener/SearchIndexer.php + namespace App\EventListener; -If you need to configure some option of the subscriber (e.g. its priority or -Doctrine connection to use) you must do that in the manual service configuration: + use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; + use Doctrine\ORM\Events; + + #[AsDoctrineListener(event: Events::postPersist, priority: 500, connection: 'default')] + class SearchIndexer + { + // ... + } + +Alternatively, if you prefer to not use PHP attributes, you must enable the +listener in the Symfony application by creating a new service for it and +:doc:`tagging it ` with the ``doctrine.event_listener`` tag: .. configuration-block:: + .. code-block:: php-attributes + + // src/EventListener/SearchIndexer.php + namespace App\EventListener; + + use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; + use Doctrine\ORM\Event\PostPersistEventArgs; + + #[AsDoctrineListener('postPersist'/*, 500, 'default'*/)] + class SearchIndexer + { + public function postPersist(PostPersistEventArgs $event): void + { + // ... + } + } + .. code-block:: yaml # config/services.yaml services: # ... - App\EventListener\DatabaseActivitySubscriber: + App\EventListener\SearchIndexer: tags: - - name: 'doctrine.event_subscriber' + - + name: 'doctrine.event_listener' + # this is the only required option for the lifecycle listener tag + event: 'postPersist' - # subscribers can define their priority in case multiple subscribers or listeners are associated - # to the same event (default priority = 0; higher numbers = listener is run earlier) - priority: 500 + # listeners can define their priority in case listeners are associated + # to the same event (default priority = 0; higher numbers = listener is run earlier) + priority: 500 - # you can also restrict listeners to a specific Doctrine connection - connection: 'default' + # you can also restrict listeners to a specific Doctrine connection + connection: 'default' .. code-block:: xml @@ -481,12 +351,16 @@ Doctrine connection to use) you must do that in the manual service configuration - - + + @@ -496,34 +370,38 @@ Doctrine connection to use) you must do that in the manual service configuration // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - use App\EventListener\DatabaseActivitySubscriber; + use App\EventListener\SearchIndexer; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $services = $container->services(); - $services->set(DatabaseActivitySubscriber::class) - ->tag('doctrine.event_subscriber'[ - // subscribers can define their priority in case multiple subscribers or listeners are associated + // listeners are applied by default to all Doctrine connections + $services->set(SearchIndexer::class) + ->tag('doctrine.event_listener', [ + // this is the only required option for the lifecycle listener tag + 'event' => 'postPersist', + + // listeners can define their priority in case multiple listeners are associated // to the same event (default priority = 0; higher numbers = listener is run earlier) 'priority' => 500, - // you can also restrict listeners to a specific Doctrine connection + # you can also restrict listeners to a specific Doctrine connection 'connection' => 'default', ]) ; }; -.. versionadded:: 5.3 +.. versionadded:: 2.8.0 - Subscriber priority was introduced in Symfony 5.3. + The `AsDoctrineListener`_ attribute was introduced in DoctrineBundle 2.8.0. .. tip:: - Symfony loads (and instantiates) Doctrine subscribers whenever the - application executes; whereas Doctrine listeners are only loaded when the - related event is actually fired, making them more performant. + The value of the ``connection`` option can also be a + :ref:`configuration parameter `. .. _`Doctrine`: https://www.doctrine-project.org/ .. _`lifecycle events`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/events.html#lifecycle-events .. _`official docs about Doctrine events`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/events.html .. _`DoctrineMongoDBBundle documentation`: https://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html +.. _`AsDoctrineListener`: https://github.com/doctrine/DoctrineBundle/blob/2.12.x/src/Attribute/AsDoctrineListener.php diff --git a/doctrine/multiple_entity_managers.rst b/doctrine/multiple_entity_managers.rst index 34a33b22cac..014d9e4dccb 100644 --- a/doctrine/multiple_entity_managers.rst +++ b/doctrine/multiple_entity_managers.rst @@ -43,7 +43,6 @@ The following configuration code shows how you can configure two entity managers mappings: Main: is_bundle: false - type: annotation dir: '%kernel.project_dir%/src/Entity/Main' prefix: 'App\Entity\Main' alias: Main @@ -52,7 +51,6 @@ The following configuration code shows how you can configure two entity managers mappings: Customer: is_bundle: false - type: annotation dir: '%kernel.project_dir%/src/Entity/Customer' prefix: 'App\Entity\Customer' alias: Customer @@ -85,7 +83,6 @@ The following configuration code shows how you can configure two entity managers dbal() ->connection('default') @@ -120,14 +116,13 @@ The following configuration code shows how you can configure two entity managers ->connection('customer') ->url(env('CUSTOMER_DATABASE_URL')->resolve()); $doctrine->dbal()->defaultConnection('default'); - + // Entity Managers: $doctrine->orm()->defaultEntityManager('default'); $defaultEntityManager = $doctrine->orm()->entityManager('default'); $defaultEntityManager->connection('default'); $defaultEntityManager->mapping('Main') ->isBundle(false) - ->type('annotation') ->dir('%kernel.project_dir%/src/Entity/Main') ->prefix('App\Entity\Main') ->alias('Main'); @@ -135,7 +130,6 @@ The following configuration code shows how you can configure two entity managers $customerEntityManager->connection('customer'); $customerEntityManager->mapping('Customer') ->isBundle(false) - ->type('annotation') ->dir('%kernel.project_dir%/src/Entity/Customer') ->prefix('App\Entity\Customer') ->alias('Customer') diff --git a/doctrine/registration_form.rst b/doctrine/registration_form.rst deleted file mode 100644 index 7063b7157a4..00000000000 --- a/doctrine/registration_form.rst +++ /dev/null @@ -1,15 +0,0 @@ -How to Implement a Registration Form -==================================== - -This article has been removed because it only explained things that are -already explained in other articles. Specifically, to implement a registration -form you must: - -#. :ref:`Define a class to represent users `; -#. :doc:`Create a form ` to ask for the registration information (you can - generate this with the ``make:registration-form`` command provided by the `MakerBundle`_); -#. Create :doc:`a controller ` to :ref:`process the form `; -#. :ref:`Protect some parts of your application ` so that - only registered users can access to them. - -.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html diff --git a/doctrine/resolve_target_entity.rst b/doctrine/resolve_target_entity.rst index a3b837fe076..5ae6475a957 100644 --- a/doctrine/resolve_target_entity.rst +++ b/doctrine/resolve_target_entity.rst @@ -42,10 +42,8 @@ A Customer entity:: use App\Model\InvoiceSubjectInterface; use Doctrine\ORM\Mapping as ORM; - /** - * @ORM\Entity - * @ORM\Table(name="customer") - */ + #[ORM\Entity] + #[ORM\Table(name: 'customer')] class Customer extends BaseCustomer implements InvoiceSubjectInterface { // In this example, any methods defined in the InvoiceSubjectInterface @@ -62,17 +60,13 @@ An Invoice entity:: /** * Represents an Invoice. - * - * @ORM\Entity - * @ORM\Table(name="invoice") */ + #[ORM\Entity] + #[ORM\Table(name: 'invoice')] class Invoice { - /** - * @ORM\ManyToOne(targetEntity="App\Model\InvoiceSubjectInterface") - * @var InvoiceSubjectInterface - */ - protected $subject; + #[ORM\ManyToOne(targetEntity: InvoiceSubjectInterface::class)] + protected InvoiceSubjectInterface $subject; } An InvoiceSubjectInterface:: @@ -137,7 +131,7 @@ about the replacement: use App\Model\InvoiceSubjectInterface; use Symfony\Config\DoctrineConfig; - return static function (DoctrineConfig $doctrine) { + return static function (DoctrineConfig $doctrine): void { $orm = $doctrine->orm(); // ... $orm->resolveTargetEntity(InvoiceSubjectInterface::class, Customer::class); diff --git a/email.rst b/email.rst deleted file mode 100644 index 8cb879ad4ab..00000000000 --- a/email.rst +++ /dev/null @@ -1,10 +0,0 @@ -Swift Mailer -============ - -.. caution:: - - The Swift Mailer project is not supported since November 2021 and its - integration with Symfony was removed in Symfony 6.0. - - Use the :doc:`Symfony Mailer ` component, which was introduced in - Symfony 4.3 as a modern replacement of Swift Mailer. diff --git a/emoji.rst b/emoji.rst new file mode 100644 index 00000000000..551497f0c76 --- /dev/null +++ b/emoji.rst @@ -0,0 +1,173 @@ +Working with Emojis +=================== + +.. versionadded:: 7.1 + + The emoji component was introduced in Symfony 7.1. + +Symfony provides several utilities to work with emoji characters and sequences +from the `Unicode CLDR dataset`_. They are available via the Emoji component, +which you must first install in your application: + +.. _installation: + +.. code-block:: terminal + + $ composer require symfony/emoji + +.. include:: /components/require_autoload.rst.inc + +The data needed to store the transliteration of all emojis (~5,000) into all +languages take a considerable disk space. + +If you need to save disk space (e.g. because you deploy to some service with tight +size constraints), run this command (e.g. as an automated script after ``composer install``) +to compress the internal Symfony emoji data files using the PHP ``zlib`` extension: + +.. code-block:: terminal + + # adjust the path to the 'compress' binary based on your application installation + $ php ./vendor/symfony/emoji/Resources/bin/compress + +.. _emoji-transliteration: + +Emoji Transliteration +--------------------- + +The ``EmojiTransliterator`` class offers a way to translate emojis into their +textual representation in all languages based on the `Unicode CLDR dataset`_:: + + use Symfony\Component\Emoji\EmojiTransliterator; + + // Describe emojis in English + $transliterator = EmojiTransliterator::create('en'); + $transliterator->transliterate('Menus with 🍕 or 🍝'); + // => 'Menus with pizza or spaghetti' + + // Describe emojis in Ukrainian + $transliterator = EmojiTransliterator::create('uk'); + $transliterator->transliterate('Menus with 🍕 or 🍝'); + // => 'Menus with піца or спагеті' + +.. tip:: + + When using the :ref:`slugger ` from the String component, + you can combine it with the ``EmojiTransliterator`` to :ref:`slugify emojis `. + +Transliterating Emoji Text Short Codes +-------------------------------------- + +Services like GitHub and Slack allows to include emojis in your messages using +text short codes (e.g. you can add the ``:+1:`` code to render the 👍 emoji). + +Symfony also provides a feature to transliterate emojis into short codes and vice +versa. The short codes are slightly different on each service, so you must pass +the name of the service as an argument when creating the transliterator. + +GitHub Emoji Short Codes Transliteration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Convert emojis to GitHub short codes with the ``emoji-github`` locale:: + + $transliterator = EmojiTransliterator::create('emoji-github'); + $transliterator->transliterate('Teenage 🐢 really love 🍕'); + // => 'Teenage :turtle: really love :pizza:' + +Convert GitHub short codes to emojis with the ``github-emoji`` locale:: + + $transliterator = EmojiTransliterator::create('github-emoji'); + $transliterator->transliterate('Teenage :turtle: really love :pizza:'); + // => 'Teenage 🐢 really love 🍕' + +Gitlab Emoji Short Codes Transliteration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Convert emojis to Gitlab short codes with the ``emoji-gitlab`` locale:: + + $transliterator = EmojiTransliterator::create('emoji-gitlab'); + $transliterator->transliterate('Breakfast with 🥝 or 🥛'); + // => 'Breakfast with :kiwi: or :milk:' + +Convert Gitlab short codes to emojis with the ``gitlab-emoji`` locale:: + + $transliterator = EmojiTransliterator::create('gitlab-emoji'); + $transliterator->transliterate('Breakfast with :kiwi: or :milk:'); + // => 'Breakfast with 🥝 or 🥛' + +Slack Emoji Short Codes Transliteration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Convert emojis to Slack short codes with the ``emoji-slack`` locale:: + + $transliterator = EmojiTransliterator::create('emoji-slack'); + $transliterator->transliterate('Menus with 🥗 or 🧆'); + // => 'Menus with :green_salad: or :falafel:' + +Convert Slack short codes to emojis with the ``slack-emoji`` locale:: + + $transliterator = EmojiTransliterator::create('slack-emoji'); + $transliterator->transliterate('Menus with :green_salad: or :falafel:'); + // => 'Menus with 🥗 or 🧆' + +.. _text-emoji: + +Universal Emoji Short Codes Transliteration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you don't know which service was used to generate the short codes, you can use +the ``text-emoji`` locale, which combines all codes from all services:: + + $transliterator = EmojiTransliterator::create('text-emoji'); + + // Github short codes + $transliterator->transliterate('Breakfast with :kiwi-fruit: or :milk-glass:'); + // Gitlab short codes + $transliterator->transliterate('Breakfast with :kiwi: or :milk:'); + // Slack short codes + $transliterator->transliterate('Breakfast with :kiwifruit: or :glass-of-milk:'); + + // all the above examples produce the same result: + // => 'Breakfast with 🥝 or 🥛' + +You can convert emojis to short codes with the ``emoji-text`` locale:: + + $transliterator = EmojiTransliterator::create('emoji-text'); + $transliterator->transliterate('Breakfast with 🥝 or 🥛'); + // => 'Breakfast with :kiwifruit: or :milk-glass: + +Inverse Emoji Transliteration +----------------------------- + +Given the textual representation of an emoji, you can reverse it back to get the +actual emoji thanks to the :ref:`emojify filter `: + +.. code-block:: twig + + {{ 'I like :kiwi-fruit:'|emojify }} {# renders: I like 🥝 #} + {{ 'I like :kiwi:'|emojify }} {# renders: I like 🥝 #} + {{ 'I like :kiwifruit:'|emojify }} {# renders: I like 🥝 #} + +By default, ``emojify`` uses the :ref:`text catalog `, which +merges the emoji text codes of all services. If you prefer, you can select a +specific catalog to use: + +.. code-block:: twig + + {{ 'I :green-heart: this'|emojify }} {# renders: I 💚 this #} + {{ ':green_salad: is nice'|emojify('slack') }} {# renders: 🥗 is nice #} + {{ 'My :turtle: has no name yet'|emojify('github') }} {# renders: My 🐢 has no name yet #} + {{ ':kiwi: is a great fruit'|emojify('gitlab') }} {# renders: 🥝 is a great fruit #} + +Removing Emojis +--------------- + +The ``EmojiTransliterator`` can also be used to remove all emojis from a string, +via the special ``strip`` locale:: + + use Symfony\Component\Emoji\EmojiTransliterator; + + $transliterator = EmojiTransliterator::create('strip'); + $transliterator->transliterate('🎉Hey!🥳 🎁Happy Birthday!🎁'); + // => 'Hey! Happy Birthday!' + +.. _`Unicode CLDR dataset`: https://github.com/unicode-org/cldr diff --git a/event_dispatcher.rst b/event_dispatcher.rst index ab3428f6cb0..6787cba2d83 100644 --- a/event_dispatcher.rst +++ b/event_dispatcher.rst @@ -91,7 +91,7 @@ notify Symfony that it is an event listener by using a special "tag": use App\EventListener\ExceptionListener; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(ExceptionListener::class) @@ -266,17 +266,17 @@ listen to the same ``kernel.exception`` event:: ]; } - public function processException(ExceptionEvent $event) + public function processException(ExceptionEvent $event): void { // ... } - public function logException(ExceptionEvent $event) + public function logException(ExceptionEvent $event): void { // ... } - public function notifyException(ExceptionEvent $event) + public function notifyException(ExceptionEvent $event): void { // ... } @@ -309,10 +309,8 @@ a "main" request or a "sub request":: class RequestListener { - public function onKernelRequest(RequestEvent $event) + public function onKernelRequest(RequestEvent $event): void { - // The isMainRequest() method was introduced in Symfony 5.3. - // In previous versions it was called isMasterRequest() if (!$event->isMainRequest()) { // don't do anything if it's not the main request return; @@ -362,7 +360,7 @@ name (FQCN) of the corresponding event class:: ]; } - public function onKernelRequest(RequestEvent $event) + public function onKernelRequest(RequestEvent $event): void { // ... } @@ -386,7 +384,7 @@ compiler pass ``AddEventAliasesPass``:: class Kernel extends BaseKernel { - protected function build(ContainerBuilder $container) + protected function build(ContainerBuilder $container): void { $container->addCompilerPass(new AddEventAliasesPass([ MyCustomEvent::class => 'my_custom_event', @@ -422,10 +420,6 @@ or can get everything which partial matches the event name: $ php bin/console debug:event-dispatcher kernel // matches "kernel.exception", "kernel.response" etc. $ php bin/console debug:event-dispatcher Security // matches "Symfony\Component\Security\Http\Event\CheckPassportEvent" -.. versionadded:: 5.3 - - The ability to match partial event names was introduced in Symfony 5.3. - The :doc:`security ` system uses an event dispatcher per firewall. Use the ``--dispatcher`` option to get the registered listeners for a particular event dispatcher: @@ -434,10 +428,6 @@ for a particular event dispatcher: $ php bin/console debug:event-dispatcher --dispatcher=security.event_dispatcher.main -.. versionadded:: 5.3 - - The ``dispatcher`` option was introduced in Symfony 5.3. - .. _event-dispatcher-before-after-filters: How to Set Up Before and After Filters @@ -532,11 +522,12 @@ A controller that implements this interface looks like this:: use App\Controller\TokenAuthenticatedController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; class FooController extends AbstractController implements TokenAuthenticatedController { // An action that needs authentication - public function bar() + public function bar(): Response { // ... } @@ -560,14 +551,12 @@ event subscribers, you can learn more about them at :doc:`/event_dispatcher`:: class TokenSubscriber implements EventSubscriberInterface { - private $tokens; - - public function __construct($tokens) - { - $this->tokens = $tokens; + public function __construct( + private array $tokens + ) { } - public function onKernelController(ControllerEvent $event) + public function onKernelController(ControllerEvent $event): void { $controller = $event->getController(); @@ -585,7 +574,7 @@ event subscribers, you can learn more about them at :doc:`/event_dispatcher`:: } } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ KernelEvents::CONTROLLER => 'onKernelController', @@ -624,7 +613,7 @@ For example, take the ``TokenSubscriber`` from the previous example and first record the authentication token inside the request attributes. This will serve as a basic flag that this request underwent token authentication:: - public function onKernelController(ControllerEvent $event) + public function onKernelController(ControllerEvent $event): void { // ... @@ -646,7 +635,7 @@ header on the response if it's found:: // add the new use statement at the top of your file use Symfony\Component\HttpKernel\Event\ResponseEvent; - public function onKernelResponse(ResponseEvent $event) + public function onKernelResponse(ResponseEvent $event): void { // check to see if onKernelController marked this as a token "auth'ed" request if (!$token = $event->getRequest()->attributes->get('auth_token')) { @@ -660,7 +649,7 @@ header on the response if it's found:: $response->headers->set('X-CONTENT-HASH', $hash); } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ KernelEvents::CONTROLLER => 'onKernelController', @@ -688,7 +677,7 @@ end of the method:: { // ... - public function send($subject, $message) + public function send(string $subject, string $message): mixed { // dispatch an event before the method $event = new BeforeSendMailEvent($subject, $message); @@ -725,31 +714,28 @@ this:: class BeforeSendMailEvent extends Event { - private $subject; - private $message; - - public function __construct($subject, $message) - { - $this->subject = $subject; - $this->message = $message; + public function __construct( + private string $subject, + private string $message, + ) { } - public function getSubject() + public function getSubject(): string { return $this->subject; } - public function setSubject($subject) + public function setSubject(string $subject): string { $this->subject = $subject; } - public function getMessage() + public function getMessage(): string { return $this->message; } - public function setMessage($message) + public function setMessage(string $message): void { $this->message = $message; } @@ -764,19 +750,17 @@ And the ``AfterSendMailEvent`` even like this:: class AfterSendMailEvent extends Event { - private $returnValue; - - public function __construct($returnValue) - { - $this->returnValue = $returnValue; + public function __construct( + private mixed $returnValue, + ) { } - public function getReturnValue() + public function getReturnValue(): mixed { return $this->returnValue; } - public function setReturnValue($returnValue) + public function setReturnValue(mixed $returnValue): void { $this->returnValue = $returnValue; } @@ -796,7 +780,7 @@ could listen to the ``mailer.post_send`` event and change the method's return va class MailPostSendSubscriber implements EventSubscriberInterface { - public function onMailerPostSend(AfterSendMailEvent $event) + public function onMailerPostSend(AfterSendMailEvent $event): void { $returnValue = $event->getReturnValue(); // modify the original $returnValue value @@ -804,7 +788,7 @@ could listen to the ``mailer.post_send`` event and change the method's return va $event->setReturnValue($returnValue); } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ 'mailer.post_send' => 'onMailerPostSend', diff --git a/form/bootstrap4.rst b/form/bootstrap4.rst index bbcd0819369..eef016aa58a 100644 --- a/form/bootstrap4.rst +++ b/form/bootstrap4.rst @@ -57,7 +57,7 @@ configuration: // config/packages/twig.php use Symfony\Config\TwigConfig; - return static function (TwigConfig $twig) { + return static function (TwigConfig $twig): void { $twig->formThemes(['bootstrap_4_layout.html.twig']); // ... diff --git a/form/bootstrap5.rst b/form/bootstrap5.rst index 5647e003593..400747bba12 100644 --- a/form/bootstrap5.rst +++ b/form/bootstrap5.rst @@ -1,10 +1,6 @@ Bootstrap 5 Form Theme ====================== -.. versionadded:: 5.3 - - The Bootstrap 5 Form Theme was introduced in Symfony 5.3. - Symfony provides several ways of integrating Bootstrap into your application. The most straightforward way is to add the required ```` and `` The major benefit of submitting the whole form to just extract the updated diff --git a/form/embedded.rst b/form/embedded.rst index c43f8a7a592..dd163235f03 100644 --- a/form/embedded.rst +++ b/form/embedded.rst @@ -21,10 +21,8 @@ creating the ``Category`` class:: class Category { - /** - * @Assert\NotBlank - */ - public $name; + #[Assert\NotBlank] + public string $name; } Next, add a new ``category`` property to the ``Task`` class:: @@ -35,11 +33,9 @@ Next, add a new ``category`` property to the ``Task`` class:: { // ... - /** - * @Assert\Type(type="App\Entity\Category") - * @Assert\Valid - */ - protected $category; + #[Assert\Type(type: Category::class)] + #[Assert\Valid] + protected ?Category $category = null; // ... @@ -48,7 +44,7 @@ Next, add a new ``category`` property to the ``Task`` class:: return $this->category; } - public function setCategory(?Category $category) + public function setCategory(?Category $category): void { $this->category = $category; } diff --git a/form/events.rst b/form/events.rst index 44cb6cc0074..745df2df453 100644 --- a/form/events.rst +++ b/form/events.rst @@ -16,7 +16,7 @@ register an event listener to the ``FormEvents::PRE_SUBMIT`` event as follows:: use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; - $listener = function (FormEvent $event) { + $listener = function (FormEvent $event): void { // ... }; @@ -274,16 +274,16 @@ method of the ``FormFactory``:: // ... + use Symfony\Component\Form\Event\PreSubmitEvent; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\TextType; - use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; $form = $formFactory->createBuilder() ->add('username', TextType::class) ->add('showEmail', CheckboxType::class) - ->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { + ->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event): void { $user = $event->getData(); $form = $event->getForm(); @@ -311,9 +311,9 @@ callback for better readability:: // src/Form/SubscriptionType.php namespace App\Form; + use Symfony\Component\Form\Event\PreSetDataEvent; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\TextType; - use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; // ... @@ -331,7 +331,7 @@ callback for better readability:: ; } - public function onPreSetData(FormEvent $event): void + public function onPreSetData(PreSetDataEvent $event): void { // ... } @@ -352,8 +352,9 @@ Consider the following example of a form event subscriber:: namespace App\Form\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\Form\Event\PreSetDataEvent; + use Symfony\Component\Form\Event\PreSubmitEvent; use Symfony\Component\Form\Extension\Core\Type\EmailType; - use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; class AddEmailFieldListener implements EventSubscriberInterface @@ -366,7 +367,7 @@ Consider the following example of a form event subscriber:: ]; } - public function onPreSetData(FormEvent $event): void + public function onPreSetData(PreSetDataEvent $event): void { $user = $event->getData(); $form = $event->getForm(); @@ -378,7 +379,7 @@ Consider the following example of a form event subscriber:: } } - public function onPreSubmit(FormEvent $event): void + public function onPreSubmit(PreSubmitEvent $event): void { $user = $event->getData(); $form = $event->getForm(); diff --git a/form/form_collections.rst b/form/form_collections.rst index b3caff2f436..f0ad76a8a61 100644 --- a/form/form_collections.rst +++ b/form/form_collections.rst @@ -11,13 +11,12 @@ Let's start by creating a ``Task`` entity:: // src/Entity/Task.php namespace App\Entity; - use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; class Task { - protected $description; - protected $tags; + protected string $description; + protected Collection $tags; public function __construct() { @@ -53,7 +52,7 @@ objects:: class Tag { - private $name; + private string $name; public function getName(): string { @@ -161,7 +160,7 @@ In your controller, you'll create a new form from the ``TaskType``:: // ... do your form processing, like saving the Task and Tag entities } - return $this->renderForm('task/new.html.twig', [ + return $this->render('task/new.html.twig', [ 'form' => $form, ]); } @@ -241,7 +240,11 @@ it will receive an *unknown* number of tags. Otherwise, you'll see a The ``allow_add`` option also makes a ``prototype`` variable available to you. This "prototype" is a little "template" that contains all the HTML needed to -dynamically create any new "tag" forms with JavaScript. To render the prototype, add +dynamically create any new "tag" forms with JavaScript. + +Let's start with plain JavaScript (Vanilla JS) – if you're using Stimulus, see below. + +To render the prototype, add the following ``data-prototype`` attribute to the existing ``
    `` in your template: @@ -311,7 +314,7 @@ you'll replace with a unique, incrementing number (e.g. ``task[tags][3][name]``) .. code-block:: javascript - const addFormToCollection = (e) => { + function addFormToCollection(e) { const collectionHolder = document.querySelector('.' + e.currentTarget.dataset.collectionHolderClass); const item = document.createElement('li'); @@ -337,6 +340,49 @@ into new ``Tag`` objects and added to the ``tags`` property of the ``Task`` obje You can find a working example in this `JSFiddle`_. +JavaScript with Stimulus +~~~~~~~~~~~~~~~~~~~~~~~~ + +If you're using `Stimulus`_, wrap everything in a ``
    ``: + +.. code-block:: html+twig + +
    +
      + +
      + +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:: @@ -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..3f3cd0bbc89 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) }} @@ -129,10 +129,6 @@ fields, so you no longer have to deal with form themes: {% endfor %} -.. versionadded:: 5.2 - - The ``field_*()`` helpers were introduced in Symfony 5.2. - Form Rendering Variables ------------------------ diff --git a/form/form_themes.rst b/form/form_themes.rst index 5f462ce4bbb..eb6f6f2ae22 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', ]); @@ -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..19b14b27bcd 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``, diff --git a/form/type_guesser.rst b/form/type_guesser.rst index 29c9cea0e21..111f1b77986 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 @@ -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..bf57e6d1afc 100644 --- a/form/unit_testing.rst +++ b/form/unit_testing.rst @@ -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 @@ -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 $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..589f8a4739e 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 diff --git a/forms.rst b/forms.rst index 8b8a0534201..1e891ab23ef 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 -------------------------- @@ -1083,3 +1016,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..05f7e6c69df 100644 --- a/frontend.rst +++ b/frontend.rst @@ -1,107 +1,139 @@ -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. -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. +* :ref:`Webpack Encore ` is built with `Node.js`_ + on top of `Webpack`_. -.. tip:: +================================ ================================== ========== + 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 yes +Removes comments from CSS no no +Versioned assets always optional +Can update 3rd party packages yes no :ref:`[2] ` +================================ ================================== ========== - 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-1: -.. _encore-toc: +**[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 Documentation --------------------- +.. _ux-note-2: -Getting Started -............... +**[2]** If you use ``npm``, there are update checkers available (e.g. ``npm-check``). -* :doc:`Installation ` -* :doc:`Using Webpack Encore ` +.. _frontend-asset-mapper: -Adding more Features -.................... +AssetMapper (Recommended) +~~~~~~~~~~~~~~~~~~~~~~~~~ -* :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) ` +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. -Optimizing -.......... +:doc:`Read the AssetMapper Documentation ` -* :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` +.. _frontend-webpack-encore: -Guides -...... +Webpack Encore +~~~~~~~~~~~~~~ -* :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 ` +.. screencast:: -Issues & Questions -.................. + 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. -* :doc:`FAQ & Common Issues ` +:doc:`Read the Encore Documentation ` -Full API -........ +Switch from AssetMapper +^^^^^^^^^^^^^^^^^^^^^^^ -* `Full API`_ +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. -Symfony UX Components ---------------------- +.. code-block:: terminal -.. include:: /frontend/_ux-libraries.rst.inc + # 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) +----------------------------------------------------- + +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/ 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* - `` + + + + +The :class:`Symfony\\Component\\Security\\Http\\Attribute\\IsCsrfTokenValid` +attribute also accepts an :class:`Symfony\\Component\\ExpressionLanguage\\Expression` +object evaluated to the id:: + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid; + // ... + + #[IsCsrfTokenValid(new Expression('"delete-item-" ~ args["post"].getId()'), tokenKey: 'token')] + public function delete(Post $post): Response + { + // ... do something, like deleting an object + } + +.. versionadded:: 7.1 + + The :class:`Symfony\\Component\\Security\\Http\\Attribute\\IsCsrfTokenValid` + attribute was introduced in Symfony 7.1. + CSRF Tokens and Compression Side-Channel Attacks ------------------------------------------------ @@ -222,10 +302,6 @@ targeted parts of the plaintext. To mitigate these attacks, and prevent an attacker from guessing the CSRF tokens, a random mask is prepended to the token and used to scramble it. -.. versionadded:: 5.3 - - The randomization of tokens was introduced in Symfony 5.3 - .. _`Cross-site request forgery`: https://en.wikipedia.org/wiki/Cross-site_request_forgery .. _`BREACH`: https://en.wikipedia.org/wiki/BREACH .. _`CRIME`: https://en.wikipedia.org/wiki/CRIME diff --git a/security/custom_authenticator.rst b/security/custom_authenticator.rst index dcddbc03444..689df6108e3 100644 --- a/security/custom_authenticator.rst +++ b/security/custom_authenticator.rst @@ -91,7 +91,6 @@ The authenticator can be enabled using the ``custom_authenticators`` setting: # config/packages/security.yaml security: - enable_authenticator_manager: true # ... firewalls: @@ -111,7 +110,7 @@ The authenticator can be enabled using the ``custom_authenticators`` setting: http://symfony.com/schema/dic/security https://symfony.com/schema/dic/security/security-1.0.xsd"> - + @@ -126,7 +125,7 @@ The authenticator can be enabled using the ``custom_authenticators`` setting: use App\Security\ApiKeyAuthenticator; use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { $security->enableAuthenticatorManager(true); // .... @@ -135,20 +134,12 @@ The authenticator can be enabled using the ``custom_authenticators`` setting: ; }; -.. deprecated:: 5.4 - - If you have registered multiple user providers, you must set the - ``provider`` key to one of the configured providers, even if your - custom authenticators don't use it. Not doing so is deprecated in Symfony 5.4. - -.. versionadded:: 5.2 - - Starting with Symfony 5.2, the custom authenticator is automatically - registered as an entry point if it implements ``AuthenticationEntryPointInterface``. +.. tip:: - Prior to 5.2, you had to configure the entry point separately using the - ``entry_point`` option. Read :doc:`/security/entry_point` for more - information. + You may want your authenticator to implement + ``AuthenticationEntryPointInterface``. This defines the response sent + to users to start authentication (e.g. when they visit a protected + page). Read more about it in :doc:`/security/entry_point`. The ``authenticate()`` method is the most important method of the authenticator. Its job is to extract credentials (e.g. username & @@ -204,11 +195,6 @@ can define what happens in these cases: Security Passports ------------------ -.. versionadded:: 5.2 - - The ``UserBadge`` was introduced in Symfony 5.2. Prior to 5.2, the user - instance was provided directly to the passport. - A passport is an object that contains the user that will be authenticated as well as other pieces of information, like whether a password should be checked or if "remember me" functionality should be enabled. @@ -228,6 +214,11 @@ using :ref:`the user provider `:: // ... $passport = new Passport(new UserBadge($email), $credentials); +.. note:: + + The maximum length allowed for the user identifier is 4096 characters to + prevent `session storage flooding`_ attacks. + .. note:: You can optionally pass a user loader as second argument to the @@ -243,11 +234,9 @@ using :ref:`the user provider `:: class CustomAuthenticator extends AbstractAuthenticator { - private $userRepository; - - public function __construct(UserRepository $userRepository) - { - $this->userRepository = $userRepository; + public function __construct( + private UserRepository $userRepository, + ) { } public function authenticate(Request $request): Passport @@ -255,7 +244,7 @@ using :ref:`the user provider `:: // ... return new Passport( - new UserBadge($email, function (string $userIdentifier) { + new UserBadge($email, function (string $userIdentifier): ?UserInterface { return $this->userRepository->findOneBy(['email' => $userIdentifier]); }), $credentials @@ -285,7 +274,7 @@ The following credential classes are supported by default: // If this function returns anything else than `true`, the credentials // are marked as invalid. // The $credentials parameter is equal to the next argument of this class - function ($credentials, UserInterface $user) { + function (string $credentials, UserInterface $user): bool { return $user->getApiToken() === $credentials; }, @@ -329,10 +318,10 @@ the following badges are supported: initiated). This skips the :doc:`pre-authentication user checker `. -.. versionadded:: 5.2 +.. note:: - Since 5.2, the ``PasswordUpgradeBadge`` is automatically added to - the passport if the passport has ``PasswordCredentials``. + The ``PasswordUpgradeBadge`` is automatically added to the passport if the + passport has ``PasswordCredentials``. For instance, if you want to add CSRF to your custom authenticator, you would initialize the passport like this:: @@ -350,11 +339,11 @@ would initialize the passport like this:: { public function authenticate(Request $request): Passport { - $password = $request->request->get('password'); - $username = $request->request->get('username'); - $csrfToken = $request->request->get('csrf_token'); + $password = $request->getPayload()->get('password'); + $username = $request->getPayload()->get('username'); + $csrfToken = $request->getPayload()->get('csrf_token'); - // ... validate no parameter is empty + // ... return new Passport( new UserBadge($username), @@ -367,10 +356,6 @@ would initialize the passport like this:: Passport Attributes ------------------- -.. versionadded:: 5.2 - - Passport attributes were introduced in Symfony 5.2. - Besides badges, passports can define attributes, which allows the ``authenticate()`` method to store arbitrary information in the passport to access it from other authenticator methods (e.g. ``createToken()``):: @@ -382,7 +367,7 @@ authenticator methods (e.g. ``createToken()``):: { // ... - public function authenticate(Request $request): PassportInterface + public function authenticate(Request $request): Passport { // ... process the request @@ -394,9 +379,11 @@ authenticator methods (e.g. ``createToken()``):: return $passport; } - public function createToken(PassportInterface $passport, string $firewallName): TokenInterface + public function createToken(Passport $passport, string $firewallName): TokenInterface { // read the attribute value return new CustomOauthToken($passport->getUser(), $passport->getAttribute('scope')); } } + +.. _`session storage flooding`: https://symfony.com/blog/cve-2016-4423-large-username-storage-in-session diff --git a/security/entry_point.rst b/security/entry_point.rst index 9dfaf8bca8c..cfbef00ff88 100644 --- a/security/entry_point.rst +++ b/security/entry_point.rst @@ -18,7 +18,6 @@ You can configure this using the ``entry_point`` setting: # config/packages/security.yaml security: - enable_authenticator_manager: true # ... firewalls: @@ -43,7 +42,7 @@ You can configure this using the ``entry_point`` setting: http://symfony.com/schema/dic/security https://symfony.com/schema/dic/security/security-1.0.xsd"> - + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + $security->firewall('main') + // ... + ->switchUser() + ->targetRoute('app_user_dashboard') + ; + }; + Limiting User Switching ----------------------- @@ -293,7 +351,7 @@ be called): // config/packages/security.php use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { // ... $security->firewall('main') // ... @@ -308,18 +366,16 @@ logic you want:: // src/Security/Voter/SwitchToCustomerVoter.php namespace App\Security\Voter; + use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; - use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; class SwitchToCustomerVoter extends Voter { - private $security; - - public function __construct(Security $security) - { - $this->security = $security; + public function __construct( + private Security $security, + ) { } protected function supports($attribute, $subject): bool diff --git a/security/ldap.rst b/security/ldap.rst index 39cf26081c7..6466178ee69 100644 --- a/security/ldap.rst +++ b/security/ldap.rst @@ -25,7 +25,7 @@ This means that the following scenarios will work: either the LDAP form login or LDAP HTTP Basic authentication providers. * Checking a user's password against an LDAP server while fetching user - information from another source (database using FOSUserBundle, for + information from another source (like your main database for example). * Loading user information from an LDAP server, while using another @@ -184,7 +184,7 @@ use the ``ldap`` user provider. use Symfony\Component\Ldap\Ldap; use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { $security->provider('ldap_users') ->ldap() ->service(Ldap::class) @@ -197,12 +197,6 @@ use the ``ldap`` user provider. ; }; -.. versionadded:: 5.4 - - The ``LdapUser::getExtraFields()`` method always returns an array of values. - In prior Symfony versions, ``LdapUserProvider`` threw an ``InvalidArgumentException`` - on multiple attributes. - .. caution:: The Security component escapes provided input data when the LDAP user @@ -292,14 +286,14 @@ filter This key lets you configure which LDAP query will be used. The ``{uid_key}`` string will be replaced by the value of the ``uid_key`` configuration value -(by default, ``sAMAccountName``), and the ``{username}`` string will be -replaced by the username you are trying to load. +(by default, ``sAMAccountName``), and the ``{user_identifier}`` string will be +replaced by the user identified you are trying to load. For example, with a ``uid_key`` of ``uid``, and if you are trying to load the user ``fabpot``, the final string will be: ``(uid=fabpot)``. If you pass ``null`` as the value of this option, the default filter is used -``({uid_key}={username})``. +``({uid_key}={user_identifier})``. To prevent `LDAP injection`_, the username will be escaped. @@ -326,15 +320,15 @@ number or contain white spaces. dn_string ......... -**type**: ``string`` **default**: ``{username}`` +**type**: ``string`` **default**: ``{user_identifier}`` This key defines the form of the string used to compose the -DN of the user, from the username. The ``{username}`` string is +DN of the user, from the username. The ``{user_identifier}`` string is replaced by the actual username of the person trying to authenticate. For example, if your users have DN strings in the form ``uid=einstein,dc=example,dc=com``, then the ``dn_string`` will be -``uid={username},dc=example,dc=com``. +``uid={user_identifier},dc=example,dc=com``. query_string ............ @@ -344,8 +338,8 @@ query_string This (optional) key makes the user provider search for a user and then use the found DN for the bind process. This is useful when using multiple LDAP user providers with different ``base_dn``. The value of this option must be a valid -search string (e.g. ``uid="{username}"``). The placeholder value will be -replaced by the actual username. +search string (e.g. ``uid="{user_identifier}"``). The placeholder value will be +replaced by the actual user identifier. When this option is used, ``query_string`` will search in the DN specified by ``dn_string`` and the DN resulted of the ``query_string`` will be used to @@ -378,7 +372,7 @@ Configuration example for form login form_login_ldap: # ... service: Symfony\Component\Ldap\Ldap - dn_string: 'uid={username},dc=example,dc=com' + dn_string: 'uid={user_identifier},dc=example,dc=com' .. code-block:: xml @@ -395,7 +389,7 @@ Configuration example for form login + dn-string="uid={user_identifier},dc=example,dc=com"/> @@ -406,11 +400,11 @@ Configuration example for form login use Symfony\Component\Ldap\Ldap; use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { $security->firewall('main') ->formLoginLdap() ->service(Ldap::class) - ->dnString('uid={username},dc=example,dc=com') + ->dnString('uid={user_identifier},dc=example,dc=com') ; }; @@ -430,7 +424,7 @@ Configuration example for HTTP Basic stateless: true http_basic_ldap: service: Symfony\Component\Ldap\Ldap - dn_string: 'uid={username},dc=example,dc=com' + dn_string: 'uid={user_identifier},dc=example,dc=com' .. code-block:: xml @@ -449,7 +443,7 @@ Configuration example for HTTP Basic + dn-string="uid={user_identifier},dc=example,dc=com"/> @@ -460,12 +454,12 @@ Configuration example for HTTP Basic use Symfony\Component\Ldap\Ldap; use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { $security->firewall('main') ->stateless(true) ->formLoginLdap() ->service(Ldap::class) - ->dnString('uid={username},dc=example,dc=com') + ->dnString('uid={user_identifier},dc=example,dc=com') ; }; @@ -486,7 +480,7 @@ Configuration example for form login and query_string form_login_ldap: service: Symfony\Component\Ldap\Ldap dn_string: 'dc=example,dc=com' - query_string: '(&(uid={username})(memberOf=cn=users,ou=Services,dc=example,dc=com))' + query_string: '(&(uid={user_identifier})(memberOf=cn=users,ou=Services,dc=example,dc=com))' search_dn: '...' search_password: 'the-raw-password' @@ -507,7 +501,7 @@ Configuration example for form login and query_string @@ -520,13 +514,13 @@ Configuration example for form login and query_string use Symfony\Component\Ldap\Ldap; use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { $security->firewall('main') ->stateless(true) ->formLoginLdap() ->service(Ldap::class) ->dnString('dc=example,dc=com') - ->queryString('(&(uid={username})(memberOf=cn=users,ou=Services,dc=example,dc=com))') + ->queryString('(&(uid={user_identifier})(memberOf=cn=users,ou=Services,dc=example,dc=com))') ->searchDn('...') ->searchPassword('the-raw-password') ; diff --git a/security/login_link.rst b/security/login_link.rst index 51f6f613f1b..e80a3cb27b0 100644 --- a/security/login_link.rst +++ b/security/login_link.rst @@ -10,13 +10,6 @@ This authentication method can help you eliminate most of the customer support related to authentication (e.g. I forgot my password, how can I change or reset my password, etc.) -.. note:: - - Login links are only supported by Symfony when using the - :doc:`authenticator system `. Before using this - authenticator, make sure you have enabled it with - ``enable_authenticator_manager: true`` in your ``security.yaml`` file. - Using the Login Link Authenticator ---------------------------------- @@ -68,7 +61,7 @@ and ``signature_properties`` (explained below): // config/packages/security.php use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { $security->firewall('main') ->loginLink() ->checkRoute('login_check') @@ -88,37 +81,18 @@ intercept requests to this route: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/SecurityController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class SecurityController extends AbstractController - { - /** - * @Route("/login_check", name="login_check") - */ - public function check() - { - throw new \LogicException('This code should never be reached'); - } - } - .. code-block:: php-attributes // src/Controller/SecurityController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; class SecurityController extends AbstractController { #[Route('/login_check', name: 'login_check')] - public function check() + public function check(): never { throw new \LogicException('This code should never be reached'); } @@ -151,7 +125,7 @@ intercept requests to this route: use App\Controller\DefaultController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return function (RoutingConfigurator $routes): void { // ... $routes->add('login_check', '/login_check'); }; @@ -173,20 +147,19 @@ this interface:: use App\Repository\UserRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface; class SecurityController extends AbstractController { - /** - * @Route("/login", name="login") - */ - public function requestLoginLink(LoginLinkHandlerInterface $loginLinkHandler, UserRepository $userRepository, Request $request) + #[Route('/login', name: 'login')] + public function requestLoginLink(LoginLinkHandlerInterface $loginLinkHandler, UserRepository $userRepository, Request $request): Response { // check if form is submitted if ($request->isMethod('POST')) { // load the user in some way (e.g. using the form input) - $email = $request->request->get('email'); + $email = $request->getPayload()->get('email'); $user = $userRepository->findOneBy(['email' => $email]); // create a login link for $user this returns an instance @@ -251,13 +224,11 @@ number:: class SecurityController extends AbstractController { - /** - * @Route("/login", name="login") - */ - public function requestLoginLink(NotifierInterface $notifier, LoginLinkHandlerInterface $loginLinkHandler, UserRepository $userRepository, Request $request) + #[Route('/login', name: 'login')] + public function requestLoginLink(NotifierInterface $notifier, LoginLinkHandlerInterface $loginLinkHandler, UserRepository $userRepository, Request $request): Response { if ($request->isMethod('POST')) { - $email = $request->request->get('email'); + $email = $request->getPayload()->get('email'); $user = $userRepository->findOneBy(['email' => $email]); $loginLinkDetails = $loginLinkHandler->createLoginLink($user); @@ -308,6 +279,8 @@ This will send an email like this to the user: // src/Notifier/CustomLoginLinkNotification namespace App\Notifier; + use Symfony\Component\Notifier\Message\EmailMessage; + use Symfony\Component\Notifier\Recipient\EmailRecipientInterface; use Symfony\Component\Security\Http\LoginLink\LoginLinkNotification; class CustomLoginLinkNotification extends LoginLinkNotification @@ -342,6 +315,8 @@ configuration decisions are discussed: * `Invalidate Login Links`_ * `Allow a Link to only be Used Once`_ +.. _login-link-lifetime: + Limit Login Link Lifetime ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -390,7 +365,7 @@ seconds). You can customize this using the ``lifetime`` option: // config/packages/security.php use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { $security->firewall('main') ->loginLink() ->checkRoute('login_check') @@ -399,6 +374,10 @@ seconds). You can customize this using the ``lifetime`` option: ; }; +.. tip:: + + You can also :ref:`customize the lifetime per link `. + .. _security-login-link-signature: Invalidate Login Links @@ -471,7 +450,7 @@ You can add more properties to the ``hash`` by using the // config/packages/security.php use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { $security->firewall('main') ->loginLink() ->checkRoute('login_check') @@ -543,7 +522,7 @@ cache. Enable this support by setting the ``max_uses`` option: // config/packages/security.php use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { $security->firewall('main') ->loginLink() ->checkRoute('login_check') @@ -616,7 +595,7 @@ the authenticator only handle HTTP POST methods: // config/packages/security.php use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { $security->firewall('main') ->loginLink() ->checkRoute('login_check') @@ -636,10 +615,8 @@ user create this POST request (e.g. by clicking a button):: class SecurityController extends AbstractController { - /** - * @Route("/login_check", name="login_check") - */ - public function check(Request $request) + #[Route('/login_check', name: 'login_check')] + public function check(Request $request): Response { // get the login link query parameters $expires = $request->query->get('expires'); @@ -766,7 +743,7 @@ Then, configure this service ID as the ``success_handler``: use App\Security\Authentication\AuthenticationSuccessHandler; use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { $security->firewall('main') ->loginLink() ->checkRoute('login_check') @@ -785,10 +762,6 @@ Then, configure this service ID as the ``success_handler``: Customizing the Login Link -------------------------- -.. versionadded:: 5.3 - - The possibility to customize the login link was introduced in Symfony 5.3. - The ``createLoginLink()`` method accepts a second optional argument to pass the ``Request`` object used when generating the login link. This allows to customize features such as the locale used to generate the link:: @@ -802,10 +775,8 @@ features such as the locale used to generate the link:: class SecurityController extends AbstractController { - /** - * @Route("/login", name="login") - */ - public function requestLoginLink(LoginLinkHandlerInterface $loginLinkHandler, Request $request) + #[Route('/login', name: 'login')] + public function requestLoginLink(LoginLinkHandlerInterface $loginLinkHandler, Request $request): Response { // check if login form is submitted if ($request->isMethod('POST')) { @@ -827,3 +798,13 @@ features such as the locale used to generate the link:: // ... } + +.. _customizing-link-lifetime: + +By default, generated links use :ref:`the lifetime configured globally ` +but you can change the lifetime per link using the third argument of the +``createLoginLink()`` method:: + + // the third optional argument is the lifetime in seconds + $loginLinkDetails = $loginLinkHandler->createLoginLink($user, null, 60); + $loginLink = $loginLinkDetails->getUrl(); diff --git a/security/passwords.rst b/security/passwords.rst index b228058c7e3..09e01507dec 100644 --- a/security/passwords.rst +++ b/security/passwords.rst @@ -11,12 +11,6 @@ Make sure it is installed by running: $ composer require symfony/password-hasher -.. versionadded:: 5.3 - - The PasswordHasher component was introduced in 5.3. Prior to this - version, password hashing functionality was provided by the Security - component. - Configuring a Password Hasher ----------------------------- @@ -77,7 +71,7 @@ optionally some *algorithm options*: use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { // ... // auto hasher with default options for the User class (and children) @@ -107,11 +101,6 @@ optionally some *algorithm options*: ], ]); -.. versionadded:: 5.3 - - The ``password_hashers`` option was introduced in Symfony 5.3. In previous - versions it was called ``encoders``. - In this example, the "auto" algorithm is used. This hasher automatically selects the most secure algorithm available on your system. Combined with :ref:`password migration `, this allows you to @@ -190,7 +179,7 @@ Further in this article, you can find a use App\Entity\User; use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { // ... // Use your user class name here @@ -220,13 +209,12 @@ After configuring the correct algorithm, you can use the namespace App\Controller; // ... - use - Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; + use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; class UserController extends AbstractController { - public function registration(UserPasswordHasherInterface $passwordHasher) + public function registration(UserPasswordHasherInterface $passwordHasher): Response { // ... e.g. get the user data from a registration form $user = new User(...); @@ -387,7 +375,7 @@ on the new hasher to point to the old, legacy hasher(s): // config/packages/security.php use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { // ... $security->passwordHasher('legacy') ->algorithm('sha256') @@ -482,13 +470,14 @@ storing the newly created password hash:: namespace App\Repository; // ... + use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; class UserRepository extends EntityRepository implements PasswordUpgraderInterface { // ... - public function upgradePassword(UserInterface $user, string $newHashedPassword): void + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void { // set the new hashed password on the User object $user->setPassword($newHashedPassword); @@ -536,13 +525,13 @@ migration by returning ``true`` in the ``needsRehash()`` method:: namespace App\Security; // ... - use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; + use Symfony\Component\PasswordHasher\PasswordHasherInterface; - class CustomPasswordHasher implements UserPasswordHasherInterface + class CustomPasswordHasher implements PasswordHasherInterface { // ... - public function needsRehash(string $hashed): bool + public function needsRehash(string $hashedPassword): bool { // check whether the current password is hashed using an outdated hasher $hashIsOutdated = ...; @@ -551,8 +540,10 @@ migration by returning ``true`` in the ``needsRehash()`` method:: } } -Named Password Hashers ----------------------- +.. _named-password-hashers: + +Dynamic Password Hashers +------------------------ Usually, the same password hasher is used for all users by configuring it to apply to all instances of a specific class. Another option is to use a @@ -603,7 +594,7 @@ cost. This can be done with named hashers: // config/packages/security.php use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { // ... $security->passwordHasher('harsh') ->algorithm('auto') @@ -653,6 +644,12 @@ the name of the hasher to use:: } } +.. caution:: + + When :ref:`migrating passwords `, you don't + need to implement ``PasswordHasherAwareInterface`` to return the legacy + hasher name: Symfony will detect it from your ``migrate_from`` configuration. + If you created your own password hasher implementing the :class:`Symfony\\Component\\PasswordHasher\\PasswordHasherInterface`, you must register a service for it in order to use it as a named hasher: @@ -694,7 +691,7 @@ you must register a service for it in order to use it as a named hasher: use App\Security\Hasher\MyCustomPasswordHasher; use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { // ... $security->passwordHasher('app_hasher') ->id(MyCustomPasswordHasher::class) @@ -752,9 +749,9 @@ Supported Algorithms The "auto" Hasher ~~~~~~~~~~~~~~~~~~ -It automatically selects the best available hasher. Starting from Symfony 5.3, -it uses the Bcrypt hasher. If PHP or Symfony adds new password hashers in the -future, it might select a different hasher. +It automatically selects the best available hasher (currently Bcrypt). If +PHP or Symfony adds new password hashers in the future, it might select a +different hasher. Because of this, the length of the hashed passwords may change in the future, so make sure to allocate enough space for them to be persisted (``varchar(255)`` @@ -912,7 +909,7 @@ Now, define a password hasher using the ``id`` setting: use App\Security\Hasher\CustomVerySecureHasher; use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { // ... $security->passwordHasher('app_hasher') // the service ID of your custom hasher (the FQCN using the default services.yaml) diff --git a/security/remember_me.rst b/security/remember_me.rst index 055c0a783cf..8fac6d78849 100644 --- a/security/remember_me.rst +++ b/security/remember_me.rst @@ -1,12 +1,6 @@ How to Add "Remember Me" Login Functionality ============================================ -.. caution:: - - This article documents the remember me system that was introduced in - the new authenticator system in 5.3. If you're using the deprecated - security system, refer to the `5.2 version of this documentation`_. - Once a user is authenticated, their credentials are typically stored in the session. This means that when the session ends they will be logged out and have to provide their login details again next time they wish to access the @@ -68,7 +62,7 @@ the session lasts using a cookie with the ``remember_me`` firewall option: // config/packages/security.php use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { // ... $security->firewall('main') // ... @@ -90,9 +84,9 @@ which is defined using the ``APP_SECRET`` environment variable. After enabling the ``remember_me`` system in the configuration, there are a couple more things to do before remember me works correctly: -#. :ref:`Add an opt-in checkbox to active remember me `; +#. :ref:`Add an opt-in checkbox to activate remember me `; #. :ref:`Use an authenticator that supports remember me `; -#. Optionally, :ref:`configure the how remember me cookies are stored and validated `. +#. Optionally, :ref:`configure how remember me cookies are stored and validated `. After this, the remember me cookie will be created upon successful authentication. For some pages/actions, you can @@ -114,6 +108,9 @@ Using the remember me cookie is not always appropriate (e.g. you should not use it on a shared PC). This is why by default, Symfony requires your users to opt-in to the remember me system via a request parameter. +Remember Me for Form Login +~~~~~~~~~~~~~~~~~~~~~~~~~~ + This request parameter is often set via a checkbox in the login form. This checkbox must have a name of ``_remember_me``: @@ -134,7 +131,26 @@ checkbox must have a name of ``_remember_me``: .. note:: Optionally, you can configure a custom name for this checkbox using the - ``remember_me_parameter`` setting under the ``remember_me`` section. + ``name`` setting under the ``remember_me`` section. + +Remember Me for JSON Login +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you implement the login via an API that uses :ref:`JSON Login ` +you can add a ``_remember_me`` key to the body of your POST request. + +.. code-block:: json + + { + "username": "dunglas@example.com", + "password": "MyPassword", + "_remember_me": true + } + +.. note:: + + Optionally, you can configure a custom name for this key using the + ``name`` setting under the ``remember_me`` section of your firewall. Always activating Remember Me ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -190,7 +206,7 @@ allow users to opt-out. In these cases, you can use the // config/packages/security.php use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { // ... $security->firewall('main') // ... @@ -285,10 +301,6 @@ Persistent tokens You can then configure this custom handler by configuring the service ID in the ``service`` option under ``remember_me``. - .. versionadded:: 5.1 - - The ``service`` option was introduced in Symfony 5.1. - .. _security-remember-me-signature: Using Signed Remember Me Tokens @@ -358,7 +370,7 @@ are fetched from the user object using the // config/packages/security.php use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { // ... $security->firewall('main') // ... @@ -442,7 +454,7 @@ You can enable the doctrine token provider using the ``doctrine`` setting: // config/packages/security.php use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { // ... $security->firewall('main') // ... @@ -531,7 +543,7 @@ Then, configure the service ID of your custom token provider as ``service``: use App\Security\RememberMe\CustomTokenProvider; use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { // ... $security->firewall('main') // ... @@ -585,10 +597,6 @@ users to change their password. You can do this by leveraging a few special There is also a ``IS_REMEMBERED`` attribute that grants access *only* when the user is authenticated via the remember me mechanism. -.. versionadded:: 5.1 - - The ``IS_REMEMBERED`` attribute was introduced in Symfony 5.1. - Customizing the Remember Me Cookie ---------------------------------- @@ -627,5 +635,3 @@ cookie created by the system: ``samesite`` (default value: ``null``) If set to ``strict``, the cookie associated with this feature will not be sent along with cross-site requests, even when following a regular link. - -.. _`5.2 version of this documentation`: https://symfony.com/doc/5.2/security/remember_me.html diff --git a/security/user_checkers.rst b/security/user_checkers.rst index 66981736ded..d62cc0bea32 100644 --- a/security/user_checkers.rst +++ b/security/user_checkers.rst @@ -53,10 +53,6 @@ displayed to the user:: } } -.. versionadded:: 5.1 - - The ``CustomUserMessageAccountStatusException`` class was introduced in Symfony 5.1. - Enabling the Custom User Checker -------------------------------- @@ -109,7 +105,7 @@ is the service id of your user checker: use App\Security\UserChecker; use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { // ... $security->firewall('main') ->pattern('^/') @@ -117,3 +113,144 @@ is the service id of your user checker: // ... ; }; + +Using Multiple User Checkers +---------------------------- + +It is common for applications to have multiple authentication entry points (such as +traditional form based login and an API) which may have unique checker rules for each +entry point as well as common rules for all entry points. To allow using multiple user +checkers on a firewall, a service for the :class:`Symfony\\Component\\Security\\Core\\User\\ChainUserChecker` +class is created for each firewall. + +To use the chain user checker, first you will need to tag your user checker services with the +``security.user_checker.`` tag (where ```` is the name of the firewall +in your security configuration). The service tag also supports the priority attribute, allowing you to define the +order in which user checkers are called:: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + + # ... + services: + App\Security\AccountEnabledUserChecker: + tags: + - { name: security.user_checker.api, priority: 10 } + - { name: security.user_checker.main, priority: 10 } + + App\Security\APIAccessAllowedUserChecker: + tags: + - { name: security.user_checker.api, priority: 5 } + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Security\AccountEnabledUserChecker; + use App\Security\APIAccessAllowedUserChecker; + + return function(ContainerConfigurator $containerConfigurator) { + $services = $containerConfigurator->services(); + + $services->set(AccountEnabledUserChecker::class) + ->tag('security.user_checker.api', ['priority' => 10]) + ->tag('security.user_checker.main', ['priority' => 10]); + + $services->set(APIAccessAllowedUserChecker::class) + ->tag('security.user_checker.api', ['priority' => 5]); + }; + +Once your checker services are tagged, next you will need configure your firewalls to use the +``security.user_checker.chain.`` service:: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + + # ... + security: + firewalls: + api: + pattern: ^/api + user_checker: security.user_checker.chain.api + # ... + main: + pattern: ^/ + user_checker: security.user_checker.chain.main + # ... + + .. code-block:: xml + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + $security->firewall('api') + ->pattern('^/api') + ->userChecker('security.user_checker.chain.api') + // ... + ; + + $security->firewall('main') + ->pattern('^/') + ->userChecker('security.user_checker.chain.main') + // ... + ; + }; diff --git a/security/user_providers.rst b/security/user_providers.rst index cab94b76af8..17a18468168 100644 --- a/security/user_providers.rst +++ b/security/user_providers.rst @@ -122,19 +122,8 @@ in your :ref:`Doctrine repository ` (e.g. ``UserRepository``): ->setParameter('query', $usernameOrEmail) ->getOneOrNullResult(); } - - /** @deprecated since Symfony 5.3 */ - public function loadUserByUsername(string $usernameOrEmail): ?User - { - return $this->loadUserByIdentifier($usernameOrEmail); - } } -.. versionadded:: 5.3 - - The method ``loadUserByIdentifier()`` was introduced to the - ``UserLoaderInterface`` in Symfony 5.3. - To finish this, remove the ``property`` key from the user provider in ``security.yaml``: @@ -408,9 +397,6 @@ command will generate a nice skeleton to get you started:: class UserProvider implements UserProviderInterface, PasswordUpgraderInterface { /** - * The loadUserByIdentifier() method was introduced in Symfony 5.3. - * In previous versions it was called loadUserByUsername() - * * Symfony calls this method if you use features like switch_user * or remember_me. If you're not using these features, you do not * need to implement this method. @@ -435,8 +421,6 @@ command will generate a nice skeleton to get you started:: * * If your firewall is "stateless: true" (for a pure API), this * method is not called. - * - * @return UserInterface */ public function refreshUser(UserInterface $user): UserInterface { diff --git a/security/voters.rst b/security/voters.rst index a770e386c02..01be3eb0745 100644 --- a/security/voters.rst +++ b/security/voters.rst @@ -44,66 +44,84 @@ which makes creating a voter even easier:: abstract class Voter implements VoterInterface { - abstract protected function supports(string $attribute, $subject) bool; - abstract protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool; + abstract protected function supports(string $attribute, mixed $subject): bool; + abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool; } .. _how-to-use-the-voter-in-a-controller: .. tip:: - Checking each voter several times can be time consumming for applications + Checking each voter several times can be time consuming for applications that perform a lot of permission checks. To improve performance in those cases, you can make your voters implement the :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\CacheableVoterInterface`. This allows the access decision manager to remember the attribute and type of subject supported by the voter, to only call the needed voters each time. - .. versionadded:: 5.4 - - The ``CacheableVoterInterface`` interface was introduced in Symfony 5.4. - Setup: Checking for Access in a Controller ------------------------------------------ Suppose you have a ``Post`` object and you need to decide whether or not the current user can *edit* or *view* the object. In your controller, you'll check access with -code like this:: +code like this: - // src/Controller/PostController.php +.. configuration-block:: - // ... - class PostController extends AbstractController - { - /** - * @Route("/posts/{id}", name="post_show") - */ - public function show($id): Response - { - // get a Post object - e.g. query for it - $post = ...; + .. code-block:: php-attributes + // src/Controller/PostController.php + + // ... + use Symfony\Component\Security\Http\Attribute\IsGranted; + + class PostController extends AbstractController + { + #[Route('/posts/{id}', name: 'post_show')] // check for "view" access: calls all voters - $this->denyAccessUnlessGranted('view', $post); + #[IsGranted('view', 'post')] + public function show(Post $post): Response + { + // ... + } - // ... + #[Route('/posts/{id}/edit', name: 'post_edit')] + // check for "edit" access: calls all voters + #[IsGranted('edit', 'post')] + public function edit(Post $post): Response + { + // ... + } } - /** - * @Route("/posts/{id}/edit", name="post_edit") - */ - public function edit($id): Response + .. code-block:: php + + // src/Controller/PostController.php + + // ... + use App\Security\PostVoter; + + class PostController extends AbstractController { - // get a Post object - e.g. query for it - $post = ...; + #[Route('/posts/{id}', name: 'post_show')] + public function show(Post $post): Response + { + // check for "view" access: calls all voters + $this->denyAccessUnlessGranted(PostVoter::VIEW, $post); - // check for "edit" access: calls all voters - $this->denyAccessUnlessGranted('edit', $post); + // ... + } - // ... + #[Route('/posts/{id}/edit', name: 'post_edit')] + public function edit(Post $post): Response + { + // check for "edit" access: calls all voters + $this->denyAccessUnlessGranted(PostVoter::EDIT, $post); + + // ... + } } - } -The ``denyAccessUnlessGranted()`` method (and also the ``isGranted()`` method) +The ``#[IsGranted]`` attribute or ``denyAccessUnlessGranted()`` method (and also the ``isGranted()`` method) calls out to the "voter" system. Right now, no voters will vote on whether or not the user can "view" or "edit" a ``Post``. But you can create your *own* voter that decides this using whatever logic you want. @@ -130,7 +148,7 @@ would look like this:: const VIEW = 'view'; const EDIT = 'edit'; - protected function supports(string $attribute, $subject): bool + protected function supports(string $attribute, mixed $subject): bool { // if the attribute isn't one we support, return false if (!in_array($attribute, [self::VIEW, self::EDIT])) { @@ -145,7 +163,7 @@ would look like this:: return true; } - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { $user = $token->getUser(); @@ -158,14 +176,11 @@ would look like this:: /** @var Post $post */ $post = $subject; - switch ($attribute) { - case self::VIEW: - return $this->canView($post, $user); - case self::EDIT: - return $this->canEdit($post, $user); - } - - throw new \LogicException('This code should not be reached!'); + return match($attribute) { + self::VIEW => $this->canView($post, $user), + self::EDIT => $this->canEdit($post, $user), + default => throw new \LogicException('This code should not be reached!') + }; } private function canView(Post $post, User $user): bool @@ -190,7 +205,7 @@ That's it! The voter is done! Next, :ref:`configure it security = $security; + public function __construct( + private Security $security, + ) { } - protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute($attribute, mixed $subject, TokenInterface $token): bool { // ... @@ -289,10 +302,6 @@ There are four strategies available: This grants or denies access by the first voter that does not abstain, based on their service priority; - .. versionadded:: 5.1 - - The ``priority`` version strategy was introduced in Symfony 5.1. - Regardless the chosen strategy, if all voters abstained from voting, the decision is based on the ``allow_if_all_abstain`` config option (which defaults to ``false``). @@ -335,7 +344,7 @@ security configuration: // config/packages/security.php use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { $security->accessDecisionManager() ->strategy('unanimous') ->allowIfAllAbstain(false) @@ -345,10 +354,6 @@ security configuration: Custom Access Decision Strategy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.4 - - The ``strategy_service`` option was introduced in Symfony 5.4. - If none of the built-in strategies fits your use case, define the ``strategy_service`` option to use a custom service (your service must implement the :class:`Symfony\\Component\\Security\\Core\Authorization\\Strategy\\AccessDecisionStrategyInterface`): @@ -386,7 +391,7 @@ option to use a custom service (your service must implement the use App\Security\MyCustomAccessDecisionStrategy; use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { $security->accessDecisionManager() ->strategyService(MyCustomAccessDecisionStrategy::class) // ... @@ -433,9 +438,41 @@ must implement the :class:`Symfony\\Component\\Security\\Core\\Authorization\\Ac use App\Security\MyCustomAccessDecisionManager; use Symfony\Config\SecurityConfig; - return static function (SecurityConfig $security) { + return static function (SecurityConfig $security): void { $security->accessDecisionManager() ->service(MyCustomAccessDecisionManager::class) // ... ; }; + +.. _security-voters-change-message-and-status-code: + +Changing the message and status code returned +--------------------------------------------- + +By default, the ``#[IsGranted]`` attribute will throw a +:class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException` +and return an http **403** status code with **Access Denied** as message. + +However, you can change this behavior by specifying the message and status code returned:: + + // src/Controller/PostController.php + + // ... + use Symfony\Component\Security\Http\Attribute\IsGranted; + + class PostController extends AbstractController + { + #[Route('/posts/{id}', name: 'post_show')] + #[IsGranted('show', 'post', 'Post not found', 404)] + public function show(Post $post): Response + { + // ... + } + } + +.. tip:: + + If the status code is different than 403, an + :class:`Symfony\\Component\\HttpKernel\\Exception\\HttpException` + will be thrown instead. diff --git a/serializer.rst b/serializer.rst index 50bd0149a19..91fd92a39a3 100644 --- a/serializer.rst +++ b/serializer.rst @@ -28,11 +28,12 @@ you need it or it can be used in a controller:: namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Serializer\SerializerInterface; class DefaultController extends AbstractController { - public function index(SerializerInterface $serializer) + public function index(SerializerInterface $serializer): Response { // keep reading for usage examples } @@ -47,10 +48,6 @@ Or you can use the ``serialize`` Twig filter in a template: See the :doc:`twig reference ` for more information. -.. versionadded:: 5.3 - - A ``serialize`` filter was introduced in Symfony 5.3 that uses the Serializer component. - Adding Normalizers and Encoders ------------------------------- @@ -78,11 +75,7 @@ As well as the following normalizers: * :class:`Symfony\\Component\\Serializer\\Normalizer\\ConstraintViolationListNormalizer` * :class:`Symfony\\Component\\Serializer\\Normalizer\\ProblemNormalizer` * :class:`Symfony\\Component\\Serializer\\Normalizer\\BackedEnumNormalizer` - -.. versionadded:: 5.4 - - :class:`Symfony\\Component\\Serializer\\Normalizer\\BackedEnumNormalizer` - was introduced in Symfony 5.4. PHP BackedEnum requires at least PHP 8.1. +* :class:`Symfony\\Component\\Serializer\\Normalizer\\TranslatableNormalizer` Other :ref:`built-in normalizers ` and custom normalizers and/or encoders can also be loaded by tagging them as @@ -113,11 +106,6 @@ resources. This context is passed to all normalizers. For example: uses ``empty_array_as_object`` to represent empty arrays as ``{}`` instead of ``[]`` in JSON. -.. versionadded:: 5.4 - - The usage of the ``empty_array_as_object`` option in the - Serializer was introduced in Symfony 5.4. - You can pass the context as follows:: $serializer->serialize($something, 'json', [ @@ -141,6 +129,7 @@ configuration: serializer: default_context: enable_max_depth: true + yaml_indentation: 2 .. code-block:: xml @@ -148,50 +137,30 @@ configuration: - + .. code-block:: php // config/packages/framework.php + use Symfony\Component\Serializer\Encoder\YamlEncoder; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->serializer() ->defaultContext([ - AbstractObjectNormalizer::ENABLE_MAX_DEPTH => true + AbstractObjectNormalizer::ENABLE_MAX_DEPTH => true, + YamlEncoder::YAML_INDENTATION => 2, ]) ; }; -.. versionadded:: 5.4 - - The ability to configure the ``default_context`` option in the - Serializer was introduced in Symfony 5.4. - You can also specify the context on a per-property basis:: .. configuration-block:: - .. code-block:: php-annotations - - namespace App\Model; - - use Symfony\Component\Serializer\Annotation\Context; - use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; - - class Person - { - /** - * @Context({ DateTimeNormalizer::FORMAT_KEY = 'Y-m-d' }) - */ - public $createdAt; - - // ... - } - .. code-block:: php-attributes namespace App\Model; @@ -202,7 +171,7 @@ You can also specify the context on a per-property basis:: class Person { #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] - public $createdAt; + public \DateTimeInterface $createdAt; // ... } @@ -247,7 +216,7 @@ Use the options to specify context specific to normalization or denormalization: normalizationContext: [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'], denormalizationContext: [DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'], // To prevent to have the time from the moment of denormalization )] - public $createdAt; + public \DateTimeInterface $createdAt; // ... } @@ -268,25 +237,79 @@ You can also restrict the usage of a context to some groups:: context: [DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339_EXTENDED], groups: ['extended'], )] - public $createdAt; + public \DateTimeInterface $createdAt; // ... } -The attribute/annotation can be repeated as much as needed on a single property. -Context without group is always applied first. Then context for the matching groups are merged in the provided order. +The attribute can be repeated as much as needed on a single property. +Context without group is always applied first. Then context for the matching +groups are merged in the provided order. -.. versionadded:: 5.3 +If you repeat the same context in multiple properties, consider using the +``#[Context]`` attribute on your class to apply that context configuration to +all the properties of the class:: - The ``Context`` attribute, annotation and the configuration options were introduced in Symfony 5.3. + namespace App\Model; -.. _serializer-using-serialization-groups-annotations: + use Symfony\Component\Serializer\Annotation\Context; + use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; -Using Serialization Groups Annotations --------------------------------------- + #[Context([DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339])] + #[Context( + context: [DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339_EXTENDED], + groups: ['extended'], + )] + class Person + { + // ... + } + +.. _serializer-using-context-builders: + +Using Context Builders +---------------------- + +To define the (de)serialization context, you can use "context builders", which +are objects that help you to create that context by providing autocompletion, +validation, and documentation:: + + use Symfony\Component\Serializer\Context\Normalizer\DateTimeNormalizerContextBuilder; + + $contextBuilder = (new DateTimeNormalizerContextBuilder())->withFormat('Y-m-d H:i:s'); + $serializer->serialize($something, 'json', $contextBuilder->toArray()); + +Each normalizer/encoder has its related :ref:`context builder `. +To create a more complex (de)serialization context, you can chain them using the +``withContext()`` method:: + + use Symfony\Component\Serializer\Context\Encoder\CsvEncoderContextBuilder; + use Symfony\Component\Serializer\Context\Normalizer\ObjectNormalizerContextBuilder; + + $initialContext = [ + 'custom_key' => 'custom_value', + ]; + + $contextBuilder = (new ObjectNormalizerContextBuilder()) + ->withContext($initialContext) + ->withGroups(['group1', 'group2']); -You can add the :ref:`@Groups annotations ` -to your class:: + $contextBuilder = (new CsvEncoderContextBuilder()) + ->withContext($contextBuilder) + ->withDelimiter(';'); + + $serializer->serialize($something, 'csv', $contextBuilder->toArray()); + +You can also :doc:`create your context builders ` +to have autocompletion, validation, and documentation for your custom context values. + +.. _serializer-using-serialization-groups-attributes: + +Using Serialization Groups Attributes +------------------------------------- + +You can add :ref:`#[Groups] attributes ` +to your class properties:: // src/Entity/Product.php namespace App\Entity; @@ -294,45 +317,64 @@ to your class:: use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; - /** - * @ORM\Entity() - */ + #[ORM\Entity] class Product { - /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - * @Groups({"show_product", "list_product"}) - */ - private $id; - - /** - * @ORM\Column(type="string", length=255) - * @Groups({"show_product", "list_product"}) - */ - private $name; - - /** - * @ORM\Column(type="text") - * @Groups({"show_product"}) - */ - private $description; + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + #[Groups(['show_product', 'list_product'])] + private int $id; + + #[ORM\Column(type: 'string', length: 255)] + #[Groups(['show_product', 'list_product'])] + private string $name; + + #[ORM\Column(type: 'text')] + #[Groups(['show_product'])] + private string $description; } -You can now choose which groups to use when serializing:: +You can also use the ``#[Groups]`` attribute on class level:: + + #[ORM\Entity] + #[Groups(['show_product'])] + class Product + { + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + #[Groups(['list_product'])] + private int $id; + + #[ORM\Column(type: 'string', length: 255)] + #[Groups(['list_product'])] + private string $name; + + #[ORM\Column(type: 'text')] + private string $description; + } + +In this example, the ``id`` and the ``name`` properties belong to the +``show_product`` and ``list_product`` groups. The ``description`` property +only belongs to the ``show_product`` group. + +Now that your groups are defined, you can choose which groups to use when +serializing:: - $json = $serializer->serialize( - $product, - 'json', - ['groups' => 'show_product'] - ); + use Symfony\Component\Serializer\Context\Normalizer\ObjectNormalizerContextBuilder; + + $context = (new ObjectNormalizerContextBuilder()) + ->withGroups('show_product') + ->toArray(); + + $json = $serializer->serialize($product, 'json', $context); .. tip:: The value of the ``groups`` key can be a single string, or an array of strings. -In addition to the ``@Groups`` annotation, the Serializer component also +In addition to the ``#[Groups]`` attribute, the Serializer component also supports YAML or XML files. These files are automatically loaded when being stored in one of the following locations: @@ -343,8 +385,80 @@ stored in one of the following locations: * All ``*.yaml`` and ``*.xml`` files in the ``Resources/config/serialization/`` directory of a bundle. +.. note:: + + The groups used by default when normalizing and denormalizing objects are + ``Default`` and the group that matches the class name. For example, if you + are normalizing a ``App\Entity\Product`` object, the groups used are + ``Default`` and ``Product``. + + .. versionadded:: 7.1 + + The default use of the class name and ``Default`` groups when normalizing + and denormalizing objects was introduced in Symfony 7.1. + .. _serializer-enabling-metadata-cache: +Using Nested Attributes +----------------------- + +To map nested properties, use the ``SerializedPath`` configuration to define +their paths using a :doc:`valid PropertyAccess syntax `: + +.. configuration-block:: + + .. code-block:: php-attributes + + namespace App\Model; + + use Symfony\Component\Serializer\Attribute\SerializedPath; + + class Person + { + #[SerializedPath('[profile][information][birthday]')] + private string $birthday; + + // ... + } + + .. code-block:: yaml + + App\Model\Person: + attributes: + dob: + serialized_path: '[profile][information][birthday]' + + .. code-block:: xml + + + + + + + + +Using the configuration from above, denormalizing with a metadata-aware +normalizer will write the ``birthday`` field from ``$data`` onto the ``Person`` +object:: + + $data = [ + 'profile' => [ + 'information' => [ + 'birthday' => '01-01-1970', + ], + ], + ]; + $person = $normalizer->denormalize($data, Person::class, 'any'); + $person->getBirthday(); // 01-01-1970 + +When using attributes, the ``SerializedPath`` can either +be set on the property or the associated _getter_ method. The ``SerializedPath`` +cannot be used in combination with a ``SerializedName`` for the same property. + Configuring the Metadata Cache ------------------------------ @@ -387,10 +501,51 @@ value: // config/packages/framework.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->serializer()->nameConverter('serializer.name_converter.camel_case_to_snake_case'); }; +Debugging the Serializer +------------------------ + +Use the ``debug:serializer`` command to dump the serializer metadata of a +given class: + +.. code-block:: terminal + + $ php bin/console debug:serializer 'App\Entity\Book' + + App\Entity\Book + --------------- + + +----------+------------------------------------------------------------+ + | Property | Options | + +----------+------------------------------------------------------------+ + | name | [ | + | | "groups" => [ | + | | "book:read", | + | | "book:write", | + | | ], | + | | "maxDepth" => 1, | + | | "serializedName" => "book_name", | + | | "serializedPath" => null, | + | | "ignore" => false, | + | | "normalizationContexts" => [], | + | | "denormalizationContexts" => [] | + | | ] | + | isbn | [ | + | | "groups" => [ | + | | "book:read", | + | | ], | + | | "maxDepth" => null, | + | | "serializedName" => null, | + | | "serializedPath" => [data][isbn], | + | | "ignore" => false, | + | | "normalizationContexts" => [], | + | | "denormalizationContexts" => [] | + | | ] | + +----------+------------------------------------------------------------+ + Going Further with the Serializer --------------------------------- @@ -418,6 +573,7 @@ take a look at how this bundle works. serializer/custom_encoders serializer/custom_normalizer + serializer/custom_context_builders .. _`API Platform`: https://api-platform.com .. _`JSON-LD`: https://json-ld.org diff --git a/serializer/custom_context_builders.rst b/serializer/custom_context_builders.rst new file mode 100644 index 00000000000..00d08ef71d3 --- /dev/null +++ b/serializer/custom_context_builders.rst @@ -0,0 +1,82 @@ +How to Create your Custom Context Builder +========================================= + +The :doc:`Serializer Component ` uses Normalizers +and Encoders to transform any data to any data-structure (e.g. JSON). +That serialization process can be configured thanks to a +:ref:`serialization context `, which can be built thanks to +:ref:`context builders `. + +Each built-in normalizer/encoder has its related context builder. However, you +may want to create a custom context builder for your +:doc:`custom normalizers `. + +Creating a new Context Builder +------------------------------ + +Let's imagine that you want to handle date denormalization differently if they +are coming from a legacy system, by converting dates to ``null`` if the serialized +value is ``0000-00-00``. To do that you'll first have to create your normalizer:: + + // src/Serializer/ZeroDateTimeDenormalizer.php + namespace App\Serializer; + + use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; + use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; + use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; + + final class ZeroDateTimeDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface + { + use DenormalizerAwareTrait; + + public function denormalize($data, string $type, ?string $format = null, array $context = []): mixed + { + if ('0000-00-00' === $data) { + return null; + } + + unset($context['zero_datetime_to_null']); + + return $this->denormalizer->denormalize($data, $type, $format, $context); + } + + public function supportsDenormalization($data, string $type, ?string $format = null, array $context = []): bool + { + return true === ($context['zero_datetime_to_null'] ?? false) + && is_a($type, \DateTimeInterface::class, true); + } + } + +Now you can cast zero-ish dates to ``null`` during denormalization:: + + $legacyData = '{"updatedAt": "0000-00-00"}'; + $serializer->deserialize($legacyData, MyModel::class, 'json', ['zero_datetime_to_null' => true]); + +Now, to avoid having to remember about this specific ``zero_date_to_null`` +context key, you can create a dedicated context builder:: + + // src/Serializer/LegacyContextBuilder + namespace App\Serializer; + + use Symfony\Component\Serializer\Context\ContextBuilderInterface; + use Symfony\Component\Serializer\Context\ContextBuilderTrait; + + final class LegacyContextBuilder implements ContextBuilderInterface + { + use ContextBuilderTrait; + + public function withLegacyDates(bool $legacy): static + { + return $this->with('zero_datetime_to_null', $legacy); + } + } + +And finally, use it to build the serialization context:: + + $legacyData = '{"updatedAt": "0000-00-00"}'; + + $context = (new LegacyContextBuilder()) + ->withLegacyDates(true) + ->toArray(); + + $serializer->deserialize($legacyData, MyModel::class, 'json', $context); diff --git a/serializer/custom_encoders.rst b/serializer/custom_encoders.rst index 95f3131f418..dca6aa12ec4 100644 --- a/serializer/custom_encoders.rst +++ b/serializer/custom_encoders.rst @@ -25,34 +25,27 @@ create your own encoder that uses the class YamlEncoder implements EncoderInterface, DecoderInterface { - public function encode($data, string $format, array $context = []) + public function encode($data, string $format, array $context = []): string { return Yaml::dump($data); } - public function supportsEncoding(string $format) + public function supportsEncoding(string $format, array $context = []): bool { return 'yaml' === $format; } - public function decode(string $data, string $format, array $context = []) + public function decode(string $data, string $format, array $context = []): array { return Yaml::parse($data); } - public function supportsDecoding(string $format) + public function supportsDecoding(string $format, array $context = []): bool { return 'yaml' === $format; } } -.. tip:: - - If you need access to ``$context`` in your ``supportsDecoding`` or - ``supportsEncoding`` method, make sure to implement - ``Symfony\Component\Serializer\Encoder\ContextAwareDecoderInterface`` - or ``Symfony\Component\Serializer\Encoder\ContextAwareEncoderInterface`` accordingly. - Registering it in your app -------------------------- diff --git a/serializer/custom_normalizer.rst b/serializer/custom_normalizer.rst index dd02db39bb1..1519c2adf22 100644 --- a/serializer/custom_normalizer.rst +++ b/serializer/custom_normalizer.rst @@ -12,28 +12,30 @@ Creating a New Normalizer Imagine you want add, modify, or remove some properties during the serialization process. For that you'll have to create your own normalizer. But it's usually preferable to let Symfony normalize the object, then hook into the normalization -to customize the normalized data. To do that, leverage the ``ObjectNormalizer``:: +to customize the normalized data. To do that, you can inject a +``NormalizerInterface`` and wire it to Symfony's object normalizer. This will give +you access to a ``$normalizer`` property which takes care of most of the +normalization process:: // src/Serializer/TopicNormalizer.php namespace App\Serializer; use App\Entity\Topic; + use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; - use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - class TopicNormalizer implements ContextAwareNormalizerInterface + class TopicNormalizer implements NormalizerInterface { - private $router; - private $normalizer; + public function __construct( + #[Autowire(service: 'serializer.normalizer.object')] + private readonly NormalizerInterface $normalizer, - public function __construct(UrlGeneratorInterface $router, ObjectNormalizer $normalizer) - { - $this->router = $router; - $this->normalizer = $normalizer; + private UrlGeneratorInterface $router, + ) { } - public function normalize($topic, ?string $format = null, array $context = []) + public function normalize($topic, ?string $format = null, array $context = []): array { $data = $this->normalizer->normalize($topic, $format, $context); @@ -45,10 +47,17 @@ to customize the normalized data. To do that, leverage the ``ObjectNormalizer``: return $data; } - public function supportsNormalization($data, ?string $format = null, array $context = []) + public function supportsNormalization($data, ?string $format = null, array $context = []): bool { return $data instanceof Topic; } + + public function getSupportedTypes(?string $format): array + { + return [ + Topic::class => true, + ]; + } } Registering it in your Application @@ -59,8 +68,8 @@ a service and :doc:`tagged ` with ``serializer.normaliz If you're using the :ref:`default services.yaml configuration `, this is done automatically! -Performance ------------ +Performance of Normalizers/Denormalizers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To figure which normalizer (or denormalizer) must be used to handle an object, the :class:`Symfony\\Component\\Serializer\\Serializer` class will call the @@ -68,21 +77,52 @@ the :class:`Symfony\\Component\\Serializer\\Serializer` class will call the (or :method:`Symfony\\Component\\Serializer\\Normalizer\\DenormalizerInterface::supportsDenormalization`) of all registered normalizers (or denormalizers) in a loop. -The result of these methods can vary depending on the object to serialize, the -format and the context. That's why the result **is not cached** by default and -can result in a significant performance bottleneck. +Additionally, both +:class:`Symfony\\Component\\Serializer\\Normalizer\\NormalizerInterface` +and :class:`Symfony\\Component\\Serializer\\Normalizer\\DenormalizerInterface` +contain the ``getSupportedTypes()`` method. This method allows normalizers or +denormalizers to declare the type of objects they can handle, and whether they +are cacheable. With this info, even if the ``supports*()`` call is not cacheable, +the Serializer can skip a ton of method calls to ``supports*()`` improving +performance substantially in some cases. + +The ``getSupportedTypes()`` method should return an array where the keys +represent the supported types, and the values indicate whether the result of +the ``supports*()`` method call can be cached or not. The format of the +returned array is as follows: + +#. The special key ``object`` can be used to indicate that the normalizer or + denormalizer supports any classes or interfaces. +#. The special key ``*`` can be used to indicate that the normalizer or + denormalizer might support any types. +#. The other keys in the array should correspond to specific types that the + normalizer or denormalizer supports. +#. The values associated with each type should be a boolean indicating if the + result of the ``supports*()`` method call for that type can be cached or not. + A value of ``true`` means that the result is cacheable, while ``false`` means + that the result is not cacheable. +#. A ``null`` value for a type means that the normalizer or denormalizer does + not support that type. + +Here is an example of how to use the ``getSupportedTypes()`` method:: + + use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + + class MyNormalizer implements NormalizerInterface + { + // ... -However, most normalizers (and denormalizers) always return the same result when -the object's type and the format are the same, so the result can be cached. To -do so, make those normalizers (and denormalizers) implement the -:class:`Symfony\\Component\\Serializer\\Normalizer\\CacheableSupportsMethodInterface` -and return ``true`` when -:method:`Symfony\\Component\\Serializer\\Normalizer\\CacheableSupportsMethodInterface::hasCacheableSupportsMethod` -is called. + public function getSupportedTypes(?string $format): array + { + return [ + 'object' => null, // Doesn't support any classes or interfaces + '*' => false, // Supports any other types, but the result is not cacheable + MyCustomClass::class => true, // Supports MyCustomClass and result is cacheable + ]; + } + } .. note:: - All built-in :ref:`normalizers and denormalizers ` - as well the ones included in `API Platform`_ natively implement this interface. - -.. _`API Platform`: https://api-platform.com + The ``supports*()`` method implementations should not assume that + ``getSupportedTypes()`` has been called before. diff --git a/service_container.rst b/service_container.rst index 8f3d53b6733..67bf80e3101 100644 --- a/service_container.rst +++ b/service_container.rst @@ -30,13 +30,11 @@ service's class or interface name. Want to :doc:`log ` something? No p use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; class ProductController extends AbstractController { - /** - * @Route("/products") - */ + #[Route('/products')] public function list(LoggerInterface $logger): Response { $logger->info('Look, I just used a service!'); @@ -59,13 +57,13 @@ What other services are available? Find out by running: The following classes & interfaces can be used as type-hints when autowiring: Describes a logger instance. - Psr\Log\LoggerInterface (logger) + Psr\Log\LoggerInterface - alias:logger Request stack that controls the lifecycle of requests. - Symfony\Component\HttpFoundation\RequestStack (request_stack) + Symfony\Component\HttpFoundation\RequestStack - alias:request_stack RouterInterface is the interface that all Router classes must implement. - Symfony\Component\Routing\RouterInterface (router.default) + Symfony\Component\Routing\RouterInterface - alias:router.default [...] @@ -119,7 +117,7 @@ inside your controller:: use App\Service\MessageGenerator; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; class ProductController extends AbstractController { @@ -204,7 +202,7 @@ each time you ask for it. // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { // default configuration for services in *this* file $services = $container->services() ->defaults() @@ -240,12 +238,8 @@ each time you ask for it. Limiting Services to a specific Symfony Environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.3 - - The ``#[When]`` attribute was introduced in Symfony 5.3. - -If you are using PHP 8.0 or later, you can use the ``#[When]`` PHP -attribute to only register the class as a service in some environments:: +You can use the ``#[When]`` attribute to only register the class +as a service in some environments:: use Symfony\Component\DependencyInjection\Attribute\When; @@ -283,11 +277,9 @@ and use it later:: class MessageGenerator { - private $logger; - - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; + public function __construct( + private LoggerInterface $logger, + ) { } public function getHappyMessage(): string @@ -321,13 +313,13 @@ type-hints by running: # this is just a *small* sample of the output... Describes a logger instance. - Psr\Log\LoggerInterface (monolog.logger) + Psr\Log\LoggerInterface - alias:monolog.logger Request stack that controls the lifecycle of requests. - Symfony\Component\HttpFoundation\RequestStack (request_stack) + Symfony\Component\HttpFoundation\RequestStack - alias:request_stack RouterInterface is the interface that all Router classes must implement. - Symfony\Component\Routing\RouterInterface (router.default) + Symfony\Component\Routing\RouterInterface - alias:router.default [...] @@ -346,13 +338,10 @@ made. To do that, you create a new class:: class SiteUpdateManager { - private MessageGenerator $messageGenerator; - private MailerInterface $mailer; - - public function __construct(MessageGenerator $messageGenerator, MailerInterface $mailer) - { - $this->messageGenerator = $messageGenerator; - $this->mailer = $mailer; + public function __construct( + private MessageGenerator $messageGenerator, + private MailerInterface $mailer, + ) { } public function notifyOfSiteUpdate(): bool @@ -418,13 +407,13 @@ example, suppose you want to make the admin email configurable: class SiteUpdateManager { // ... - + private $adminEmail; + + private string $adminEmail; - - public function __construct(MessageGenerator $messageGenerator, MailerInterface $mailer) - + public function __construct(MessageGenerator $messageGenerator, MailerInterface $mailer, string $adminEmail) - { - // ... - + $this->adminEmail = $adminEmail; + public function __construct( + private MessageGenerator $messageGenerator, + private MailerInterface $mailer, + + private string $adminEmail + ) { } public function notifyOfSiteUpdate(): bool @@ -500,7 +489,7 @@ pass here. No problem! In your configuration, you can explicitly set this argume use App\Service\SiteUpdateManager; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { // ... // same as before @@ -574,11 +563,10 @@ parameter and in PHP config use the ``service()`` function: use App\Service\MessageGenerator; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(MessageGenerator::class) - // In versions earlier to Symfony 5.1 the service() function was called ref() ->args([service('logger')]) ; }; @@ -622,11 +610,9 @@ The ``MessageGenerator`` service created earlier requires a ``LoggerInterface`` class MessageGenerator { - private $logger; - - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; + public function __construct( + private LoggerInterface $logger, + ) { } // ... } @@ -681,7 +667,7 @@ But, you can control this and pass in a different logger: use App\Service\MessageGenerator; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { // ... same code as before // explicitly configure the service @@ -732,6 +718,103 @@ for example to make a service unavailable in some :ref:`configuration environmen Now, the container will not contain the ``App\RemovedService`` in the ``test`` environment. +.. _container_closure-as-argument: + +Injecting a Closure as an Argument +---------------------------------- + +It is possible to inject a callable as an argument of a service. +Let's add an argument to our ``MessageGenerator`` constructor:: + + // src/Service/MessageGenerator.php + namespace App\Service; + + use Psr\Log\LoggerInterface; + + class MessageGenerator + { + private string $messageHash; + + public function __construct( + private LoggerInterface $logger, + callable $generateMessageHash, + ) { + $this->messageHash = $generateMessageHash(); + } + // ... + } + +Now, we would add a new invokable service to generate the message hash:: + + // src/Hash/MessageHashGenerator.php + namespace App\Hash; + + class MessageHashGenerator + { + public function __invoke(): string + { + // Compute and return a message hash + } + } + +Our configuration looks like this: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... same code as before + + # explicitly configure the service + App\Service\MessageGenerator: + arguments: + $logger: '@monolog.logger.request' + $generateMessageHash: !closure '@App\Hash\MessageHashGenerator' + + .. code-block:: xml + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Service\MessageGenerator; + + return function(ContainerConfigurator $containerConfigurator): void { + // ... same code as before + + // explicitly configure the service + $services->set(MessageGenerator::class) + ->arg('$logger', service('monolog.logger.request')) + ->arg('$generateMessageHash', closure('App\Hash\MessageHashGenerator')) + ; + }; + +.. seealso:: + + Closures can be injected :ref:`by using autowiring ` + and its dedicated attributes. + .. _services-binding: Binding Arguments by Name or Type @@ -810,7 +893,7 @@ You can also use the ``bind`` keyword to bind specific arguments by name or type use Psr\Log\LoggerInterface; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services() ->defaults() // pass this value to any $adminEmail argument for any service @@ -909,10 +992,6 @@ If you don't replace the value of an abstract argument during runtime, a ``RuntimeException`` will be thrown with a message like ``Argument "$rootNamespace" of service "App\Service\MyService" is abstract: should be defined by Pass.`` -.. versionadded:: 5.1 - - The abstract service arguments were introduced in Symfony 5.1. - .. _services-autowire: The autowire Option @@ -953,27 +1032,27 @@ Autoconfiguration also works with attributes. Some attributes like for autoconfiguration. Any class using these attributes will have tags applied to them. -.. versionadded:: 5.3 - - Autoconfiguration through attributes was introduced in Symfony 5.3. - Linting Service Definitions --------------------------- -The ``lint:container`` command checks that the arguments injected into services -match their type declarations. It's useful to run it before deploying your +The ``lint:container`` command performs additional checks to ensure the container +is properly configured. It is useful to run this command before deploying your application to production (e.g. in your continuous integration server): .. code-block:: terminal $ php bin/console lint:container -Checking the types of all service arguments whenever the container is compiled -can hurt performance. That's why this type checking is implemented in a -:doc:`compiler pass ` called -``CheckTypeDeclarationsPass`` which is disabled by default and enabled only when -executing the ``lint:container`` command. If you don't mind the performance -loss, enable the compiler pass in your application. +Performing those checks whenever the container is compiled can hurt performance. +That's why they are implemented in :doc:`compiler passes ` +called ``CheckTypeDeclarationsPass`` and ``CheckAliasValidityPass``, which are +disabled by default and enabled only when executing the ``lint:container`` command. +If you don't mind the performance loss, you can enable these compiler passes in +your application. + +.. versionadded:: 7.1 + + The ``CheckAliasValidityPass`` compiler pass was introduced in Symfony 7.1. .. _container-public: @@ -1027,7 +1106,7 @@ setting: use App\Service\PublicService; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { // ... same as code before // explicitly configure the service @@ -1051,16 +1130,6 @@ you want to configure:: // ... } -.. versionadded:: 5.3 - - The ``#[Autoconfigure]`` attribute was introduced in Symfony 5.3. PHP - attributes require at least PHP 8.0. - -.. deprecated:: 5.1 - - As of Symfony 5.1, it is no longer possible to autowire the service - container by type-hinting ``Psr\Container\ContainerInterface``. - .. _service-psr4-loader: Importing Many Services at once with resource @@ -1104,7 +1173,7 @@ key. For example, the default Symfony configuration contains this: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { // ... // makes classes in src/ available to be used as services @@ -1116,7 +1185,9 @@ key. For example, the default Symfony configuration contains this: .. tip:: The value of the ``resource`` and ``exclude`` options can be any valid - `glob pattern`_. + `glob pattern`_. If you want to exclude only a few services, you + may use the :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Exclude` + attribute directly on your class to exclude it. This can be used to quickly make many classes available as services and apply some default configuration. The ``id`` of each service is its fully-qualified class name. @@ -1210,11 +1281,8 @@ unique string as the key of each service config: Explicitly Configuring Services and Arguments --------------------------------------------- -Prior to Symfony 3.3, all services and (typically) arguments were explicitly configured: -it was not possible to :ref:`load services automatically ` -and :ref:`autowiring ` was much less common. - -Both of these features are optional. And even if you use them, there may be some +:ref:`Loading services automatically ` +and :ref:`autowiring ` are optional. And even if you use them, there may be some cases where you want to manually wire a service. For example, suppose that you want to register *2* services for the ``SiteUpdateManager`` class - each with a different admin email. In this case, each needs to have a unique service id: @@ -1286,7 +1354,7 @@ admin email. In this case, each needs to have a unique service id: use App\Service\MessageGenerator; use App\Service\SiteUpdateManager; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { // ... // site_update_manager.superadmin is the service's id @@ -1327,6 +1395,138 @@ or to create a named :ref:`autowiring alias `. and the automatically loaded service will be passed - by default - when you type-hint ``SiteUpdateManager``. That's why creating the alias is a good idea. +When using PHP closures to configure your services, it is possible to automatically +inject the current environment value by adding a string argument named ``$env`` to +the closure:: + + // config/packages/my_config.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $containerConfigurator, string $env): void { + // `$env` is automatically filled in, so you can configure your + // services depending on which environment you're on + }; + +Generating Adapters for Functional Interfaces +--------------------------------------------- + +Functional interfaces are interfaces with a single method. +They are conceptually very similar to a closure except that their only method +has a name. Moreover, they can be used as type-hints across your code. + +The :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireCallable` +attribute can be used to generate an adapter for a functional interface. +Let's say you have the following functional interface:: + + // src/Service/MessageFormatterInterface.php + namespace App\Service; + + interface MessageFormatterInterface + { + public function format(string $message, array $parameters): string; + } + +You also have a service that defines many methods and one of them is the same +``format()`` method of the previous interface:: + + // src/Service/MessageUtils.php + namespace App\Service; + + class MessageUtils + { + // other methods... + + public function format(string $message, array $parameters): string + { + // ... + } + } + +Thanks to the ``#[AutowireCallable]`` attribute, you can now inject this +``MessageUtils`` service as a functional interface implementation:: + + namespace App\Service\Mail; + + use App\Service\MessageFormatterInterface; + use App\Service\MessageUtils; + use Symfony\Component\DependencyInjection\Attribute\AutowireCallable; + + class Mailer + { + public function __construct( + #[AutowireCallable(service: MessageUtils::class, method: 'format')] + private MessageFormatterInterface $formatter + ) { + } + + public function sendMail(string $message, array $parameters): string + { + $formattedMessage = $this->formatter->format($message, $parameters); + + // ... + } + } + +Instead of using the ``#[AutowireCallable]`` attribute, you can also generate +an adapter for a functional interface through configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + + # ... + + app.message_formatter: + class: App\Service\MessageFormatterInterface + from_callable: [!service {class: 'App\Service\MessageUtils'}, 'format'] + + .. code-block:: xml + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Service\MessageFormatterInterface; + use App\Service\MessageUtils; + + return function(ContainerConfigurator $container) { + // ... + + $container + ->set('app.message_formatter', MessageFormatterInterface::class) + ->fromCallable([inline_service(MessageUtils::class), 'format']) + ->alias(MessageFormatterInterface::class, 'app.message_formatter') + ; + }; + +By doing so, Symfony will generate a class (also called an *adapter*) +implementing ``MessageFormatterInterface`` that will forward calls of +``MessageFormatterInterface::format()`` to your underlying service's method +``MessageUtils::format()``, with all its arguments. + Learn more ---------- diff --git a/service_container/alias_private.rst b/service_container/alias_private.rst index 8ccb131cf49..f99f7cb5f3e 100644 --- a/service_container/alias_private.rst +++ b/service_container/alias_private.rst @@ -55,7 +55,7 @@ You can also control the ``public`` option on a service-by-service basis: use App\Service\Foo; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(Foo::class) @@ -77,11 +77,6 @@ you want to configure:: // ... } -.. versionadded:: 5.3 - - The ``#[Autoconfigure]`` attribute was introduced in Symfony 5.3. PHP - attributes require at least PHP 8.0. - .. _services-why-private: Private services are special because they allow the container to optimize whether @@ -112,6 +107,20 @@ services. .. configuration-block:: + .. code-block:: php-attributes + + // src/Mail/PhpMailer.php + namespace App\Mail; + + // ... + use Symfony\Component\DependencyInjection\Attribute\AsAlias; + + #[AsAlias(id: 'app.mailer', public: true)] + class PhpMailer + { + // ... + } + .. code-block:: yaml # config/services.yaml @@ -147,7 +156,7 @@ services. use App\Mail\PhpMailer; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(PhpMailer::class) @@ -172,14 +181,27 @@ This means that when using the container directly, you can access the # ... app.mailer: '@App\Mail\PhpMailer' -Deprecating Service Aliases -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. tip:: + + When using ``#[AsAlias]`` attribute, you may omit passing ``id`` argument + if the class implements exactly one interface. ``MailerInterface`` will be + alias of ``PhpMailer``:: -.. versionadded:: 5.1 + // src/Mail/PhpMailer.php + namespace App\Mail; - The ``package`` and ``version`` options were introduced in Symfony 5.1. - Prior to 5.1, you had to use ``deprecated: true`` or - ``deprecated: 'Custom message'``. + // ... + use Symfony\Component\DependencyInjection\Attribute\AsAlias; + use Symfony\Component\Mailer\MailerInterface; + + #[AsAlias] + class PhpMailer implements MailerInterface + { + // ... + } + +Deprecating Service Aliases +~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you decide to deprecate the use of a service alias (because it is outdated or you decided not to maintain it anymore), you can deprecate its definition: @@ -295,11 +317,10 @@ The following example shows how to inject an anonymous service into another serv use App\AnonymousBar; use App\Foo; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(Foo::class) - // In versions earlier to Symfony 5.1 the inline_service() function was called inline() ->args([inline_service(AnonymousBar::class)]); }; @@ -347,7 +368,7 @@ Using an anonymous service as a factory looks like this: use App\AnonymousBar; use App\Foo; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(Foo::class) @@ -393,7 +414,7 @@ or you decided not to maintain it anymore), you can deprecate its definition: use App\Service\OldService; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(OldService::class) @@ -404,12 +425,6 @@ or you decided not to maintain it anymore), you can deprecate its definition: ); }; -.. versionadded:: 5.1 - - Starting from Symfony 5.1, the ``deprecated`` YAML option, the ```` - XML tag and the ``deprecate()`` PHP function require three arguments (the - package name, the version and the deprecation message). - Now, every time this service is used, a deprecation warning is triggered, advising you to stop or to change your uses of that service. diff --git a/service_container/autowiring.rst b/service_container/autowiring.rst index 6e86ee9c6f2..48bb40985b8 100644 --- a/service_container/autowiring.rst +++ b/service_container/autowiring.rst @@ -42,11 +42,9 @@ And now a Twitter client using this transformer:: class TwitterClient { - private $transformer; - - public function __construct(Rot13Transformer $transformer) - { - $this->transformer = $transformer; + public function __construct( + private Rot13Transformer $transformer, + ) { } public function tweet(User $user, string $key, string $status): void @@ -104,7 +102,7 @@ both services: .. code-block:: php // config/services.php - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services() ->defaults() ->autowire() @@ -128,13 +126,11 @@ Now, you can use the ``TwitterClient`` service immediately in a controller:: use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; class DefaultController extends AbstractController { - /** - * @Route("/tweet", methods={"POST"}) - */ + #[Route('/tweet')] public function tweet(TwitterClient $twitterClient, Request $request): Response { // fetch $user, $key, $status from the POST'ed data @@ -165,9 +161,9 @@ Autowiring works by reading the ``Rot13Transformer`` *type-hint* in ``TwitterCli { // ... - public function __construct(Rot13Transformer $transformer) - { - $this->transformer = $transformer; + public function __construct( + private Rot13Transformer $transformer, + ) { } } @@ -242,7 +238,7 @@ adding a service alias: use App\Util\Rot13Transformer; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { // ... // the id is not a class, so it won't be used for autowiring @@ -296,8 +292,9 @@ Now that you have an interface, you should use this as your type-hint:: class TwitterClient { - public function __construct(TransformerInterface $transformer) - { + public function __construct( + private TransformerInterface $transformer, + ) { // ... } @@ -348,7 +345,7 @@ To fix that, add an :ref:`alias `: use App\Util\Rot13Transformer; use App\Util\TransformerInterface; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { // ... $services->set(Rot13Transformer::class); @@ -380,18 +377,15 @@ dealing with the ``TransformerInterface``. class DataFormatter { - public function __construct((NormalizerInterface&DenormalizerInterface)|SerializerInterface $transformer) - { + public function __construct( + private (NormalizerInterface&DenormalizerInterface)|SerializerInterface $transformer, + ) { // ... } // ... } -.. versionadded:: 5.4 - - The support of union and intersection types was introduced in Symfony 5.4. - .. _autowiring-multiple-implementations-same-type: Dealing with Multiple Implementations of the Same Type @@ -438,11 +432,9 @@ the injection:: class MastodonClient { - private $transformer; - - public function __construct(TransformerInterface $shoutyTransformer) - { - $this->transformer = $shoutyTransformer; + public function __construct( + private TransformerInterface $shoutyTransformer, + ) { } public function toot(User $user, string $key, string $status): void @@ -519,7 +511,7 @@ the injection:: use App\Util\TransformerInterface; use App\Util\UppercaseTransformer; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { // ... $services->set(Rot13Transformer::class)->autowire(); @@ -557,13 +549,35 @@ Another option is to use the ``#[Target]`` attribute. By adding this attribute to the argument you want to autowire, you can specify which service to inject by passing the name of the argument used in the named alias. This way, you can have multiple services implementing the same interface and keep the argument name -separate from any implementation name (like shown in the example above). +separate from any implementation name (like shown in the example above). In addition, +you'll get an exception in case you make any typo in the target name. .. warning:: The ``#[Target]`` attribute only accepts the name of the argument used in the named alias; it **does not** accept service ids or service aliases. +You can get a list of named autowiring aliases by running the ``debug:autowiring`` command:: + +.. code-block:: terminal + + $ php bin/console debug:autowiring LoggerInterface + + Autowirable Types + ================= + + The following classes & interfaces can be used as type-hints when autowiring: + (only showing classes/interfaces matching LoggerInterface) + + Describes a logger instance. + Psr\Log\LoggerInterface - alias:monolog.logger + Psr\Log\LoggerInterface $assetMapperLogger - target:asset_mapperLogger - alias:monolog.logger.asset_mapper + Psr\Log\LoggerInterface $cacheLogger - alias:monolog.logger.cache + Psr\Log\LoggerInterface $httpClientLogger - target:http_clientLogger - alias:monolog.logger.http_client + Psr\Log\LoggerInterface $mailerLogger - alias:monolog.logger.mailer + + [...] + Suppose you want to inject the ``App\Util\UppercaseTransformer`` service. You would use the ``#[Target]`` attribute by passing the name of the ``$shoutyTransformer`` argument:: @@ -575,12 +589,10 @@ the ``#[Target]`` attribute by passing the name of the ``$shoutyTransformer`` ar class MastodonClient { - private $transformer; - public function __construct( - #[Target('shoutyTransformer')] TransformerInterface $transformer, + #[Target('shoutyTransformer')] + private TransformerInterface $transformer, ) { - $this->transformer = $transformer; } } @@ -596,9 +608,7 @@ the ``#[Target]`` attribute by passing the name of the ``$shoutyTransformer`` ar The reason is that thanks to `PHP constructor promotion`_ this constructor argument is both a parameter and a class property. You can safely ignore this error message. -.. versionadded:: 5.3 - - The ``#[Target]`` attribute was introduced in Symfony 5.3. +.. _autowire-attribute: Fixing Non-Autowireable Arguments --------------------------------- @@ -607,43 +617,198 @@ Autowiring only works when your argument is an *object*. But if you have a scala argument (e.g. a string), this cannot be autowired: Symfony will throw a clear exception. -To fix this, you can :ref:`manually wire the problematic argument `. -You wire up the difficult arguments, Symfony takes care of the rest. +To fix this, you can :ref:`manually wire the problematic argument ` +in the service configuration. You wire up only the difficult arguments, +Symfony takes care of the rest. -.. _autowiring-calls: +You can also use the ``#[Autowire]`` parameter attribute to instruct the autowiring +logic about those arguments:: -Autowiring other Methods (e.g. Setters and Public Typed Properties) -------------------------------------------------------------------- + // src/Service/MessageGenerator.php + namespace App\Service; -When autowiring is enabled for a service, you can *also* configure the container -to call methods on your class when it's instantiated. For example, suppose you want -to inject the ``logger`` service, and decide to use setter-injection: + use Psr\Log\LoggerInterface; + use Symfony\Component\DependencyInjection\Attribute\Autowire; -.. configuration-block:: + class MessageGenerator + { + public function __construct( + #[Autowire(service: 'monolog.logger.request')] + private LoggerInterface $logger, + ) { + // ... + } + } - .. code-block:: php-annotations +The ``#[Autowire]`` attribute can also be used for :ref:`parameters `, +:doc:`complex expressions ` and even +:ref:`environment variables ` , +:doc:`including env variable processors `:: - // src/Util/Rot13Transformer.php - namespace App\Util; + // src/Service/MessageGenerator.php + namespace App\Service; - class Rot13Transformer + use Psr\Log\LoggerInterface; + use Symfony\Component\DependencyInjection\Attribute\Autowire; + + class MessageGenerator + { + public function __construct( + // use the %...% syntax for parameters + #[Autowire('%kernel.project_dir%/data')] + string $dataDir, + + // or use argument "param" + #[Autowire(param: 'kernel.debug')] + bool $debugMode, + + // expressions + #[Autowire(expression: 'service("App\\\Mail\\\MailerConfiguration").getMailerMethod()')] + string $mailerMethod, + + // environment variables + #[Autowire(env: 'SOME_ENV_VAR')] + string $senderName, + + // environment variables with processors + #[Autowire(env: 'bool:SOME_BOOL_ENV_VAR')] + bool $allowAttachments, + ) { + } + // ... + } + +.. _autowiring_closures: + +Generate Closures With Autowiring +--------------------------------- + +A **service closure** is an anonymous function that returns a service. This type +of instantiation is handy when you are dealing with lazy-loading. It is also +useful for non-shared service dependencies. + +Automatically creating a closure encapsulating the service instantiation can be +done with the +:class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireServiceClosure` +attribute:: + + // src/Service/Remote/MessageFormatter.php + namespace App\Service\Remote; + + use Symfony\Component\DependencyInjection\Attribute\AsAlias; + + #[AsAlias('third_party.remote_message_formatter')] + class MessageFormatter + { + public function __construct() { - private $logger; + // ... + } - /** - * @required - */ - public function setLogger(LoggerInterface $logger): void - { - $this->logger = $logger; - } + public function format(string $message): string + { + // ... + } + } - public function transform($value): string - { - $this->logger->info('Transforming '.$value); - // ... - } + // src/Service/MessageGenerator.php + namespace App\Service; + + use App\Service\Remote\MessageFormatter; + use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure; + + class MessageGenerator + { + public function __construct( + #[AutowireServiceClosure('third_party.remote_message_formatter')] + private \Closure $messageFormatterResolver, + ) { + } + + public function generate(string $message): void + { + $formattedMessage = ($this->messageFormatterResolver)()->format($message); + + // ... + } + } + +It is common that a service accepts a closure with a specific signature. +In this case, you can use the +:class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireCallable` attribute +to generate a closure with the same signature as a specific method of a service. When +this closure is called, it will pass all its arguments to the underlying service +function. If the closure needs to be called more than once, the service instance +is reused for repeated calls. Unlike a service closure, this will not +create extra instances of a non-shared service:: + + // src/Service/MessageGenerator.php + namespace App\Service; + + use Symfony\Component\DependencyInjection\Attribute\AutowireCallable; + + class MessageGenerator + { + public function __construct( + #[AutowireCallable(service: 'third_party.remote_message_formatter', method: 'format')] + private \Closure $formatCallable, + ) { + } + + public function generate(string $message): void + { + $formattedMessage = ($this->formatCallable)($message); + + // ... + } + } + +Finally, you can pass the ``lazy: true`` option to the +:class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireCallable` +attribute. By doing so, the callable will automatically be lazy, which means +that the encapsulated service will be instantiated **only** at the +closure's first call. + +The :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireMethodOf` +attribute provides a simpler way of specifying the name of the service method +by using the property name as method name:: + + // src/Service/MessageGenerator.php + namespace App\Service; + + use Symfony\Component\DependencyInjection\Attribute\AutowireMethodOf; + + class MessageGenerator + { + public function __construct( + #[AutowireMethodOf('third_party.remote_message_formatter')] + private \Closure $format, + ) { + } + + public function generate(string $message): void + { + $formattedMessage = ($this->format)($message); + + // ... } + } + +.. versionadded:: 7.1 + + The :class:`Symfony\Component\DependencyInjection\Attribute\\AutowireMethodOf` + attribute was introduced in Symfony 7.1. + +.. _autowiring-calls: + +Autowiring other Methods (e.g. Setters and Public Typed Properties) +------------------------------------------------------------------- + +When autowiring is enabled for a service, you can *also* configure the container +to call methods on your class when it's instantiated. For example, suppose you want +to inject the ``logger`` service, and decide to use setter-injection: + +.. configuration-block:: .. code-block:: php-attributes @@ -654,7 +819,7 @@ to inject the ``logger`` service, and decide to use setter-injection: class Rot13Transformer { - private $logger; + private LoggerInterface $logger; #[Required] public function setLogger(LoggerInterface $logger): void @@ -673,35 +838,12 @@ Autowiring will automatically call *any* method with the ``#[Required]`` attribu above it, autowiring each argument. If you need to manually wire some of the arguments to a method, you can always explicitly :doc:`configure the method call `. -If your PHP version doesn't support attributes (they were introduced in PHP 8), -you can use the ``@required`` annotation instead. - -.. versionadded:: 5.2 - - The ``#[Required]`` attribute was introduced in Symfony 5.2. - Despite property injection having some :ref:`drawbacks `, -autowiring with ``#[Required]`` or ``@required`` can also be applied to public +autowiring with ``#[Required]`` can also be applied to public typed properties: .. configuration-block:: - .. code-block:: php-annotations - - namespace App\Util; - - class Rot13Transformer - { - /** @required */ - public LoggerInterface $logger; - - public function transform($value) - { - $this->logger->info('Transforming '.$value); - // ... - } - } - .. code-block:: php-attributes namespace App\Util; @@ -713,17 +855,13 @@ typed properties: #[Required] public LoggerInterface $logger; - public function transform($value) + public function transform($value): void { $this->logger->info('Transforming '.$value); // ... } } -.. versionadded:: 5.1 - - Public typed properties autowiring was introduced in Symfony 5.1. - Autowiring Controller Action Methods ------------------------------------ diff --git a/service_container/calls.rst b/service_container/calls.rst index a40ca68e29c..cb364b59489 100644 --- a/service_container/calls.rst +++ b/service_container/calls.rst @@ -3,7 +3,7 @@ Service Method Calls and Setter Injection .. tip:: - If you're using autowiring, you can use ``#[Required]`` or ``@required`` to + If you're using autowiring, you can use ``#[Required]`` to :ref:`automatically configure method calls `. Usually, you'll want to inject your dependencies via the constructor. But sometimes, @@ -17,7 +17,7 @@ example:: class MessageGenerator { - private $logger; + private LoggerInterface $logger; public function setLogger(LoggerInterface $logger): void { @@ -66,11 +66,10 @@ To configure the container to call the ``setLogger`` method, use the ``calls`` k use App\Service\MessageGenerator; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { // ... $services->set(MessageGenerator::class) - // In versions earlier to Symfony 5.1 the service() function was called ref() ->call('setLogger', [service('logger')]); }; @@ -85,7 +84,7 @@ instead of mutating the object they were called on:: class MessageGenerator { - private $logger; + private LoggerInterface $logger; public function withLogger(LoggerInterface $logger): self { @@ -143,14 +142,11 @@ The configuration to tell the container it should do so would be like: .. tip:: - If autowire is enabled, you can also use annotations; with the previous + If autowire is enabled, you can also use attributes; with the previous example it would be:: - /** - * @required - * @return static - */ - public function withLogger(LoggerInterface $logger) + #[Required] + public function withLogger(LoggerInterface $logger): static { $new = clone $this; $new->logger = $logger; @@ -158,13 +154,7 @@ The configuration to tell the container it should do so would be like: return $new; } - You can also leverage the PHP 8 ``static`` return type instead of the - ``@return static`` annotation. If you don't want a method with a - PHP 8 ``static`` return type and a ``@required`` annotation to behave as - a wither, you can add a ``@return $this`` annotation to disable the - *returns clone* feature. - - .. versionadded:: 5.1 - - Support for the PHP 8 ``static`` return type was introduced in - Symfony 5.1. + If you don't want a method with a ``static`` return type and + a ``#[Required]`` attribute to behave as a wither, you can + add a ``@return $this`` annotation to disable the *returns clone* + feature. diff --git a/service_container/compiler_passes.rst b/service_container/compiler_passes.rst index fda044a1195..fc5728685e0 100644 --- a/service_container/compiler_passes.rst +++ b/service_container/compiler_passes.rst @@ -75,9 +75,9 @@ method in the extension):: use App\DependencyInjection\Compiler\CustomPass; use Symfony\Component\DependencyInjection\ContainerBuilder; - use Symfony\Component\HttpKernel\Bundle\Bundle; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; - class MyBundle extends Bundle + class MyBundle extends AbstractBundle { public function build(ContainerBuilder $container): void { diff --git a/service_container/configurators.rst b/service_container/configurators.rst index 1d289580815..7817a383761 100644 --- a/service_container/configurators.rst +++ b/service_container/configurators.rst @@ -23,7 +23,7 @@ You start defining a ``NewsletterManager`` class like this:: class NewsletterManager implements EmailFormatterAwareInterface { - private $enabledFormatters; + private array $enabledFormatters; public function setEnabledFormatters(array $enabledFormatters): void { @@ -40,7 +40,7 @@ and also a ``GreetingCardManager`` class:: class GreetingCardManager implements EmailFormatterAwareInterface { - private $enabledFormatters; + private array $enabledFormatters; public function setEnabledFormatters(array $enabledFormatters): void { @@ -82,11 +82,9 @@ to create a configurator class to configure these instances:: class EmailConfigurator { - private $formatterManager; - - public function __construct(EmailFormatterManager $formatterManager) - { - $this->formatterManager = $formatterManager; + public function __construct( + private EmailFormatterManager $formatterManager, + ) { } public function configure(EmailFormatterAwareInterface $emailManager): void @@ -169,14 +167,13 @@ all the classes are already loaded as services. All you need to do is specify th use App\Mail\GreetingCardManager; use App\Mail\NewsletterManager; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); // Registers all 4 classes as services, including App\Mail\EmailConfigurator $services->load('App\\', '../src/*'); // override the services to set the configurator - // In versions earlier to Symfony 5.1 the service() function was called ref() $services->set(NewsletterManager::class) ->configurator([service(EmailConfigurator::class), 'configure']); @@ -239,7 +236,7 @@ Services can be configured via invokable configurators (replacing the use App\Mail\GreetingCardManager; use App\Mail\NewsletterManager; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); // Registers all 4 classes as services, including App\Mail\EmailConfigurator diff --git a/service_container/debug.rst b/service_container/debug.rst index 1e460b03770..c09413e7213 100644 --- a/service_container/debug.rst +++ b/service_container/debug.rst @@ -17,6 +17,31 @@ To see a list of all of the available types that can be used for autowiring, run $ php bin/console debug:autowiring +Debugging Service Tags +---------------------- + +Run the following command to find out what services are :doc:`tagged ` +with a specific tag: + +.. code-block:: terminal + + $ php bin/console debug:container --tag=kernel.event_listener + +Partial search is also available: + +.. code-block:: terminal + + $ php bin/console debug:container --tag=kernel + + Select one of the following tags to display its information: + [0] kernel.event_listener + [1] kernel.event_subscriber + [2] kernel.reset + [3] kernel.cache_warmer + [4] kernel.locale_aware + [5] kernel.fragment_renderer + [6] kernel.cache_clearer + Detailed Info about a Single Service ------------------------------------ diff --git a/service_container/expression_language.rst b/service_container/expression_language.rst index f1de823e47b..41c538db468 100644 --- a/service_container/expression_language.rst +++ b/service_container/expression_language.rst @@ -55,7 +55,7 @@ to another service: ``App\Mailer``. One way to do this is with an expression: use App\Mail\MailerConfiguration; use App\Mailer; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { // ... $services->set(MailerConfiguration::class); @@ -67,12 +67,14 @@ to another service: ``App\Mailer``. One way to do this is with an expression: Learn more about the :doc:`expression language syntax `. -In this context, you have access to 2 functions: +In this context, you have access to 3 functions: ``service`` Returns a given service (see the example above). ``parameter`` Returns a specific parameter value (syntax is like ``service``). +``env`` + Returns the value of an env variable. You also have access to the :class:`Symfony\\Component\\DependencyInjection\\Container` via a ``container`` variable. Here's another example: @@ -110,7 +112,7 @@ via a ``container`` variable. Here's another example: use App\Mailer; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(Mailer::class) @@ -118,4 +120,5 @@ via a ``container`` variable. Here's another example: }; Expressions can be used in ``arguments``, ``properties``, as arguments with -``configurator`` and as arguments to ``calls`` (method calls). +``configurator``, as arguments to ``calls`` (method calls) and in +``factories`` (:doc:`service factories `). diff --git a/service_container/factories.rst b/service_container/factories.rst index 3f13655c6cb..0c6a4724609 100644 --- a/service_container/factories.rst +++ b/service_container/factories.rst @@ -74,7 +74,7 @@ create its object: use App\Email\NewsletterManager; use App\Email\NewsletterManagerStaticFactory; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(NewsletterManager::class) @@ -155,7 +155,7 @@ You can omit the class on the factory declaration: use App\Email\NewsletterManager; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); // Note that we are not using service() @@ -163,6 +163,73 @@ You can omit the class on the factory declaration: ->factory([null, 'create']); }; +It is also possible to use the ``constructor`` option, instead of passing ``null`` +as the factory class: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Email/NewsletterManager.php + namespace App\Email; + + use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + + #[Autoconfigure(bind: ['$sender' => 'fabien@symfony.com'], constructor: 'create')] + class NewsletterManager + { + private string $sender; + + public static function create(string $sender): self + { + $newsletterManager = new self(); + $newsletterManager->sender = $sender; + // ... + + return $newsletterManager; + } + } + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\Email\NewsletterManager: + constructor: 'create' + arguments: + $sender: 'fabien@symfony.com' + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Email\NewsletterManager; + + return function(ContainerConfigurator $containerConfigurator) { + $services = $containerConfigurator->services(); + + $services->set(NewsletterManager::class) + ->constructor('create'); + }; + Non-Static Factories -------------------- @@ -217,7 +284,7 @@ Configuration of the service container then looks like this: use App\Email\NewsletterManager; use App\Email\NewsletterManagerFactory; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); // first, create a service for the factory @@ -226,7 +293,6 @@ Configuration of the service container then looks like this: // second, use the factory service as the first argument of the 'factory' // method and the factory method as the second argument $services->set(NewsletterManager::class) - // In versions earlier to Symfony 5.1 the service() function was called ref() ->factory([service(NewsletterManagerFactory::class), 'createNewsletterManager']); }; @@ -296,13 +362,85 @@ method name: use App\Email\NewsletterManager; use App\Email\NewsletterManagerFactory; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(NewsletterManager::class) ->factory(service(InvokableNewsletterManagerFactory::class)); }; +Using Expressions in Service Factories +-------------------------------------- + +Instead of using PHP classes as a factory, you can also use +:doc:`expressions `. This allows you to +e.g. change the service based on a parameter: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + App\Email\NewsletterManagerInterface: + # use the "tracable_newsletter" service when debug is enabled, "newsletter" otherwise. + # "@=" indicates that this is an expression + factory: '@=parameter("kernel.debug") ? service("tracable_newsletter") : service("newsletter")' + + # you can use the arg() function to retrieve an argument from the definition + App\Email\NewsletterManagerInterface: + factory: "@=arg(0).createNewsletterManager() ?: service("default_newsletter_manager")" + arguments: + - '@App\Email\NewsletterManagerFactory' + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Email\NewsletterManagerFactory; + use App\Email\NewsletterManagerInterface; + + return function(ContainerConfigurator $containerConfigurator) { + $services = $containerConfigurator->services(); + + $services->set(NewsletterManagerInterface::class) + // use the "tracable_newsletter" service when debug is enabled, "newsletter" otherwise. + ->factory(expr("parameter('kernel.debug') ? service('tracable_newsletter') : service('newsletter')")) + ; + + // you can use the arg() function to retrieve an argument from the definition + $services->set(NewsletterManagerInterface::class) + ->factory(expr("arg(0).createNewsletterManager() ?: service('default_newsletter_manager')")) + ->args([ + service(NewsletterManagerFactory::class), + ]) + ; + }; + .. _factories-passing-arguments-factory-method: Passing Arguments to the Factory Method @@ -356,7 +494,7 @@ previous examples takes the ``templating`` service as an argument: use App\Email\NewsletterManager; use App\Email\NewsletterManagerFactory; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(NewsletterManager::class) diff --git a/service_container/import.rst b/service_container/import.rst index 1e0fcfb2cee..d5056032115 100644 --- a/service_container/import.rst +++ b/service_container/import.rst @@ -116,7 +116,7 @@ a relative or absolute path to the imported file: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $container->import('services/mailer.php'); // If you want to import a whole directory: $container->import('services/'); diff --git a/service_container/injection_types.rst b/service_container/injection_types.rst index d801ae0210d..f56458b4c20 100644 --- a/service_container/injection_types.rst +++ b/service_container/injection_types.rst @@ -22,11 +22,9 @@ the dependency:: // ... class NewsletterManager { - private $mailer; - - public function __construct(MailerInterface $mailer) - { - $this->mailer = $mailer; + public function __construct( + private MailerInterface $mailer, + ) { } // ... @@ -71,11 +69,10 @@ service container configuration: use App\Mail\NewsletterManager; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(NewsletterManager::class) - // In versions earlier to Symfony 5.1 the service() function was called ref() ->args(service('mailer')); }; @@ -114,15 +111,16 @@ by cloning the original service, this approach allows you to make a service immu // ... use Symfony\Component\Mailer\MailerInterface; + use Symfony\Contracts\Service\Attribute\Required; class NewsletterManager { - private $mailer; + private MailerInterface $mailer; /** - * @required * @return static */ + #[Required] public function withMailer(MailerInterface $mailer): self { $new = clone $this; @@ -182,8 +180,9 @@ In order to use this type of injection, don't forget to configure it: .. note:: If you decide to use autowiring, this type of injection requires - that you add a ``@return static`` docblock in order for the container - to be capable of registering the method. + that you add a ``@return static`` docblock or the ``static`` return + type in order for the container to be capable of registering + the method. This approach is useful if you need to configure your service according to your needs, so, here's the advantages of immutable-setters: @@ -218,14 +217,14 @@ that accepts the dependency:: // src/Mail/NewsletterManager.php namespace App\Mail; + use Symfony\Contracts\Service\Attribute\Required; + // ... class NewsletterManager { - private $mailer; + private MailerInterface $mailer; - /** - * @required - */ + #[Required] public function setMailer(MailerInterface $mailer): void { $this->mailer = $mailer; @@ -274,7 +273,7 @@ that accepts the dependency:: use App\Mail\NewsletterManager; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(NewsletterManager::class) @@ -313,7 +312,7 @@ Another possibility is setting public fields of the class directly:: // ... class NewsletterManager { - public $mailer; + public MailerInterface $mailer; // ... } @@ -356,7 +355,7 @@ Another possibility is setting public fields of the class directly:: use App\Mail\NewsletterManager; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set('app.newsletter_manager', NewsletterManager::class) diff --git a/service_container/lazy_services.rst b/service_container/lazy_services.rst index 38d2f2186f0..41f27d8448f 100644 --- a/service_container/lazy_services.rst +++ b/service_container/lazy_services.rst @@ -29,16 +29,6 @@ until you interact with the proxy in some way. In PHP versions prior to 8.0 lazy services do not support parameters with default values for built-in PHP classes (e.g. ``PDO``). -Installation ------------- - -In order to use the lazy service instantiation, you will need to install the -``symfony/proxy-manager-bridge`` package: - -.. code-block:: terminal - - $ composer require symfony/proxy-manager-bridge - .. _lazy-services_configuration: Configuration @@ -76,29 +66,22 @@ You can mark the service as ``lazy`` by manipulating its definition: use App\Twig\AppExtension; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(AppExtension::class)->lazy(); }; -Once you inject the service into another service, a virtual `proxy`_ with the -same signature of the class representing the service should be injected. The -same happens when calling ``Container::get()`` directly. - -The actual class will be instantiated as soon as you try to interact with the -service (e.g. call one of its methods). +Once you inject the service into another service, a lazy ghost object with the +same signature of the class representing the service should be injected. A lazy +`ghost object`_ is an object that is created empty and that is able to initialize +itself when being accessed for the first time). The same happens when calling +``Container::get()`` directly. -To check if your proxy works you can check the interface of the received object:: +To check if your lazy service works you can check the interface of the received object:: dump(class_implements($service)); - // the output should include "ProxyManager\Proxy\LazyLoadingInterface" - -.. note:: - - If you don't install the `ProxyManager bridge`_ , the container will skip - over the ``lazy`` flag and directly instantiate the service as it would - normally do. + // the output should include "Symfony\Component\VarExporter\LazyObjectInterface" You can also configure your service's laziness thanks to the :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Autoconfigure` attribute. @@ -115,10 +98,57 @@ For example, to define your service as lazy use the following:: // ... } -.. versionadded:: 5.4 +You can also configure laziness when your service is injected with the +:class:`Symfony\\Component\\DependencyInjection\\Attribute\\Autowire` attribute:: + + namespace App\Service; + + use App\Twig\AppExtension; + use Symfony\Component\DependencyInjection\Attribute\Autowire; + + class MessageGenerator + { + public function __construct( + #[Autowire(service: 'app.twig.app_extension', lazy: true)] ExtensionInterface $extension + ) { + // ... + } + } + +This attribute also allows you to define the interfaces to proxy when using +laziness, and supports lazy-autowiring of union types:: + + public function __construct( + #[Autowire(service: 'foo', lazy: FooInterface::class)] + FooInterface|BarInterface $foo, + ) { + } + +Another possibility is to use the :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Lazy` attribute:: + + namespace App\Twig; + + use Symfony\Component\DependencyInjection\Attribute\Lazy; + use Twig\Extension\ExtensionInterface; + + #[Lazy] + class AppExtension implements ExtensionInterface + { + // ... + } + +This attribute can be applied to both class and parameters that should be lazy-loaded. +It defines an optional parameter used to define interfaces for proxy and intersection types:: + + public function __construct( + #[Lazy(FooInterface::class)] + FooInterface|BarInterface $foo, + ) { + } + +.. versionadded:: 7.1 - The :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Autoconfigure` attribute - was introduced in Symfony 5.4. + The ``#[Lazy]`` attribute was introduced in Symfony 7.1. Interface Proxifying -------------------- @@ -169,7 +199,7 @@ specific interfaces. use App\Twig\AppExtension; use Twig\Extension\ExtensionInterface; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(AppExtension::class) @@ -194,11 +224,6 @@ parameter value:: // ... } -.. versionadded:: 5.4 - - The :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Autoconfigure` attribute - was introduced in Symfony 5.4. - The virtual `proxy`_ injected into other services will only implement the specified interfaces and will not extend the original service class, allowing to lazy load services using `final`_ classes. You can configure the proxy to @@ -212,13 +237,6 @@ implement multiple interfaces by adding new "proxy" tags. prevents injecting the dependency at all if you type-hinted a concrete implementation instead of the interface. -Additional Resources --------------------- - -You can read more about how proxies are instantiated, generated and initialized -in the `documentation of ProxyManager`_. - -.. _`ProxyManager bridge`: https://github.com/symfony/symfony/tree/master/src/Symfony/Bridge/ProxyManager -.. _`proxy`: https://en.wikipedia.org/wiki/Proxy_pattern -.. _`documentation of ProxyManager`: https://github.com/Ocramius/ProxyManager/blob/master/docs/lazy-loading-value-holder.md +.. _`ghost object`: https://en.wikipedia.org/wiki/Lazy_loading#Ghost .. _`final`: https://www.php.net/manual/en/language.oop5.final.php +.. _`proxy`: https://en.wikipedia.org/wiki/Proxy_pattern diff --git a/service_container/optional_dependencies.rst b/service_container/optional_dependencies.rst index 86aa0c2eb22..bc8f03cf7e0 100644 --- a/service_container/optional_dependencies.rst +++ b/service_container/optional_dependencies.rst @@ -38,11 +38,10 @@ if the service does not exist: use App\Newsletter\NewsletterManager; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(NewsletterManager::class) - // In versions earlier to Symfony 5.1 the service() function was called ref() ->args([service('logger')->nullOnInvalid()]); }; @@ -95,7 +94,7 @@ call if the service exists and remove the method call if it does not: use App\Newsletter\NewsletterManager; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(NewsletterManager::class) diff --git a/service_container/parent_services.rst b/service_container/parent_services.rst index b3792dc5a6a..b82222c43af 100644 --- a/service_container/parent_services.rst +++ b/service_container/parent_services.rst @@ -9,18 +9,17 @@ you may have multiple repository classes which need the // src/Repository/BaseDoctrineRepository.php namespace App\Repository; - use Doctrine\Persistence\ObjectManager; + use Doctrine\ORM\EntityManager; use Psr\Log\LoggerInterface; // ... abstract class BaseDoctrineRepository { - protected $objectManager; - protected $logger; + protected LoggerInterface $logger; - public function __construct(ObjectManager $objectManager) - { - $this->objectManager = $objectManager; + public function __construct( + protected EntityManager $entityManager, + ) { } public function setLogger(LoggerInterface $logger): void @@ -119,13 +118,12 @@ avoid duplicated service definitions: use App\Repository\DoctrinePostRepository; use App\Repository\DoctrineUserRepository; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(BaseDoctrineRepository::class) ->abstract() ->args([service('doctrine.orm.entity_manager')]) - // In versions earlier to Symfony 5.1 the service() function was called ref() ->call('setLogger', [service('logger')]) ; @@ -229,7 +227,7 @@ the child class: use App\Repository\DoctrineUserRepository; // ... - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(BaseDoctrineRepository::class) diff --git a/service_container/request.rst b/service_container/request.rst index 35a20b8d69f..1abb289983f 100644 --- a/service_container/request.rst +++ b/service_container/request.rst @@ -14,14 +14,12 @@ method:: class NewsletterManager { - protected $requestStack; - - public function __construct(RequestStack $requestStack) - { - $this->requestStack = $requestStack; + public function __construct( + protected RequestStack $requestStack, + ) { } - public function anyMethod() + public function anyMethod(): void { $request = $this->requestStack->getCurrentRequest(); // ... do something with the request diff --git a/service_container/service_closures.rst b/service_container/service_closures.rst index 990ba8b813c..cedbaaa2bf9 100644 --- a/service_container/service_closures.rst +++ b/service_container/service_closures.rst @@ -1,10 +1,6 @@ Service Closures ================ -.. versionadded:: 5.4 - - The ``service_closure()`` function was introduced in Symfony 5.4. - This feature wraps the injected service into a closure allowing it to be lazily loaded when and if needed. This is useful if the service being injected is a bit heavy to instantiate @@ -21,13 +17,11 @@ all subsequent calls return the same instance, unless the service is class MyService { /** - * @var callable(): MailerInterface + * @param callable(): MailerInterface */ - private \Closure $mailer; - - public function __construct(\Closure $mailer) - { - $this->mailer = $mailer; + public function __construct( + private \Closure $mailer, + ) { } public function doSomething(): void @@ -85,7 +79,7 @@ argument of type ``service_closure``: use App\Service\MyService; - return function (ContainerConfigurator $container) { + return function (ContainerConfigurator $container): void { $services = $container->services(); $services->set(MyService::class) @@ -96,6 +90,11 @@ argument of type ``service_closure``: // ->args([service_closure('mailer')->ignoreOnInvalid()]); }; +.. seealso:: + + Service closures can be injected :ref:`by using autowiring ` + and its dedicated attributes. + .. seealso:: Another way to inject services lazily is via a diff --git a/service_container/service_decoration.rst b/service_container/service_decoration.rst index 08bff60b534..9b1e4d44e1f 100644 --- a/service_container/service_decoration.rst +++ b/service_container/service_decoration.rst @@ -41,7 +41,7 @@ When overriding an existing definition, the original service is lost: use App\Mailer; use App\NewMailer; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(Mailer::class); @@ -59,6 +59,20 @@ but keeps a reference of the old one as ``.inner``: .. configuration-block:: + .. code-block:: php-attributes + + // src/DecoratingMailer.php + namespace App; + + // ... + use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + + #[AsDecorator(decorates: Mailer::class)] + class DecoratingMailer + { + // ... + } + .. code-block:: yaml # config/services.yaml @@ -98,7 +112,7 @@ but keeps a reference of the old one as ``.inner``: use App\DecoratingMailer; use App\Mailer; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(Mailer::class); @@ -122,6 +136,27 @@ automatically changed to ``'.inner'``): .. configuration-block:: + .. code-block:: php-attributes + + // src/DecoratingMailer.php + namespace App; + + // ... + use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; + + #[AsDecorator(decorates: Mailer::class)] + class DecoratingMailer + { + public function __construct( + #[AutowireDecorated] + private object $inner, + ) { + } + + // ... + } + .. code-block:: yaml # config/services.yaml @@ -161,7 +196,7 @@ automatically changed to ``'.inner'``): use App\DecoratingMailer; use App\Mailer; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(Mailer::class); @@ -169,15 +204,9 @@ automatically changed to ``'.inner'``): $services->set(DecoratingMailer::class) ->decorate(Mailer::class) // pass the old service as an argument - // In versions earlier to Symfony 5.1 the service() function was called ref() ->args([service('.inner')]); }; -.. versionadded:: 5.1 - - The special ``.inner`` value was introduced in Symfony 5.1. In previous - versions you needed to use: ``decorating_service_id + '.inner'``. - .. tip:: The visibility of the decorated ``App\Mailer`` service (which is an alias @@ -233,7 +262,7 @@ automatically changed to ``'.inner'``): use App\DecoratingMailer; use App\Mailer; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(Mailer::class); @@ -252,6 +281,35 @@ the ``decoration_priority`` option. Its value is an integer that defaults to .. configuration-block:: + .. code-block:: php-attributes + + // ... + use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; + + #[AsDecorator(decorates: Foo::class, priority: 5)] + class Bar + { + public function __construct( + #[AutowireDecorated] + private $inner, + ) { + } + // ... + } + + #[AsDecorator(decorates: Foo::class, priority: 1)] + class Baz + { + public function __construct( + #[AutowireDecorated] + private $inner, + ) { + } + + // ... + } + .. code-block:: yaml # config/services.yaml @@ -295,7 +353,7 @@ the ``decoration_priority`` option. Its value is an integer that defaults to // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(\Foo::class); @@ -381,7 +439,7 @@ ordered services, each one decorating the next: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $container->services() ->stack('decorated_foo_stack', [ inline_service(\Baz::class)->args([service('.inner')]), @@ -464,7 +522,7 @@ advanced example of composition: use App\Decorated; use App\Decorator; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $container->services() ->set('some_decorator', Decorator::class) @@ -529,10 +587,6 @@ The result will be:: The ``Baz`` frame id will now be ``.decorated_foo_stack.second``. -.. versionadded:: 5.1 - - The ability to define ``stack`` was introduced in Symfony 5.1. - Control the Behavior When the Decorated Service Does Not Exist -------------------------------------------------------------- @@ -547,6 +601,24 @@ Three different behaviors are available: .. configuration-block:: + .. code-block:: php-attributes + + // ... + use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; + use Symfony\Component\DependencyInjection\ContainerInterface; + + #[AsDecorator(decorates: Mailer::class, onInvalid: ContainerInterface::IGNORE_ON_INVALID_REFERENCE)] + class Bar + { + public function __construct( + private #[AutowireDecorated] $inner, + ) { + } + + // ... + } + .. code-block:: yaml # config/services.yaml @@ -582,7 +654,7 @@ Three different behaviors are available: use Symfony\Component\DependencyInjection\ContainerInterface; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(Foo::class); @@ -605,11 +677,9 @@ Three different behaviors are available: class DecoratorService { - private $decorated; - - public function __construct(?OptionalService $decorated) - { - $this->decorated = $decorated; + public function __construct( + private ?OptionalService $decorated, + ) { } public function tellInterestingStuff(): string diff --git a/service_container/service_subscribers_locators.rst b/service_container/service_subscribers_locators.rst index 530519afd52..8afbb767e45 100644 --- a/service_container/service_subscribers_locators.rst +++ b/service_container/service_subscribers_locators.rst @@ -27,24 +27,22 @@ to handle their respective command when it is asked for:: class CommandBus { /** - * @var CommandHandler[] + * @param CommandHandler[] $handlerMap */ - private $handlerMap; - - public function __construct(array $handlerMap) - { - $this->handlerMap = $handlerMap; + public function __construct( + private array $handlerMap, + ) { } - public function handle(Command $command) + public function handle(Command $command): mixed { $commandClass = get_class($command); - if (!isset($this->handlerMap[$commandClass])) { + if (!$handler = $this->handlerMap[$commandClass] ?? null) { return; } - return $this->handlerMap[$commandClass]->handle($command); + return $handler->handle($command); } } @@ -69,8 +67,7 @@ Defining a Service Subscriber First, turn ``CommandBus`` into an implementation of :class:`Symfony\\Contracts\\Service\\ServiceSubscriberInterface`. Use its ``getSubscribedServices()`` method to include as many services as needed -in the service subscriber and change the type hint of the container to -a PSR-11 ``ContainerInterface``:: +in the service subscriber:: // src/CommandBus.php namespace App; @@ -82,11 +79,9 @@ a PSR-11 ``ContainerInterface``:: class CommandBus implements ServiceSubscriberInterface { - private $locator; - - public function __construct(ContainerInterface $locator) - { - $this->locator = $locator; + public function __construct( + private ContainerInterface $locator, + ) { } public static function getSubscribedServices(): array @@ -97,7 +92,7 @@ a PSR-11 ``ContainerInterface``:: ]; } - public function handle(Command $command) + public function handle(Command $command): mixed { $commandClass = get_class($command); @@ -115,14 +110,36 @@ a PSR-11 ``ContainerInterface``:: that you have :ref:`autoconfigure ` enabled. You can also manually add the ``container.service_subscriber`` tag. -The injected service is an instance of :class:`Symfony\\Component\\DependencyInjection\\ServiceLocator` -which implements the PSR-11 ``ContainerInterface``, but it is also a callable:: +A service locator is a `PSR-11 container`_ that contains a set of services, +but only instantiates them when they are actually used. Consider the following code:: // ... - $handler = ($this->locator)($commandClass); + $handler = $this->locator->get($commandClass); return $handler->handle($command); +In this example, the ``$handler`` service is only instantiated when the +``$this->locator->get($commandClass)`` method is called. + +You can also type-hint the service locator argument with +:class:`Symfony\\Contracts\\Service\\ServiceCollectionInterface` instead of +``Psr\Container\ContainerInterface``. By doing so, you'll be able to +count and iterate over the services of the locator:: + + // ... + $numberOfHandlers = count($this->locator); + $nameOfHandlers = array_keys($this->locator->getProvidedServices()); + + // you can iterate through all services of the locator + foreach ($this->locator as $serviceId => $service) { + // do something with the service, the service id or both + } + +.. versionadded:: 7.1 + + The :class:`Symfony\\Contracts\\Service\\ServiceCollectionInterface` was + introduced in Symfony 7.1. + Including Services ------------------ @@ -233,7 +250,7 @@ service type to a service. use App\CommandBus; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(CommandBus::class) @@ -245,6 +262,149 @@ service type to a service. The ``key`` attribute can be omitted if the service name internally is the same as in the service container. +Add Dependency Injection Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As an alternate to aliasing services in your configuration, you can also configure +the following dependency injection attributes in the ``getSubscribedServices()`` +method directly: + +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Autowire` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedIterator` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedLocator` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Target` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireDecorated` + +This is done by having ``getSubscribedServices()`` return an array of +:class:`Symfony\\Contracts\\Service\\Attribute\\SubscribedService` objects +(these can be combined with standard ``string[]`` values):: + + use Psr\Container\ContainerInterface; + use Psr\Log\LoggerInterface; + use Symfony\Component\DependencyInjection\Attribute\Autowire; + use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; + use Symfony\Component\DependencyInjection\Attribute\TaggedLocator; + use Symfony\Component\DependencyInjection\Attribute\Target; + use Symfony\Contracts\Service\Attribute\SubscribedService; + + public static function getSubscribedServices(): array + { + return [ + // ... + new SubscribedService('logger', LoggerInterface::class, attributes: new Autowire(service: 'monolog.logger.event')), + + // can event use parameters + new SubscribedService('env', 'string', attributes: new Autowire('%kernel.environment%')), + + // Target + new SubscribedService('event.logger', LoggerInterface::class, attributes: new Target('eventLogger')), + + // TaggedIterator + new SubscribedService('loggers', 'iterable', attributes: new TaggedIterator('logger.tag')), + + // TaggedLocator + new SubscribedService('handlers', ContainerInterface::class, attributes: new TaggedLocator('handler.tag')), + ]; + } + +.. deprecated:: 7.1 + + The :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedIterator` + and :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedLocator` + attributes were deprecated in Symfony 7.1 in favor of + :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireIterator` + and :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireLocator`. + +.. note:: + + The above example requires using ``3.2`` version or newer of ``symfony/service-contracts``. + +.. _service-locator_autowire-locator: +.. _service-locator_autowire-iterator: + +The AutowireLocator and AutowireIterator Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Another way to define a service locator is to use the +:class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireLocator` +attribute:: + + // src/CommandBus.php + namespace App; + + use App\CommandHandler\BarHandler; + use App\CommandHandler\FooHandler; + use Psr\Container\ContainerInterface; + use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; + + class CommandBus + { + public function __construct( + #[AutowireLocator([ + FooHandler::class, + BarHandler::class, + ])] + private ContainerInterface $handlers, + ) { + } + + public function handle(Command $command): mixed + { + $commandClass = get_class($command); + + if ($this->handlers->has($commandClass)) { + $handler = $this->handlers->get($commandClass); + + return $handler->handle($command); + } + } + } + +Just like with the ``getSubscribedServices()`` method, it is possible +to define aliased services thanks to the array keys, as well as optional +services, plus you can nest it with +:class:`Symfony\\Contracts\\Service\\Attribute\\SubscribedService` +attribute:: + + // src/CommandBus.php + namespace App; + + use App\CommandHandler\BarHandler; + use App\CommandHandler\BazHandler; + use App\CommandHandler\FooHandler; + use Psr\Container\ContainerInterface; + use Symfony\Component\DependencyInjection\Attribute\Autowire; + use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; + use Symfony\Contracts\Service\Attribute\SubscribedService; + + class CommandBus + { + public function __construct( + #[AutowireLocator([ + 'foo' => FooHandler::class, + 'bar' => new SubscribedService(type: 'string', attributes: new Autowire('%some.parameter%')), + 'optionalBaz' => '?'.BazHandler::class, + ])] + private ContainerInterface $handlers, + ) { + } + + public function handle(Command $command): mixed + { + $fooHandler = $this->handlers->get('foo'); + + // ... + } + } + +.. note:: + + To receive an iterable instead of a service locator, you can switch the + :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireLocator` + attribute to + :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireIterator` + attribute. + .. _service-subscribers-locators_defining-service-locator: Defining a Service Locator @@ -256,15 +416,16 @@ argument of type ``service_locator``. Consider the following ``CommandBus`` class where you want to inject some services into it via a service locator:: - // src/HandlerCollection.php + // src/CommandBus.php namespace App; - use Symfony\Component\DependencyInjection\ServiceLocator; + use Psr\Container\ContainerInterface; class CommandBus { - public function __construct(ServiceLocator $locator) - { + public function __construct( + private ContainerInterface $locator, + ) { } } @@ -278,14 +439,15 @@ or directly via PHP attributes: // src/CommandBus.php namespace App; - use Symfony\Component\DependencyInjection\Attribute\TaggedLocator; - use Symfony\Component\DependencyInjection\ServiceLocator; + use Psr\Container\ContainerInterface; + use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; class CommandBus { public function __construct( // creates a service locator with all the services tagged with 'app.handler' - #[TaggedLocator('app.handler')] ServiceLocator $locator + #[AutowireLocator('app.handler')] + private ContainerInterface $locator, ) { } } @@ -325,12 +487,11 @@ or directly via PHP attributes: use App\CommandBus; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(CommandBus::class) ->args([service_locator([ - // In versions earlier to Symfony 5.1 the service() function was called ref() 'App\FooCommand' => service('app.command_handler.foo'), 'App\BarCommand' => service('app.command_handler.bar'), ])]); @@ -340,10 +501,6 @@ As shown in the previous sections, the constructor of the ``CommandBus`` class must type-hint its argument with ``ContainerInterface``. Then, you can get any of the service locator services via their ID (e.g. ``$this->locator->get('App\FooCommand')``). -.. versionadded:: 5.3 - - The ``#[TaggedLocator]`` attribute was introduced in Symfony 5.3 and requires PHP 8. - Reusing a Service Locator in Multiple Services ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -368,13 +525,6 @@ other services. To do so, create a new service definition using the # add the following tag to the service definition: # tags: ['container.service_locator'] - # if the element has no key, the ID of the original service is used - app.another_command_handler_locator: - class: Symfony\Component\DependencyInjection\ServiceLocator - arguments: - - - - '@app.command_handler.baz' - .. code-block:: xml @@ -389,8 +539,6 @@ other services. To do so, create a new service definition using the - - + + + + + + + + + + + + + + + App\Handler\Three + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $containerConfigurator) { + $services = $containerConfigurator->services(); + + // ... + + // This is the service we want to exclude, even if the 'app.handler' tag is attached + $services->set(App\Handler\Three::class) + ->tag('app.handler') + ; + + $services->set(App\HandlerCollection::class) + // inject all services tagged with app.handler as first argument + ->args([tagged_iterator('app.handler', exclude: [App\Handler\Three::class])]) + ; + }; + +In the case the referencing service is itself tagged with the tag being used in the tagged +iterator, it is automatically excluded from the injected iterable. This behavior can be +disabled by setting the ``exclude_self`` option to ``false``: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/HandlerCollection.php + namespace App; + + use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; + + class HandlerCollection + { + public function __construct( + #[AutowireIterator('app.handler', exclude: ['App\Handler\Three'], excludeSelf: false)] + iterable $handlers + ) { + } + } - The ``#[TaggedIterator]`` attribute was introduced in Symfony 5.3 and requires PHP 8. + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + # This is the service we want to exclude, even if the 'app.handler' tag is attached + App\Handler\Three: + tags: ['app.handler'] + + App\HandlerCollection: + arguments: + - !tagged_iterator { tag: app.handler, exclude: ['App\Handler\Three'], exclude_self: false } + + .. code-block:: xml + + + + + + + + + + + + + + + + + App\Handler\Three + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $containerConfigurator) { + $services = $containerConfigurator->services(); + + // ... + + // This is the service we want to exclude, even if the 'app.handler' tag is attached + $services->set(App\Handler\Three::class) + ->tag('app.handler') + ; + + $services->set(App\HandlerCollection::class) + // inject all services tagged with app.handler as first argument + ->args([tagged_iterator('app.handler', exclude: [App\Handler\Three::class], excludeSelf: false)]) + ; + }; .. seealso:: @@ -829,7 +965,7 @@ the number, the earlier the tagged service will be located in the collection: use App\Handler\One; - return function(ContainerConfigurator $container) { + return function(ContainerConfigurator $container): void { $services = $container->services(); $services->set(One::class) @@ -863,12 +999,12 @@ you can define it in the configuration of the collecting service: // src/HandlerCollection.php namespace App; - use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; + use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; class HandlerCollection { public function __construct( - #[TaggedIterator('app.handler', defaultPriorityMethod: 'getPriority')] + #[AutowireIterator('app.handler', defaultPriorityMethod: 'getPriority')] iterable $handlers ) { } @@ -905,7 +1041,7 @@ you can define it in the configuration of the collecting service: use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; - return function (ContainerConfigurator $container) { + return function (ContainerConfigurator $container): void { $services = $container->services(); // ... @@ -937,12 +1073,12 @@ to index the services: // src/HandlerCollection.php namespace App; - use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; + use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; class HandlerCollection { public function __construct( - #[TaggedIterator('app.handler', indexAttribute: 'key')] + #[AutowireIterator('app.handler', indexAttribute: 'key')] iterable $handlers ) { } @@ -996,7 +1132,7 @@ to index the services: use App\Handler\Two; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; - return function (ContainerConfigurator $container) { + return function (ContainerConfigurator $container): void { $services = $container->services(); $services->set(One::class) @@ -1051,12 +1187,12 @@ get the value used to index the services: // src/HandlerCollection.php namespace App; - use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; + use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; class HandlerCollection { public function __construct( - #[TaggedIterator('app.handler', defaultIndexMethod: 'getIndex')] + #[AutowireIterator('app.handler', defaultIndexMethod: 'getIndex')] iterable $handlers ) { } @@ -1145,8 +1281,4 @@ be used directly on the class of the service you want to configure:: // ... } -.. versionadded:: 5.3 - - The ``#[AsTaggedItem]`` attribute was introduced in Symfony 5.3. - .. _`PHP constructor promotion`: https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion diff --git a/session.rst b/session.rst index a212acf9993..29d1ba364d1 100644 --- a/session.rst +++ b/session.rst @@ -41,18 +41,15 @@ if you type-hint an argument with :class:`Symfony\\Component\\HttpFoundation\\Re class SomeService { - private $requestStack; - - public function __construct(RequestStack $requestStack) - { - $this->requestStack = $requestStack; - + public function __construct( + private RequestStack $requestStack, + ) { // Accessing the session in the constructor is *NOT* recommended, since // it might not be accessible yet or lead to unwanted side-effects // $this->session = $requestStack->getSession(); } - public function someMethod() + public function someMethod(): void { $session = $this->requestStack->getSession(); @@ -300,7 +297,7 @@ configuration ` in use Symfony\Component\HttpFoundation\Cookie; use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->session() // Enables session support. Note that the session will ONLY be started if you read or write from it. // Remove or comment this section to explicitly disable session support. @@ -331,7 +328,7 @@ configuration ` in Setting the ``handler_id`` config option to ``null`` means that Symfony will use the native PHP session mechanism. The session metadata files will be stored outside of the Symfony application, in a directory controlled by PHP. Although -this usually simplify things, some session expiration related options may not +this usually simplifies things, some session expiration related options may not work as expected if other applications that write to the same directory have short max lifetime settings. @@ -374,7 +371,7 @@ session metadata files: // config/packages/framework.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->session() // ... ->handlerId('session.handler.native_file') @@ -407,11 +404,6 @@ The session cookie is also available in :ref:`the Response object - + %env(REDIS_HOST)% @@ -602,7 +594,7 @@ a Symfony service for the connection to the Redis server: use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler; $container - // you can also use \RedisArray, \RedisCluster or \Predis\Client classes + // you can also use \RedisArray, \RedisCluster, \Relay\Relay or \Predis\Client classes ->register('Redis', \Redis::class) ->addMethodCall('connect', ['%env(REDIS_HOST)%', '%env(int:REDIS_PORT)%']) // uncomment the following if your Redis server requires a password: @@ -657,7 +649,7 @@ configuration option to tell Symfony to use this service as the session handler: use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler; use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { // ... $framework->session() ->handlerId(RedisSessionHandler::class) @@ -682,11 +674,6 @@ and only the first one stored the CSRF token in the session. :ref:`handler_id ` config option, you can add the ``prefix`` and ``ttl`` options as query string parameters in the DSN. - .. versionadded:: 5.4 - - The support for ``prefix`` and ``ttl`` options in a Redis DSN was - introduced in Symfony 5.4. - .. _session-database-pdo: Store Sessions in a Relational Database (MariaDB, MySQL, PostgreSQL) @@ -745,7 +732,7 @@ To use it, first register a new handler service with your database credentials: use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $services = $container->services(); $services->set(PdoSessionHandler::class) @@ -763,11 +750,6 @@ To use it, first register a new handler service with your database credentials: When using MySQL as the database, the DSN defined in ``DATABASE_URL`` can contain the ``charset`` and ``unix_socket`` options as query string parameters. - .. versionadded:: 5.3 - - The support for ``charset`` and ``unix_socket`` options was introduced - in Symfony 5.3. - Next, use the :ref:`handler_id ` configuration option to tell Symfony to use this service as the session handler: @@ -806,7 +788,7 @@ configuration option to tell Symfony to use this service as the session handler: use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { // ... $framework->session() ->handlerId(PdoSessionHandler::class) @@ -860,7 +842,7 @@ passed to the ``PdoSessionHandler`` service: use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $services = $container->services(); $services->set(PdoSessionHandler::class) @@ -910,7 +892,14 @@ Preparing the Database to Store Sessions ........................................ Before storing sessions in the database, you must create the table that stores -the information. The session handler provides a method called +the information. + +With Doctrine installed, the session table will be automatically generated when +you run the ``make:migration`` command if the database targeted by doctrine is +identical to the one used by this component. + +Or if you prefer to create the table yourself and the table has not already been +created, the session handler provides a method called :method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\PdoSessionHandler::createTable` to set up this table for you according to the database engine used:: @@ -920,7 +909,9 @@ to set up this table for you according to the database engine used:: // the table could not be created for some reason } -If you prefer to set up the table yourself, it's recommended to generate an +If the table already exists an exception will be thrown. + +If you would rather set up the table yourself, it's recommended to generate an empty database migration with the following command: .. code-block:: terminal @@ -934,6 +925,10 @@ file and run the migration with the following command: $ php bin/console doctrine:migrations:migrate +If needed, you can also add this table to your schema by calling +:method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\PdoSessionHandler::configureSchema` +method in your code. + .. _mysql: MariaDB/MySQL @@ -1044,7 +1039,7 @@ the MongoDB connection as argument, and the required parameters: use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $services = $container->services(); $services->set(MongoDbSessionHandler::class) @@ -1093,7 +1088,7 @@ configuration option to tell Symfony to use this service as the session handler: use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler; use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { // ... $framework->session() ->handlerId(MongoDbSessionHandler::class) @@ -1163,7 +1158,7 @@ configure these values with the second argument passed to the use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $services = $container->services(); $services->set(MongoDbSessionHandler::class) @@ -1216,6 +1211,123 @@ This is the recommended migration workflow: #. After verifying that the sessions in your application are working, switch from the migrating handler to the new handler. +.. _session-configure-ttl: + +Configuring the Session TTL +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony by default will use PHP's ini setting ``session.gc_maxlifetime`` as +session lifetime. When you store sessions in a database, you can also +configure your own TTL in the framework configuration or even at runtime. + +.. note:: + + Changing the ini setting is not possible once the session is started so + if you want to use a different TTL depending on which user is logged + in, you must do it at runtime using the callback method below. + +Configure the TTL +................. + +You need to pass the TTL in the options array of the session handler you are using: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler: + arguments: + - '@Redis' + - { 'ttl': 600 } + + .. code-block:: xml + + + + + + + 600 + + + + + .. code-block:: php + + // config/services.php + use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler; + + $services + ->set(RedisSessionHandler::class) + ->args([ + service('Redis'), + ['ttl' => 600], + ]); + +Configure the TTL Dynamically at Runtime +........................................ + +If you would like to have a different TTL for different users or sessions +for whatever reason, this is also possible by passing a callback as the TTL +value. The callback will be called right before the session is written and +has to return an integer which will be used as TTL. + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler: + arguments: + - '@Redis' + - { 'ttl': !closure '@my.ttl.handler' } + + my.ttl.handler: + class: Some\InvokableClass # some class with an __invoke() method + arguments: + # Inject whatever dependencies you need to be able to resolve a TTL for the current session + - '@security' + + .. code-block:: xml + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler; + + $services + ->set(RedisSessionHandler::class) + ->args([ + service('Redis'), + ['ttl' => closure(service('my.ttl.handler'))], + ]); + + $services + // some class with an __invoke() method + ->set('my.ttl.handler', 'Some\InvokableClass') + // Inject whatever dependencies you need to be able to resolve a TTL for the current session + ->args([service('security')]); + .. _locale-sticky-session: Making the Locale "Sticky" during a User's Session @@ -1241,14 +1353,12 @@ can determine the correct locale however you want:: class LocaleSubscriber implements EventSubscriberInterface { - private $defaultLocale; - - public function __construct(string $defaultLocale = 'en') - { - $this->defaultLocale = $defaultLocale; + public function __construct( + private string $defaultLocale = 'en', + ) { } - public function onKernelRequest(RequestEvent $event) + public function onKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); if (!$request->hasPreviousSession()) { @@ -1264,7 +1374,7 @@ can determine the correct locale however you want:: } } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ // must be registered before (i.e. with a higher priority than) the default Locale listener @@ -1339,7 +1449,7 @@ Remember, to get the user's locale, always use the :method:`Request::getLocale // from a controller... use Symfony\Component\HttpFoundation\Request; - public function index(Request $request) + public function index(Request $request): void { $locale = $request->getLocale(); } @@ -1358,7 +1468,7 @@ this as the locale for the given user. To accomplish this, you can hook into the login process and update the user's session with this locale value before they are redirected to their first page. -To do this, you need an event subscriber on the ``security.interactive_login`` +To do this, you need an event subscriber on the ``LoginSuccessEvent::class`` event:: // src/EventSubscriber/UserLocaleSubscriber.php @@ -1366,8 +1476,7 @@ event:: use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\RequestStack; - use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; - use Symfony\Component\Security\Http\SecurityEvents; + use Symfony\Component\Security\Http\Event\LoginSuccessEvent; /** * Stores the locale of the user in the session after the @@ -1375,26 +1484,24 @@ event:: */ class UserLocaleSubscriber implements EventSubscriberInterface { - private $requestStack; - - public function __construct(RequestStack $requestStack) - { - $this->requestStack = $requestStack; + public function __construct( + private RequestStack $requestStack, + ) { } - public function onInteractiveLogin(InteractiveLoginEvent $event) + public function onLoginSuccess(LoginSuccessEvent $event): void { - $user = $event->getAuthenticationToken()->getUser(); + $user = $event->getUser(); if (null !== $user->getLocale()) { $this->requestStack->getSession()->set('_locale', $user->getLocale()); } } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ - SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin', + LoginSuccessEvent::class => 'onLoginSuccess', ]; } } @@ -1453,7 +1560,7 @@ Symfony to use your session handler instead of the default one: use App\Session\CustomSessionHandler; use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { // ... $framework->session() ->handlerId(CustomSessionHandler::class) @@ -1480,23 +1587,21 @@ library, but you can adapt it to any other library that you may be using:: class EncryptedSessionProxy extends SessionHandlerProxy { - private $key; - - public function __construct(\SessionHandlerInterface $handler, Key $key) - { - $this->key = $key; - + public function __construct( + private \SessionHandlerInterface $handler, + private Key $key + ) { parent::__construct($handler); } - public function read($id) + public function read($id): string { $data = parent::read($id); return Crypto::decrypt($data, $this->key); } - public function write($id, $data) + public function write($id, $data): string { $data = Crypto::encrypt($data, $this->key); @@ -1577,12 +1682,6 @@ Then, register the ``SodiumMarshaller`` service using this key: This will encrypt the values of the cache items, but not the cache keys. Be careful not to leak sensitive data in the keys. -.. versionadded:: 5.1 - - The :class:`Symfony\\Component\\Cache\\Marshaller\\SodiumMarshaller` - and :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MarshallingSessionHandler` - classes were introduced in Symfony 5.1. - Read-only Guest Sessions ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1594,21 +1693,19 @@ intercept the session before it is written:: namespace App\Session; use App\Entity\User; + use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; - use Symfony\Component\Security\Core\Security; class ReadOnlySessionProxy extends SessionHandlerProxy { - private $security; - - public function __construct(\SessionHandlerInterface $handler, Security $security) - { - $this->security = $security; - + public function __construct( + private \SessionHandlerInterface $handler, + private Security $security + ) { parent::__construct($handler); } - public function write($id, $data) + public function write($id, $data): string { if ($this->getUser() && $this->getUser()->isGuest()) { return; @@ -1617,12 +1714,14 @@ intercept the session before it is written:: return parent::write($id, $data); } - private function getUser() + private function getUser(): ?User { $user = $this->security->getUser(); if (is_object($user)) { return $user; } + + return null; } } @@ -1670,7 +1769,7 @@ for the ``handler_id``: // config/packages/framework.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->session() ->storageFactoryId('session.storage.factory.php_bridge') ->handlerId(null) @@ -1730,7 +1829,7 @@ the example below: // config/packages/framework.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->session() ->storageFactoryId('session.storage.factory.php_bridge') ->handlerId('session.storage.native_file') diff --git a/setup.rst b/setup.rst index 2404c5c3738..c8654296986 100644 --- a/setup.rst +++ b/setup.rst @@ -4,7 +4,7 @@ Installing & Setting up the Symfony Framework .. admonition:: Screencast :class: screencast - Do you prefer video tutorials? Check out the `Stellar Development with Symfony`_ + Do you prefer video tutorials? Check out the `Harmonious Development with Symfony`_ screencast series. .. _symfony-tech-requirements: @@ -14,10 +14,9 @@ Technical Requirements Before creating your first Symfony application you must: -* Install PHP 7.2.5 or higher and these PHP extensions (which are installed and - enabled by default in most PHP 7 installations): `Ctype`_, `iconv`_, `JSON`_, +* Install PHP 8.2 or higher and these PHP extensions (which are installed and + enabled by default in most PHP 8 installations): `Ctype`_, `iconv`_, `PCRE`_, `Session`_, `SimpleXML`_, and `Tokenizer`_; - * `Install Composer`_, which is used to install PHP packages. .. _setup-symfony-cli: @@ -49,10 +48,10 @@ application: .. code-block:: terminal # run this if you are building a traditional web application - $ symfony new my_project_directory --version=5.4 --webapp + $ symfony new my_project_directory --version="7.1.*" --webapp # run this if you are building a microservice, console application or API - $ symfony new my_project_directory --version=5.4 + $ symfony new my_project_directory --version="7.1.*" The only difference between these two commands is the number of packages installed by default. The ``--webapp`` option installs extra packages to give @@ -64,12 +63,12 @@ Symfony application using Composer: .. code-block:: terminal # run this if you are building a traditional web application - $ composer create-project symfony/skeleton:"^5.4" my_project_directory + $ composer create-project symfony/skeleton:"7.1.*" my_project_directory $ cd my_project_directory $ composer require webapp # run this if you are building a microservice, console application or API - $ composer create-project symfony/skeleton:"^5.4" my_project_directory + $ composer create-project symfony/skeleton:"7.1.*" my_project_directory No matter which command you run to create the Symfony application. All of them will create a new ``my_project_directory/`` directory, download some dependencies @@ -150,6 +149,7 @@ Symfony Docker Integration If you'd like to use Docker with Symfony, see :doc:`/setup/docker`. .. _symfony-flex: +.. _flex-quick-intro: Installing Packages ------------------- @@ -221,8 +221,7 @@ which in turn installs several packages like ``symfony/debug-bundle``, You won't see the ``symfony/debug-pack`` dependency in your ``composer.json``, as Flex automatically unpacks the pack. This means that it only adds the real packages as dependencies (e.g. you will see a new ``symfony/var-dumper`` in -``require-dev``). While it is not recommended, you can use the ``composer -require --no-unpack ...`` option to disable unpacking. +``require-dev``). .. _security-checker: @@ -273,14 +272,14 @@ stable version. If you want to use an LTS version, add the ``--version`` option: $ symfony new my_project_directory --version=next # you can also select an exact specific Symfony version - $ symfony new my_project_directory --version=5.4 + $ symfony new my_project_directory --version="6.4.*" The ``lts`` and ``next`` shortcuts are only available when using Symfony to create new projects. If you use Composer, you need to tell the exact version: .. code-block:: terminal - $ composer create-project symfony/skeleton:"^5.4" my_project_directory + $ composer create-project symfony/skeleton:"6.4.*" my_project_directory The Symfony Demo application ---------------------------- @@ -312,7 +311,7 @@ Learn More setup/web_server_configuration setup/* -.. _`Stellar Development with Symfony`: https://symfonycasts.com/screencast/symfony +.. _`Harmonious Development with Symfony`: https://symfonycasts.com/screencast/symfony .. _`Install Composer`: https://getcomposer.org/download/ .. _`install the Symfony CLI`: https://symfony.com/download .. _`symfony-cli/symfony-cli GitHub repository`: https://github.com/symfony-cli/symfony-cli @@ -325,7 +324,6 @@ Learn More .. _`Contrib recipe repository`: https://github.com/symfony/recipes-contrib .. _`Symfony Recipes documentation`: https://github.com/symfony/recipes/blob/master/README.rst .. _`iconv`: https://www.php.net/book.iconv -.. _`JSON`: https://www.php.net/book.json .. _`Session`: https://www.php.net/book.session .. _`Ctype`: https://www.php.net/book.ctype .. _`Tokenizer`: https://www.php.net/book.tokenizer diff --git a/setup/bundles.rst b/setup/bundles.rst index bd3346b7ea1..61d0308be66 100644 --- a/setup/bundles.rst +++ b/setup/bundles.rst @@ -78,7 +78,7 @@ PHPUnit test report: Twig Function "form_enctype" is deprecated. Use "form_start" instead in ... - The Symfony\Component\Security\Core\SecurityContext class is deprecated since + The Symfony\Bundle\SecurityBundle\SecurityContext class is deprecated since version 2.6 and will be removed in 3.0. Use ... Fix the reported deprecations, run the test suite again and repeat the process diff --git a/setup/unstable_versions.rst b/setup/unstable_versions.rst index f8010440855..8fabced2de6 100644 --- a/setup/unstable_versions.rst +++ b/setup/unstable_versions.rst @@ -7,7 +7,7 @@ they are released as stable versions. Creating a New Project Based on an Unstable Symfony Version ----------------------------------------------------------- -Suppose that the Symfony 5.4 version hasn't been released yet and you want to create +Suppose that the Symfony 6.0 version hasn't been released yet and you want to create a new project to test its features. First, `install the Composer package manager`_. Then, open a command console, enter your project's directory and run the following command: @@ -23,7 +23,7 @@ in the ``my_project/`` directory. Upgrading your Project to an Unstable Symfony Version ----------------------------------------------------- -Suppose again that Symfony 5.4 hasn't been released yet and you want to upgrade +Suppose again that Symfony 6.0 hasn't been released yet and you want to upgrade an existing application to test that your project works with it. First, open the ``composer.json`` file located in the root directory of your @@ -34,8 +34,8 @@ new version and change your ``minimum-stability`` to ``beta``: { "require": { - + "symfony/framework-bundle": "^5.4", - + "symfony/finder": "^5.4", + + "symfony/framework-bundle": "^6.0", + + "symfony/finder": "^6.0", "...": "..." }, + "minimum-stability": "beta" @@ -43,7 +43,7 @@ new version and change your ``minimum-stability`` to ``beta``: You can also use set ``minimum-stability`` to ``dev``, or omit this line entirely, and opt into your stability on each package by using constraints -like ``5.4.*@beta``. +like ``6.0.*@beta``. Finally, from a terminal, update your project's dependencies: diff --git a/setup/upgrade_major.rst b/setup/upgrade_major.rst index 7aa0d90c3b7..60983e299a6 100644 --- a/setup/upgrade_major.rst +++ b/setup/upgrade_major.rst @@ -1,4 +1,4 @@ -Upgrading a Major Version (e.g. 5.4.0 to 6.0.0) +Upgrading a Major Version (e.g. 6.4.0 to 7.0.0) =============================================== Every two years, Symfony releases a new major version release (the first number @@ -27,10 +27,10 @@ backwards incompatible changes. To accomplish this, the "old" (e.g. functions, classes, etc) code still works, but is marked as *deprecated*, indicating that it will be removed/changed in the future and that you should stop using it. -When the major version is released (e.g. 6.0.0), all deprecated features and +When the major version is released (e.g. 7.0.0), all deprecated features and functionality are removed. So, as long as you've updated your code to stop using these deprecated features in the last version before the major (e.g. -``5.4.*``), you should be able to upgrade without a problem. That means that +``6.4.*``), you should be able to upgrade without a problem. That means that you should first :doc:`upgrade to the last minor version ` (e.g. 5.4) so that you can see *all* the deprecations. @@ -107,7 +107,7 @@ done! .. sidebar:: Using the Weak Deprecations Mode Sometimes, you can't fix all deprecations (e.g. something was deprecated - in 5.4 and you still need to support 5.3). In these cases, you can still + in 6.4 and you still need to support 6.3). In these cases, you can still use the bridge to fix as many deprecations as possible and then allow more of them to make your tests pass again. You can do this by using the ``SYMFONY_DEPRECATIONS_HELPER`` env variable: @@ -144,12 +144,10 @@ starting with ``symfony/`` to the new major version: "...": "...", "require": { - - "symfony/cache": "5.4.*", - + "symfony/cache": "6.0.*", - - "symfony/config": "5.4.*", - + "symfony/config": "6.0.*", - - "symfony/console": "5.4.*", - + "symfony/console": "6.0.*", + - "symfony/config": "6.4.*", + + "symfony/config": "7.0.*", + - "symfony/console": "6.4.*", + + "symfony/console": "7.0.*", "...": "...", "...": "A few libraries starting with symfony/ follow their own @@ -157,22 +155,22 @@ starting with ``symfony/`` to the new major version: symfony/ux-[...], symfony/[...]-bundle). You do not need to update these versions: you can upgrade them independently whenever you want", - "symfony/monolog-bundle": "^3.5", + "symfony/monolog-bundle": "^3.10", }, "...": "...", } At the bottom of your ``composer.json`` file, in the ``extra`` block you can find a data setting for the Symfony version. Make sure to also upgrade -this one. For instance, update it to ``6.0.*`` to upgrade to Symfony 6.0: +this one. For instance, update it to ``7.0.*`` to upgrade to Symfony 7.0: .. code-block:: diff "extra": { "symfony": { "allow-contrib": false, - - "require": "5.4.*" - + "require": "6.0.*" + - "require": "6.4.*" + + "require": "7.0.*" } } @@ -221,17 +219,13 @@ included in the Symfony repository for any BC break that you need to be aware of Upgrading to Symfony 6: Add Native Return Types ----------------------------------------------- -.. versionadded:: 5.4 - - The return-type checking and fixing features were introduced in Symfony 5.4. - -Symfony 6 will come with native PHP return types to (almost all) methods. +Symfony 6 and Symfony 7 added native PHP return types to (almost all) methods. In PHP, if the parent has a return type declaration, any class implementing or overriding the method must have the return type as well. However, you can add a return type before the parent adds one. This means that it is important to add the native PHP return types to your classes before -upgrading to Symfony 6.0. Otherwise, you will get incompatible declaration +upgrading to Symfony 6.0 or 7.0. Otherwise, you will get incompatible declaration errors. When debug mode is enabled (typically in the dev and test environment), diff --git a/setup/upgrade_minor.rst b/setup/upgrade_minor.rst index 9e8c6943d1f..ec00e142b82 100644 --- a/setup/upgrade_minor.rst +++ b/setup/upgrade_minor.rst @@ -1,4 +1,4 @@ -Upgrading a Minor Version (e.g. 5.0.0 to 5.1.0) +Upgrading a Minor Version (e.g. 6.3.0 to 6.4.0) =============================================== If you're upgrading a minor version (where the middle number changes), then @@ -21,7 +21,7 @@ There are two steps to upgrading a minor version: The ``composer.json`` file is configured to allow Symfony packages to be upgraded to patch versions. But to upgrade to a new minor version, you will probably need to update the version constraint next to each library starting -``symfony/``. Suppose you are upgrading from Symfony 5.3 to 5.4: +``symfony/``. Suppose you are upgrading from Symfony 6.3 to 6.4: .. code-block:: diff @@ -29,19 +29,17 @@ probably need to update the version constraint next to each library starting "...": "...", "require": { - - "symfony/cache": "5.3.*", - + "symfony/cache": "5.4.*", - - "symfony/config": "5.3.*", - + "symfony/config": "5.4.*", - - "symfony/console": "5.3.*", - + "symfony/console": "5.4.*", + - "symfony/config": "6.3.*", + + "symfony/config": "6.4.*", + - "symfony/console": "6.3.*", + + "symfony/console": "6.4.*", "...": "...", "...": "A few libraries starting with symfony/ follow their own versioning scheme. You do not need to update these versions: you can upgrade them independently whenever you want", - "symfony/monolog-bundle": "^3.5", + "symfony/monolog-bundle": "^3.10", }, "...": "...", } @@ -54,8 +52,8 @@ Your ``composer.json`` file should also have an ``extra`` block that you will "extra": { "symfony": { "...": "...", - - "require": "5.3.*" - + "require": "5.4.*" + - "require": "6.3.*" + + "require": "6.4.*" } } @@ -79,7 +77,7 @@ to your code to get everything working. Additionally, some features you're using might still work, but might now be deprecated. While that's fine, if you know about these deprecations, you can start to fix them over time. -Every version of Symfony comes with an UPGRADE file (e.g. `UPGRADE-5.4.md`_) +Every version of Symfony comes with an UPGRADE file (e.g. `UPGRADE-6.4.md`_) included in the Symfony directory that describes these changes. If you follow the instructions in the document and update your code accordingly, it should be safe to update in the future. @@ -97,5 +95,5 @@ These documents can also be found in the `Symfony Repository`_. .. include:: /setup/_update_recipes.rst.inc .. _`Symfony Repository`: https://github.com/symfony/symfony -.. _`UPGRADE-5.4.md`: https://github.com/symfony/symfony/blob/5.4/UPGRADE-5.4.md +.. _`UPGRADE-6.4.md`: https://github.com/symfony/symfony/blob/6.4/UPGRADE-6.4.md .. _`Rector`: https://github.com/rectorphp/rector diff --git a/setup/upgrade_patch.rst b/setup/upgrade_patch.rst index d867f371dee..4475ff58cf3 100644 --- a/setup/upgrade_patch.rst +++ b/setup/upgrade_patch.rst @@ -1,4 +1,4 @@ -Upgrading a Patch Version (e.g. 5.0.0 to 5.0.1) +Upgrading a Patch Version (e.g. 6.0.0 to 6.0.1) =============================================== When a new patch version is released (only the last number changed), it is a diff --git a/components/string.rst b/string.rst similarity index 86% rename from components/string.rst rename to string.rst index 48b9a592aac..667dcd06010 100644 --- a/components/string.rst +++ b/string.rst @@ -1,15 +1,11 @@ -The String Component -==================== +Creating and Manipulating Strings +================================= - The String component provides a single object-oriented API to work with - three "unit systems" of strings: bytes, code points and grapheme clusters. +Symfony provides an object-oriented API to work with Unicode strings (as bytes, +code points and grapheme clusters). This API is available via the String component, +which you must first install in your application: -.. versionadded:: 5.0 - - The String component was introduced in Symfony 5.0. - -Installation ------------- +.. _installation: .. code-block:: terminal @@ -125,10 +121,6 @@ to make your code more concise:: // creates a UnicodeString object $foo = s('अनुच्छेद'); -.. versionadded:: 5.1 - - The ``s()`` function was introduced in Symfony 5.1. - There are also some specialized constructors:: // ByteString can create a random string of the given length @@ -142,10 +134,6 @@ There are also some specialized constructors:: $foo = UnicodeString::fromCodePoints(0x928, 0x92E, 0x938, 0x94D, 0x924, 0x947); // equivalent to: $foo = new UnicodeString('नमस्ते'); -.. versionadded:: 5.1 - - The second argument of ``ByteString::fromRandom()`` was introduced in Symfony 5.1. - Methods to Transform String Objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -215,7 +203,10 @@ Methods to Change Case :: // changes all graphemes/code points to lower case - u('FOO Bar')->lower(); // 'foo bar' + u('FOO Bar Brİan')->lower(); // 'foo bar bri̇an' + // changes all graphemes/code points to lower case according to locale-specific case mappings + u('FOO Bar Brİan')->localeLower('en'); // 'foo bar bri̇an' + u('FOO Bar Brİan')->localeLower('lt'); // 'foo bar bri̇̇an' // when dealing with different languages, uppercase/lowercase is not enough // there are three cases (lower, upper, title), some characters have no case, @@ -225,11 +216,17 @@ Methods to Change Case u('Die O\'Brian Straße')->folded(); // "die o'brian strasse" // changes all graphemes/code points to upper case - u('foo BAR')->upper(); // 'FOO BAR' + u('foo BAR bάz')->upper(); // 'FOO BAR BΆZ' + // changes all graphemes/code points to upper case according to locale-specific case mappings + u('foo BAR bάz')->localeUpper('en'); // 'FOO BAR BΆZ' + u('foo BAR bάz')->localeUpper('el'); // 'FOO BAR BAZ' // changes all graphemes/code points to "title case" - u('foo bar')->title(); // 'Foo bar' - u('foo bar')->title(true); // 'Foo Bar' + u('foo ijssel')->title(); // 'Foo ijssel' + u('foo ijssel')->title(allWords: true); // 'Foo Ijssel' + // changes all graphemes/code points to "title case" according to locale-specific case mappings + u('foo ijssel')->localeTitle('en'); // 'Foo ijssel' + u('foo ijssel')->localeTitle('nl'); // 'Foo IJssel' // changes all graphemes/code points to camelCase u('Foo: Bar-baz.')->camel(); // 'fooBarBaz' @@ -238,6 +235,11 @@ Methods to Change Case // other cases can be achieved by chaining methods. E.g. PascalCase: u('Foo: Bar-baz.')->camel()->title(); // 'FooBarBaz' +.. versionadded:: 7.1 + + The ``localeLower()``, ``localeUpper()`` and ``localeTitle()`` methods were + introduced in Symfony 7.1. + The methods of all string classes are case-sensitive by default. You can perform case-insensitive operations with the ``ignoreCase()`` method:: @@ -267,20 +269,20 @@ Methods to Append and Prepend u('UserControllerController')->ensureEnd('Controller'); // 'UserController' // returns the contents found before/after the first occurrence of the given string - u('hello world')->before('world'); // 'hello ' - u('hello world')->before('o'); // 'hell' - u('hello world')->before('o', true); // 'hello' + u('hello world')->before('world'); // 'hello ' + u('hello world')->before('o'); // 'hell' + u('hello world')->before('o', includeNeedle: true); // 'hello' - u('hello world')->after('hello'); // ' world' - u('hello world')->after('o'); // ' world' - u('hello world')->after('o', true); // 'o world' + u('hello world')->after('hello'); // ' world' + u('hello world')->after('o'); // ' world' + u('hello world')->after('o', includeNeedle: true); // 'o world' // returns the contents found before/after the last occurrence of the given string - u('hello world')->beforeLast('o'); // 'hello w' - u('hello world')->beforeLast('o', true); // 'hello wo' + u('hello world')->beforeLast('o'); // 'hello w' + u('hello world')->beforeLast('o', includeNeedle: true); // 'hello wo' - u('hello world')->afterLast('o'); // 'rld' - u('hello world')->afterLast('o', true); // 'orld' + u('hello world')->afterLast('o'); // 'rld' + u('hello world')->afterLast('o', includeNeedle: true); // 'orld' Methods to Pad and Trim ~~~~~~~~~~~~~~~~~~~~~~~ @@ -315,10 +317,6 @@ Methods to Pad and Trim u('file-image-0001.png')->trimPrefix(['file-', 'image-']); // 'image-0001.png' u('template.html.twig')->trimSuffix(['.twig', '.html']); // 'template.html' -.. versionadded:: 5.4 - - The ``trimPrefix()`` and ``trimSuffix()`` methods were introduced in Symfony 5.4. - Methods to Search and Replace ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -368,14 +366,10 @@ Methods to Search and Replace // replaces all occurrences of the given regular expression u('(+1) 206-555-0100')->replaceMatches('/[^A-Za-z0-9]++/', ''); // '12065550100' // you can pass a callable as the second argument to perform advanced replacements - u('123')->replaceMatches('/\d/', function ($match) { + u('123')->replaceMatches('/\d/', function (string $match): string { return '['.$match[0].']'; }); // result = '[1][2][3]' -.. versionadded:: 5.1 - - The ``containsAny()`` method was introduced in Symfony 5.1. - Methods to Join, Split, Truncate and Reverse ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -401,21 +395,17 @@ Methods to Join, Split, Truncate and Reverse u('Lorem Ipsum')->truncate(80); // 'Lorem Ipsum' // the second argument is the character(s) added when a string is cut // (the total length includes the length of this character(s)) - u('Lorem Ipsum')->truncate(8, '…'); // 'Lorem I…' + u('Lorem Ipsum')->truncate(8, '…'); // 'Lorem I…' // if the third argument is false, the last word before the cut is kept // even if that generates a string longer than the desired length - u('Lorem Ipsum')->truncate(8, '…', false); // 'Lorem Ipsum' - -.. versionadded:: 5.1 - - The third argument of ``truncate()`` was introduced in Symfony 5.1. + u('Lorem Ipsum')->truncate(8, '…', cut: false); // 'Lorem Ipsum' :: // breaks the string into lines of the given length - u('Lorem Ipsum')->wordwrap(4); // 'Lorem\nIpsum' + u('Lorem Ipsum')->wordwrap(4); // 'Lorem\nIpsum' // by default it breaks by white space; pass TRUE to break unconditionally - u('Lorem Ipsum')->wordwrap(4, "\n", true); // 'Lore\nm\nIpsu\nm' + u('Lorem Ipsum')->wordwrap(4, "\n", cut: true); // 'Lore\nm\nIpsu\nm' // replaces a portion of the string with the given contents: // the second argument is the position where the replacement starts; @@ -429,13 +419,9 @@ Methods to Join, Split, Truncate and Reverse u('0123456789')->chunk(3); // ['012', '345', '678', '9'] // reverses the order of the string contents - u('foo bar')->reverse(); // 'rab oof' + u('foo bar')->reverse(); // 'rab oof' u('さよなら')->reverse(); // 'らなよさ' -.. versionadded:: 5.1 - - The ``reverse()`` method was introduced in Symfony 5.1. - Methods Added by ByteString ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -489,7 +475,7 @@ class that allows to store a string whose value is only generated when you need use Symfony\Component\String\LazyString; - $lazyString = LazyString::fromCallable(function () { + $lazyString = LazyString::fromCallable(function (): string { // Compute the string value... $value = ...; @@ -521,10 +507,12 @@ requested during the program execution. You can also create lazy strings from a // hash computation only if it's needed $lazyHash = LazyString::fromStringable(new Hash()); -.. versionadded:: 5.1 +Working with Emojis +------------------- - The :class:`Symfony\\Component\\String\\LazyString` class was introduced - in Symfony 5.1. +These contents have been moved to the :doc:`Emoji component docs `. + +.. _string-slugger: Slugger ------- @@ -551,22 +539,10 @@ that only includes safe ASCII characters:: // $slug = '10-percent-or-5-euro' // for more dynamic substitutions, pass a PHP closure instead of an array - $slugger = new AsciiSlugger('en', function ($string, $locale) { + $slugger = new AsciiSlugger('en', function (string $string, string $locale): string { return str_replace('❤️', 'love', $string); }); -.. versionadded:: 5.1 - - The feature to define additional substitutions was introduced in Symfony 5.1. - -.. versionadded:: 5.2 - - The feature to use a PHP closure to define substitutions was introduced in Symfony 5.2. - -.. versionadded:: 5.3 - - The feature to fallback to the parent locale's symbols map was introduced in Symfony 5.3. - The separator between words is a dash (``-``) by default, but you can define another separator as the second argument:: @@ -594,28 +570,52 @@ the injected slugger is the same as the request locale:: class MyService { - private $slugger; - - public function __construct(SluggerInterface $slugger) - { - $this->slugger = $slugger; + public function __construct( + private SluggerInterface $slugger, + ) { } - public function someMethod() + public function someMethod(): void { $slug = $this->slugger->slug('...'); } } +.. _string-slugger-emoji: + +Slug Emojis +~~~~~~~~~~~ + +You can also combine the :ref:`emoji transliterator ` +with the slugger to transform any emojis into their textual representation:: + + use Symfony\Component\String\Slugger\AsciiSlugger; + + $slugger = new AsciiSlugger(); + $slugger = $slugger->withEmoji(); + + $slug = $slugger->slug('a 😺, 🐈‍⬛, and a 🦁 go to 🏞️', '-', 'en'); + // $slug = 'a-grinning-cat-black-cat-and-a-lion-go-to-national-park'; + + $slug = $slugger->slug('un 😺, 🐈‍⬛, et un 🦁 vont au 🏞️', '-', 'fr'); + // $slug = 'un-chat-qui-sourit-chat-noir-et-un-tete-de-lion-vont-au-parc-national'; + +If you want to use a specific locale for the emoji, or to use the short codes +from GitHub, Gitlab or Slack, use the first argument of ``withEmoji()`` method:: + + use Symfony\Component\String\Slugger\AsciiSlugger; + + $slugger = new AsciiSlugger(); + $slugger = $slugger->withEmoji('github'); // or "en", or "fr", etc. + + $slug = $slugger->slug('a 😺, 🐈‍⬛, and a 🦁'); + // $slug = 'a-smiley-cat-black-cat-and-a-lion'; + .. _string-inflector: Inflector --------- -.. versionadded:: 5.1 - - The inflector feature was introduced in Symfony 5.1. - In some scenarios such as code generation and code introspection, you need to convert words from/to singular/plural. For example, to know the property associated with an *adder* method, you must convert from plural diff --git a/templates.rst b/templates.rst index 9fefc066fe0..fb7549482e2 100644 --- a/templates.rst +++ b/templates.rst @@ -6,10 +6,6 @@ whether you need to render HTML from a :doc:`controller ` or genera the :doc:`contents of an email `. Templates in Symfony are created with Twig: a flexible, fast, and secure template engine. -.. caution:: - - Starting from Symfony 5.0, PHP templates are no longer supported. - .. _twig-language: Twig Templating Language @@ -189,34 +185,6 @@ Consider the following routing configuration: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/BlogController.php - namespace App\Controller; - - // ... - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; - - class BlogController extends AbstractController - { - /** - * @Route("/", name="blog_index") - */ - public function index(): Response - { - // ... - } - - /** - * @Route("/article/{slug}", name="blog_post") - */ - public function show(string $slug): Response - { - // ... - } - } - .. code-block:: php-attributes // src/Controller/BlogController.php @@ -224,7 +192,7 @@ Consider the following routing configuration: // ... use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; class BlogController extends AbstractController { @@ -276,7 +244,7 @@ Consider the following routing configuration: use App\Controller\BlogController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return function (RoutingConfigurator $routes): void { $routes->add('blog_index', '/') ->controller([BlogController::class, 'index']) ; @@ -361,8 +329,8 @@ as follows: Build, Versioning & More Advanced CSS, JavaScript and Image Handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -For help building, versioning and minifying your JavaScript and -CSS assets in a modern way, read about :doc:`Symfony's Webpack Encore `. +For help building and versioning your JavaScript and +CSS assets in a modern way, read about :doc:`Symfony's AssetMapper `. .. _twig-app-variable: @@ -406,6 +374,16 @@ gives you access to these variables: ``app.token`` A :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface` object representing the security token. +``app.current_route`` + The name of the route associated with the current request or ``null`` if no + request is available (equivalent to ``app.request.attributes.get('_route')``) +``app.current_route_parameters`` + An array with the parameters passed to the route of the current request or an + empty array if no request is available (equivalent to ``app.request.attributes.get('_route_params')``) +``app.locale`` + The locale used in the current :ref:`locale switcher ` context. +``app.enabled_locales`` + The locales enabled in the application. In addition to the global ``app`` variable injected by Symfony, you can also inject variables automatically to all Twig templates as explained in the next @@ -453,7 +431,7 @@ inside the main Twig configuration file: // config/packages/twig.php use Symfony\Config\TwigConfig; - return static function (TwigConfig $twig) { + return static function (TwigConfig $twig): void { // ... $twig->global('ga_tracking')->value('UA-xxxxx-x'); @@ -512,7 +490,7 @@ in container parameters `: use function Symfony\Component\DependencyInjection\Loader\Configurator\service; use Symfony\Config\TwigConfig; - return static function (TwigConfig $twig) { + return static function (TwigConfig $twig): void { // ... $twig->global('uuid')->value(service('App\Generator\UuidGenerator')); @@ -586,6 +564,74 @@ If your controller does not extend from ``AbstractController``, you'll need to :ref:`fetch services in your controller ` and use the ``render()`` method of the ``twig`` service. +.. _templates-template-attribute: + +Another option is to use the ``#[Template]`` attribute on the controller method +to define the template to render:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use Symfony\Bridge\Twig\Attribute\Template; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + + class ProductController extends AbstractController + { + #[Template('product/index.html.twig')] + public function index(): array + { + // ... + + // when using the #[Template] attribute, you only need to return + // an array with the parameters to pass to the template (the attribute + // is the one which will create and return the Response object). + return [ + 'category' => '...', + 'promotions' => ['...', '...'], + ]; + } + } + +The :ref:`base AbstractController ` also provides the +:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::renderBlock` +and :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::renderBlockView` +methods:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + + class ProductController extends AbstractController + { + // ... + + public function price(): Response + { + // ... + + // the `renderBlock()` method returns a `Response` object with the + // block contents + return $this->renderBlock('product/index.html.twig', 'price_block', [ + // ... + ]); + + // the `renderBlockView()` method only returns the contents created by the + // template block, so you can use those contents later in a `Response` object + $contents = $this->renderBlockView('product/index.html.twig', 'price_block', [ + // ... + ]); + + return new Response($contents); + } + } + +This might come handy when dealing with blocks in +:ref:`templates inheritance ` or when using +`Turbo Streams`_. + Rendering a Template in Services ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -601,14 +647,12 @@ the `Twig Environment`_:: class SomeService { - private $twig; - - public function __construct(Environment $twig) - { - $this->twig = $twig; + public function __construct( + private Environment $twig, + ) { } - public function someMethod() + public function someMethod(): void { // ... @@ -699,7 +743,7 @@ provided by Symfony: use Symfony\Bundle\FrameworkBundle\Controller\TemplateController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return function (RoutingConfigurator $routes): void { $routes->add('acme_privacy', '/privacy') ->controller(TemplateController::class) ->defaults([ @@ -725,14 +769,6 @@ provided by Symfony: ; }; -.. versionadded:: 5.1 - - The ``context`` option was introduced in Symfony 5.1. - -.. versionadded:: 5.4 - - The ``statusCode`` option was introduced in Symfony 5.4. - Checking if a Template Exists ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -782,6 +818,13 @@ errors. It's useful to run it before deploying your application to production # you can also show the deprecated features used in your templates $ php bin/console lint:twig --show-deprecations templates/email/ + # you can also excludes directories + $ php bin/console lint:twig templates/ --excludes=data_collector --excludes=dev_tool + +.. versionadded:: 7.1 + + The option to exclude directories was introduced in Symfony 7.1. + When running the linter inside `GitHub Actions`_, the output is automatically adapted to the format required by GitHub, but you can force that format too: @@ -789,10 +832,6 @@ adapted to the format required by GitHub, but you can force that format too: $ php bin/console lint:twig --format=github -.. versionadded:: 5.4 - - The ``github`` output format was introduced in Symfony 5.4. - Inspecting Twig Information ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -842,6 +881,10 @@ depending on your needs: and they are visible on the web page #} {{ dump(article) }} + {# optionally, use named arguments to display them as labels next to + the dumped contents #} + {{ dump(blog_posts: articles, user: app.user) }} + {{ article.title }} @@ -1012,7 +1055,7 @@ template fragments. Configure that special URL in the ``fragments`` option: // config/packages/framework.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { // ... $framework->fragments()->path('/_fragment'); }; @@ -1034,7 +1077,7 @@ JavaScript library. First, include the `hinclude.js`_ library in your page :ref:`linking to it ` from the template or adding it -to your application JavaScript :doc:`using Webpack Encore `. +to your application JavaScript :doc:`using AssetMapper `. As the embedded content comes from another page (or controller for that matter), Symfony uses a version of the standard ``render()`` function to configure @@ -1085,7 +1128,7 @@ default content rendering some template: // config/packages/framework.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { // ... $framework->fragments() ->hincludeDefaultTemplate('hinclude.html.twig') @@ -1119,6 +1162,8 @@ Use the ``attributes`` option to define the value of hinclude.js options: set this option to 'true' to run that JavaScript code #} {{ render_hinclude(controller('...'), {attributes: {evaljs: 'true'}}) }} +.. _template_inheritance-layouts: + Template Inheritance and Layouts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1228,7 +1273,7 @@ and leaves the repeated contents and HTML structure to some parent templates. .. code-block:: html+twig - {# app/Resources/views/blog/index.html.twig #} + {# templates/blog/index.html.twig #} {% extends 'base.html.twig' %} {# the line below is not captured by a "block" tag #} @@ -1263,7 +1308,7 @@ or XSS attack. To prevent this attack, use *"output escaping"* to transform the characters which have special meaning (e.g. replace ``<`` by the ``<`` HTML entity). Symfony applications are safe by default because they perform automatic output -escaping thanks to the :ref:`Twig autoescape option `: +escaping: .. code-block:: html+twig @@ -1331,7 +1376,7 @@ the ``value`` is the Twig namespace, which is explained later: // config/packages/twig.php use Symfony\Config\TwigConfig; - return static function (TwigConfig $twig) { + return static function (TwigConfig $twig): void { // ... // directories are relative to the project root dir (but you @@ -1387,7 +1432,7 @@ configuration to define a namespace for each template directory: // config/packages/twig.php use Symfony\Config\TwigConfig; - return static function (TwigConfig $twig) { + return static function (TwigConfig $twig): void { // ... $twig->path('email/default/templates', 'email'); @@ -1413,10 +1458,10 @@ may include their own Twig templates (in the ``Resources/views/`` directory of each bundle). To avoid messing with your own templates, Symfony adds bundle templates under an automatic namespace created after the bundle name. -For example, the templates of a bundle called ``AcmeFooBundle`` are available -under the ``AcmeFoo`` namespace. If this bundle includes the template -``/vendor/acmefoo-bundle/Resources/views/user/profile.html.twig``, -you can refer to it as ``@AcmeFoo/user/profile.html.twig``. +For example, the templates of a bundle called ``AcmeBlogBundle`` are available +under the ``AcmeBlog`` namespace. If this bundle includes the template +``/vendor/acme/blog-bundle/templates/user/profile.html.twig``, +you can refer to it as ``@AcmeBlog/user/profile.html.twig``. .. tip:: @@ -1459,14 +1504,14 @@ Create a class that extends ``AbstractExtension`` and fill in the logic:: class AppExtension extends AbstractExtension { - public function getFilters() + public function getFilters(): array { return [ new TwigFilter('price', [$this, 'formatPrice']), ]; } - public function formatPrice($number, $decimals = 0, $decPoint = '.', $thousandsSep = ',') + public function formatPrice(float $number, int $decimals = 0, string $decPoint = '.', string $thousandsSep = ','): string { $price = number_format($number, $decimals, $decPoint, $thousandsSep); $price = '$'.$price; @@ -1486,14 +1531,14 @@ If you want to create a function instead of a filter, define the class AppExtension extends AbstractExtension { - public function getFunctions() + public function getFunctions(): array { return [ new TwigFunction('area', [$this, 'calculateArea']), ]; } - public function calculateArea(int $width, int $length) + public function calculateArea(int $width, int $length): int { return $width * $length; } @@ -1551,7 +1596,7 @@ callable defined in ``getFilters()``:: class AppExtension extends AbstractExtension { - public function getFilters() + public function getFilters(): array { return [ // the logic of this filter is now implemented in a different class @@ -1577,7 +1622,7 @@ previous ``formatPrice()`` method:: // extensions, you'll need to inject services using this constructor } - public function formatPrice($number, $decimals = 0, $decPoint = '.', $thousandsSep = ',') + public function formatPrice(float $number, int $decimals = 0, string $decPoint = '.', string $thousandsSep = ','): string { $price = number_format($number, $decimals, $decPoint, $thousandsSep); $price = '$'.$price; @@ -1597,6 +1642,7 @@ for this class and :doc:`tag your service ` with ``twig .. _`GitHub Actions`: https://docs.github.com/en/free-pro-team@latest/actions .. _`global variables`: https://twig.symfony.com/doc/3.x/advanced.html#id1 .. _`hinclude.js`: https://mnot.github.io/hinclude/ +.. _`Turbo Streams`: https://symfony.com/bundles/ux-turbo/current/index.html .. _`official Twig extensions`: https://github.com/twigphp?q=extra .. _`snake case`: https://en.wikipedia.org/wiki/Snake_case .. _`tags`: https://twig.symfony.com/doc/3.x/tags/index.html diff --git a/testing.rst b/testing.rst index 281f8c45ad8..bc5578c6b36 100644 --- a/testing.rst +++ b/testing.rst @@ -121,7 +121,7 @@ class to help you creating and booting the kernel in your tests using class NewsletterGeneratorTest extends KernelTestCase { - public function testSomething() + public function testSomething(): void { self::bootKernel(); @@ -188,7 +188,7 @@ code to production: // config/packages/test/twig.php use Symfony\Config\TwigConfig; - return static function (TwigConfig $twig) { + return static function (TwigConfig $twig): void { $twig->strictVariables(true); }; @@ -255,7 +255,7 @@ the container is returned by ``static::getContainer()``:: class NewsletterGeneratorTest extends KernelTestCase { - public function testSomething() + public function testSomething(): void { // (1) boot the Symfony kernel self::bootKernel(); @@ -296,7 +296,7 @@ concrete one:: class NewsletterGeneratorTest extends KernelTestCase { - public function testSomething() + public function testSomething(): void { // ... same bootstrap as the section above @@ -309,7 +309,6 @@ concrete one:: ]) ; - // the following line won't work unless the alias is made public $container->set(NewsRepositoryInterface::class, $newsRepository); // will be injected the mocked repository @@ -319,52 +318,8 @@ concrete one:: } } -In order to make the alias public, you will need to update configuration for -the ``test`` environment as follows: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services_test.yaml - services: - # redefine the alias as it should be while making it public - App\Contracts\Repository\NewsRepositoryInterface: - alias: App\Repository\NewsRepository - public: true - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // config/services_test.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - use App\Contracts\Repository\NewsRepositoryInterface; - use App\Repository\NewsRepository; - - return static function (ContainerConfigurator $container) { - $container->services() - // redefine the alias as it should be while making it public - ->alias(NewsRepositoryInterface::class, NewsRepository::class) - ->public() - ; - }; +No further configuration is required, as the test service container is a special one +that allows you to interact with private services and aliases. .. _testing-databases: @@ -479,7 +434,7 @@ instance, to load ``Product`` objects into Doctrine, use:: class ProductFixture extends Fixture { - public function load(ObjectManager $manager) + public function load(ObjectManager $manager): void { $product = new Product(); $product->setName('Priceless widget'); @@ -706,10 +661,6 @@ will no longer be followed:: Logging in Users (Authentication) ................................. -.. versionadded:: 5.1 - - The ``loginUser()`` method was introduced in Symfony 5.1. - When you want to add application tests for protected pages, you have to first "login" as a user. Reproducing the actual steps - such as submitting a login form - makes a test very slow. For this reason, Symfony @@ -735,7 +686,7 @@ to simulate a login request:: { // ... - public function testVisitingWhileLoggedIn() + public function testVisitingWhileLoggedIn(): void { $client = static::createClient(); $userRepository = static::getContainer()->get(UserRepository::class); @@ -757,7 +708,9 @@ You can pass any :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface` instance to ``loginUser()``. This method creates a special :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\TestBrowserToken` object and -stores in the session of the test client. +stores in the session of the test client. If you need to define custom +attributes in this token, you can use the ``tokenAttributes`` argument of the +:method:`Symfony\\Bundle\\FrameworkBundle\\KernelBrowser::loginUser` method. .. note:: @@ -1010,7 +963,8 @@ Response Assertions Asserts a specific HTTP status code. ``assertResponseRedirects(?string $expectedLocation = null, ?int $expectedCode = null, string $message = '')`` Asserts the response is a redirect response (optionally, you can check - the target location and status code). + the target location and status code). The excepted location can be either + an absolute or a relative path. ``assertResponseHasHeader(string $headerName, string $message = '')``/``assertResponseNotHasHeader(string $headerName, string $message = '')`` Asserts the given header is (not) available on the response, e.g. ``assertResponseHasHeader('content-type');``. ``assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = '')``/``assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = '')`` @@ -1028,14 +982,6 @@ Response Assertions ``assertResponseIsUnprocessable(string $message = '')`` Asserts the response is unprocessable (HTTP status is 422) -.. versionadded:: 5.3 - - The ``assertResponseFormatSame()`` method was introduced in Symfony 5.3. - -.. versionadded:: 5.4 - - The ``assertResponseIsUnprocessable()`` method was introduced in Symfony 5.4. - Request Assertions .................. @@ -1064,22 +1010,26 @@ Browser Assertions self::assertThatForClient(new SomeCustomConstraint()); } -.. versionadded:: 5.4 - - The ``assertThatForClient()`` method was introduced in Symfony 5.4. - Crawler Assertions .................. ``assertSelectorExists(string $selector, string $message = '')``/``assertSelectorNotExists(string $selector, string $message = '')`` Asserts that the given selector does (not) match at least one element in the response. +``assertSelectorCount(int $expectedCount, string $selector, string $message = '')`` + Asserts that the expected number of selector elements are in the response ``assertSelectorTextContains(string $selector, string $text, string $message = '')``/``assertSelectorTextNotContains(string $selector, string $text, string $message = '')`` Asserts that the first element matching the given selector does (not) contain the expected text. +``assertAnySelectorTextContains(string $selector, string $text, string $message = '')``/``assertAnySelectorTextNotContains(string $selector, string $text, string $message = '')`` + Asserts that any element matching the given selector does (not) + contain the expected text. ``assertSelectorTextSame(string $selector, string $text, string $message = '')`` Asserts that the contents of the first element matching the given - selector does (not) equal the expected text. + selector does equal the expected text. +``assertAnySelectorTextSame(string $selector, string $text, string $message = '')`` + Asserts that the any element matching the given selector does equal the + expected text. ``assertPageTitleSame(string $expectedTitle, string $message = '')`` Asserts that the ```` element is equal to the given title. ``assertPageTitleContains(string $expectedTitle, string $message = '')`` @@ -1093,22 +1043,11 @@ Crawler Assertions Asserts that value of the field of the first form matching the given selector does (not) equal the expected value. -.. versionadded:: 5.2 - - The ``assertCheckboxChecked()``, ``assertCheckboxNotChecked()``, - ``assertFormValue()`` and ``assertNoFormValue()`` methods were introduced - in Symfony 5.2. - .. _mailer-assertions: Mailer Assertions ................. -.. versionadded:: 5.1 - - Starting from Symfony 5.1, the following assertions no longer require to make - a request with the ``Client`` in a test case extending the ``WebTestCase`` class. - ``assertEmailCount(int $count, ?string $transport = null, string $message = '')`` Asserts that the expected number of emails was sent. ``assertQueuedEmailCount(int $count, ?string $transport = null, string $message = '')`` @@ -1137,13 +1076,68 @@ Mailer Assertions Asserts that the given address header equals the expected e-mail address. This assertion normalizes addresses like ``Jane Smith <jane@example.com>`` into ``jane@example.com``. +``assertEmailSubjectContains(RawMessage $email, string $expectedValue, string $message = '')``/``assertEmailSubjectNotContains(RawMessage $email, string $expectedValue, string $message = '')`` + Asserts that the subject of the given email does (not) contain the + expected subject. + +Notifier Assertions +................... + +``assertNotificationCount(int $count, ?string $transportName = null, string $message = '')`` + Asserts that the given number of notifications has been created + (in total or for the given transport). +``assertQueuedNotificationCount(int $count, ?string $transportName = null, string $message = '')`` + Asserts that the given number of notifications are queued + (in total or for the given transport). +``assertNotificationIsQueued(MessageEvent $event, string $message = '')`` + Asserts that the given notification is queued. +``assertNotificationIsNotQueued(MessageEvent $event, string $message = '')`` + Asserts that the given notification is not queued. +``assertNotificationSubjectContains(MessageInterface $notification, string $text, string $message = '')`` + Asserts that the given text is included in the subject of + the given notification. +``assertNotificationSubjectNotContains(MessageInterface $notification, string $text, string $message = '')`` + Asserts that the given text is not included in the subject of + the given notification. +``assertNotificationTransportIsEqual(MessageInterface $notification, string $transportName, string $message = '')`` + Asserts that the name of the transport for the given notification + is the same as the given text. +``assertNotificationTransportIsNotEqual(MessageInterface $notification, string $transportName, string $message = '')`` + Asserts that the name of the transport for the given notification + is not the same as the given text. + +HttpClient Assertions +..................... + +.. tip:: + + For all the following assertions, ``$client->enableProfiler()`` must be + called before the code that will trigger HTTP request(s). + +``assertHttpClientRequest(string $expectedUrl, string $expectedMethod = 'GET', string|array|null $expectedBody = null, array $expectedHeaders = [], string $httpClientId = 'http_client')`` + Asserts that the given URL has been called using, if specified, + the given method body and headers. By default it will check on the HttpClient, + but you can also pass a specific HttpClient ID. + (It will succeed if the request has been called multiple times.) + +``assertNotHttpClientRequest(string $unexpectedUrl, string $expectedMethod = 'GET', string $httpClientId = 'http_client')`` + Asserts that the given URL has not been called using GET or the specified method. + By default it will check on the HttpClient, but a HttpClient id can be specified. + +``assertHttpClientRequestCount(int $count, string $httpClientId = 'http_client')`` + Asserts that the given number of requests has been made on the HttpClient. + By default it will check on the HttpClient, but you can also pass a specific + HttpClient ID. + +End to End Tests (E2E) +~~~~~~~~~~~~~~~~~~~~~~ + +If you need to test the application as a whole, including the JavaScript +code, you can use a real browser instead of the test client. This is +called an end-to-end test and it's a great way to test the application. -.. TODO -.. End to End Tests (E2E) -.. ---------------------- -.. * panther -.. * testing javascript -.. * UX or form collections as example? +This can be achieved thanks to the Panther component. You can learn more +about it in :doc:`the dedicated page </testing/end_to_end>`. Learn more ---------- @@ -1158,12 +1152,12 @@ Learn more .. _`PHPUnit`: https://phpunit.de/ .. _`documentation`: https://docs.phpunit.de/ -.. _`Writing Tests for PHPUnit`: https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html -.. _`PHPUnit documentation`: https://docs.phpunit.de/en/9.6/configuration.html +.. _`Writing Tests for PHPUnit`: https://docs.phpunit.de/en/10.5/writing-tests-for-phpunit.html +.. _`PHPUnit documentation`: https://docs.phpunit.de/en/10.5/configuration.html .. _`unit test`: https://en.wikipedia.org/wiki/Unit_testing .. _`DAMADoctrineTestBundle`: https://github.com/dmaicher/doctrine-test-bundle .. _`Doctrine data fixtures`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html .. _`DoctrineFixturesBundle documentation`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html .. _`SymfonyMakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html -.. _`PHPUnit Assertion`: https://docs.phpunit.de/en/9.6/assertions.html +.. _`PHPUnit Assertion`: https://docs.phpunit.de/en/10.3/assertions.html .. _`section 4.1.18 of RFC 3875`: https://tools.ietf.org/html/rfc3875#section-4.1.18 diff --git a/testing/bootstrap.rst b/testing/bootstrap.rst index c075552a9e3..59fc289f0be 100644 --- a/testing/bootstrap.rst +++ b/testing/bootstrap.rst @@ -25,14 +25,12 @@ You can modify this file to add custom logic: (new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); } - + if (isset($_ENV['BOOTSTRAP_CLEAR_CACHE_ENV'])) { - + // executes the "php bin/console cache:clear" command - + passthru(sprintf( - + 'APP_ENV=%s php "%s/../bin/console" cache:clear --no-warmup', - + $_ENV['BOOTSTRAP_CLEAR_CACHE_ENV'], - + __DIR__ - + )); - + } + + // executes the "php bin/console cache:clear" command + + passthru(sprintf( + + 'APP_ENV=%s php "%s/../bin/console" cache:clear --no-warmup', + + $_ENV['APP_ENV'], + + __DIR__ + + )); .. note:: @@ -49,21 +47,5 @@ You can modify this file to add custom logic: <!-- ... --> </phpunit> -Now, you can update the ``phpunit.xml.dist`` file to declare the custom -environment variable introduced to ``tests/bootstrap.php``: - -.. code-block:: xml - - <!-- phpunit.xml.dist --> - <?xml version="1.0" encoding="UTF-8" ?> - <phpunit> - <php> - <env name="BOOTSTRAP_CLEAR_CACHE_ENV" value="test"/> - <!-- ... --> - </php> - - <!-- ... --> - </phpunit> - Now, when running ``vendor/bin/phpunit``, the cache will be cleared automatically by the bootstrap file before running all tests. diff --git a/testing/database.rst b/testing/database.rst index 6c337ee07a3..fe74bbedd82 100644 --- a/testing/database.rst +++ b/testing/database.rst @@ -20,20 +20,18 @@ Suppose the class you want to test looks like this:: namespace App\Salary; use App\Entity\Employee; - use Doctrine\Persistence\ObjectManager; + use Doctrine\ORM\EntityManager; class SalaryCalculator { - private $objectManager; - - public function __construct(ObjectManager $objectManager) - { - $this->objectManager = $objectManager; + public function __construct( + private EntityManager $entityManager, + ) { } - public function calculateTotalSalary($id) + public function calculateTotalSalary(int $id): int { - $employeeRepository = $this->objectManager + $employeeRepository = $this->entityManager ->getRepository(Employee::class); $employee = $employeeRepository->find($id); @@ -49,37 +47,33 @@ constructor, you can pass a mock object within a test:: use App\Entity\Employee; use App\Salary\SalaryCalculator; - use Doctrine\Persistence\ObjectManager; - use Doctrine\Persistence\ObjectRepository; + use Doctrine\ORM\EntityManager; + use Doctrine\ORM\EntityRepository; use PHPUnit\Framework\TestCase; class SalaryCalculatorTest extends TestCase { - public function testCalculateTotalSalary() + public function testCalculateTotalSalary(): void { $employee = new Employee(); $employee->setSalary(1000); $employee->setBonus(1100); // Now, mock the repository so it returns the mock of the employee - $employeeRepository = $this->createMock(ObjectRepository::class); - // use getMock() on PHPUnit 5.3 or below - // $employeeRepository = $this->getMock(ObjectRepository::class); + $employeeRepository = $this->createMock(EntityRepository::class); $employeeRepository->expects($this->any()) ->method('find') ->willReturn($employee); // Last, mock the EntityManager to return the mock of the repository // (this is not needed if the class being tested injects the - // repository it uses instead of the entire object manager) - $objectManager = $this->createMock(ObjectManager::class); - // use getMock() on PHPUnit 5.3 or below - // $objectManager = $this->getMock(ObjectManager::class); - $objectManager->expects($this->any()) + // repository it uses instead of the entire entity manager) + $entityManager = $this->createMock(EntityManager::class); + $entityManager->expects($this->any()) ->method('getRepository') ->willReturn($employeeRepository); - $salaryCalculator = new SalaryCalculator($objectManager); + $salaryCalculator = new SalaryCalculator($entityManager); $this->assertEquals(2100, $salaryCalculator->calculateTotalSalary(1)); } } @@ -100,14 +94,12 @@ so, get the entity manager via the service container as follows:: namespace App\Tests\Repository; use App\Entity\Product; + use Doctrine\ORM\EntityManager; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class ProductRepositoryTest extends KernelTestCase { - /** - * @var \Doctrine\ORM\EntityManager - */ - private $entityManager; + private ?EntityManager $entityManager; protected function setUp(): void { @@ -118,7 +110,7 @@ so, get the entity manager via the service container as follows:: ->getManager(); } - public function testSearchByName() + public function testSearchByName(): void { $product = $this->entityManager ->getRepository(Product::class) diff --git a/testing/dom_crawler.rst b/testing/dom_crawler.rst index 65669698539..139d94efdd9 100644 --- a/testing/dom_crawler.rst +++ b/testing/dom_crawler.rst @@ -48,10 +48,12 @@ narrow down your node selection by chaining the method calls:: $crawler ->filter('h1') - ->reduce(function ($node, $i) { + ->reduce(function ($node, int $i): bool { if (!$node->attr('class')) { return false; } + + return true; }) ->first() ; @@ -86,6 +88,6 @@ The Crawler can extract information from the nodes:: $info = $crawler->extract(['_text', 'href']); // executes a lambda for each node and return an array of results - $data = $crawler->each(function ($node, $i) { + $data = $crawler->each(function ($node, int $i): string { return $node->attr('href'); }); diff --git a/testing/end_to_end.rst b/testing/end_to_end.rst new file mode 100644 index 00000000000..eede672bfce --- /dev/null +++ b/testing/end_to_end.rst @@ -0,0 +1,827 @@ +End-to-End Testing +================== + + The Panther component allows to drive a real web browser with PHP to create + end-to-end tests. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/panther + +.. include:: /components/require_autoload.rst.inc + +Introduction +------------ + +End to end tests are a special type of application tests that +simulate a real user interacting with your application. They are +typically used to test the user interface (UI) of your application +and the effects of these interactions (e.g. when I click on this button, a mail +must be sent). The difference with functional tests detailed above is +that End-to-End tests use a real browser instead of a simulated one. This +browser can run in headless mode (without a graphical interface) or not. +The first option is convenient for running tests in a Continuous Integration +(CI), while the second one is useful for debugging purpose. + +This is the purpose of Panther, a component that provides a real browser +to run your tests. Here are a few things that make Panther special, compared +to other testing tools provided by Symfony: + +* Possibility to take screenshots of the browser at any time during the test +* The JavaScript code contained in webpages is executed +* Panther supports everything that Chrome (or Firefox) implements +* Convenient way to test real-time applications (e.g. WebSockets, Server-Sent Events + with Mercure, etc.) + +Installing Web Drivers +~~~~~~~~~~~~~~~~~~~~~~ + +Panther uses the WebDriver protocol to control the browser used to crawl +websites. On all systems, you can use `dbrekelmans/browser-driver-installer`_ +to install ChromeDriver and geckodriver locally: + +.. code-block:: terminal + + $ composer require --dev dbrekelmans/bdi + + $ vendor/bin/bdi detect drivers + +Panther will detect and automatically use drivers stored in the ``drivers/`` directory +of your project when installing them manually. You can download `ChromeDriver`_ +for Chromium or Chrome and `GeckoDriver`_ for Firefox and put them anywhere in +your ``PATH`` or in the ``drivers/`` directory of your project. + +Alternatively, you can use the package manager of your operating system +to install them: + +.. code-block:: terminal + + # Ubuntu + $ apt-get install chromium-chromedriver firefox-geckodriver + + # MacOS, using Homebrew + $ brew install chromedriver geckodriver + + # Windows, using Chocolatey + $ choco install chromedriver selenium-gecko-driver + +.. _panther_phpunit-extension: + +Registering The PHPUnit Extension +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you intend to use Panther to test your application, it is strongly recommended +to register the Panther PHPUnit extension. While not strictly mandatory, this +extension dramatically improves the testing experience by boosting the performance +and allowing to use the :ref:`interactive debugging mode <panther_interactive-mode>`. + +When using the extension in conjunction with the ``PANTHER_ERROR_SCREENSHOT_DIR`` +environment variable, tests using the Panther client that fail or error (after the +client is created) will automatically get a screenshot taken to help debugging. + +To register the Panther extension, add the following lines to ``phpunit.xml.dist``: + +.. code-block:: xml + + <!-- phpunit.xml.dist --> + <extensions> + <extension class="Symfony\Component\Panther\ServerExtension"/> + </extensions> + +Without the extension, the web server used by Panther to serve the application +under test is started on demand and stopped when ``tearDownAfterClass()`` is called. +On the other hand, when the extension is registered, the web server will be stopped +only after the very last test. + +Usage +----- + +Here is an example of a snippet that uses Panther to test an application:: + + use Symfony\Component\Panther\Client; + + $client = Client::createChromeClient(); + // alternatively, create a Firefox client + $client = Client::createFirefoxClient(); + + $client->request('GET', 'https://api-platform.com'); + $client->clickLink('Getting started'); + + // wait for an element to be present in the DOM, even if hidden + $crawler = $client->waitFor('#installing-the-framework'); + // you can also wait for an element to be visible + $crawler = $client->waitForVisibility('#installing-the-framework'); + + // get the text of an element thanks to the query selector syntax + echo $crawler->filter('#installing-the-framework')->text(); + // take a screenshot of the current page + $client->takeScreenshot('screen.png'); + +.. note:: + + According to the specification, WebDriver implementations return only the + **displayed** text by default. When you filter on a ``head`` tag (like + ``title``), the method ``text()`` returns an empty string. Use the + ``html()`` method to get the complete contents of the tag, including the + tag itself. + +Creating a TestCase +~~~~~~~~~~~~~~~~~~~ + +The ``PantherTestCase`` class allows you to write end-to-end tests. It +automatically starts your app using the built-in PHP web server and lets +you crawl it using Panther. To provide all the testing tools you're used +to, it extends `PHPUnit`_'s ``TestCase``. + +If you are testing a Symfony application, ``PantherTestCase`` automatically +extends the :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase` class. +It means you can create functional tests, which can directly execute the +kernel of your application and access all your existing services. +In this case, you can use +:ref:`all crawler test assertions <testing-application-assertions>` +provided by Symfony with Panther. + +Here is an example of a ``PantherTestCase``:: + + namespace App\Tests; + + use Symfony\Component\Panther\PantherTestCase; + + class HomepageTest extends PantherTestCase + { + public function testMyApp(): void + { + // your app is automatically started using the built-in web server + $client = static::createPantherClient(); + $client->request('GET', '/home'); + + // use any PHPUnit assertion, including the ones provided by Symfony... + $this->assertPageTitleContains('My Title'); + $this->assertSelectorTextContains('#main', 'My body'); + + // ... or the one provided by Panther + $this->assertSelectorIsEnabled('.search'); + $this->assertSelectorIsDisabled('[type="submit"]'); + $this->assertSelectorIsVisible('.errors'); + $this->assertSelectorIsNotVisible('.loading'); + $this->assertSelectorAttributeContains('.price', 'data-old-price', '42'); + $this->assertSelectorAttributeNotContains('.price', 'data-old-price', '36'); + + // ... + } + } + +Panther client comes with methods that wait until some asynchronous process +finishes:: + + namespace App\Tests; + + use Symfony\Component\Panther\PantherTestCase; + + class HomepageTest extends PantherTestCase + { + public function testMyApp(): void + { + // ... + + // wait for element to be attached to the DOM + $client->waitFor('.popin'); + + // wait for element to be removed from the DOM + $client->waitForStaleness('.popin'); + + // wait for element of the DOM to become visible + $client->waitForVisibility('.loader'); + + // wait for element of the DOM to become hidden + $client->waitForInvisibility('.loader'); + + // wait for text to be inserted in the element content + $client->waitForElementToContain('.total', '25 €'); + + // wait for text to be removed from the element content + $client->waitForElementToNotContain('.promotion', '5%'); + + // wait for the button to become enabled + $client->waitForEnabled('[type="submit"]'); + + // wait for the button to become disabled + $client->waitForDisabled('[type="submit"]'); + + // wait for the attribute to contain content + $client->waitForAttributeToContain('.price', 'data-old-price', '25 €'); + + // wait for the attribute to not contain content + $client->waitForAttributeToNotContain('.price', 'data-old-price', '25 €'); + } + } + +Finally, you can also make assertions on things that will happen in the +future:: + + namespace App\Tests; + + use Symfony\Component\Panther\PantherTestCase; + + class HomepageTest extends PantherTestCase + { + public function testMyApp(): void + { + // ... + + // element will be attached to the DOM + $this->assertSelectorWillExist('.popin'); + + // element will be removed from the DOM + $this->assertSelectorWillNotExist('.popin'); + + // element will be visible + $this->assertSelectorWillBeVisible('.loader'); + + // element will not be visible + $this->assertSelectorWillNotBeVisible('.loader'); + + // text will be inserted in the element content + $this->assertSelectorWillContain('.total', '€25'); + + // text will be removed from the element content + $this->assertSelectorWillNotContain('.promotion', '5%'); + + // button will be enabled + $this->assertSelectorWillBeEnabled('[type="submit"]'); + + // button will be disabled + $this->assertSelectorWillBeDisabled('[type="submit"]'); + + // attribute will contain content + $this->assertSelectorAttributeWillContain('.price', 'data-old-price', '€25'); + + // attribute will not contain content + $this->assertSelectorAttributeWillNotContain('.price', 'data-old-price', '€25'); + } + } + +You can then run this test using PHPUnit, like you would for any other test: + +.. code-block:: terminal + + $ ./vendor/bin/phpunit tests/HomepageTest.php + +When writing end-to-end tests, you should keep in mind that they are +slower than other tests. If you need to check that the WebDriver connection +is still active during long-running tests, you can use the +``Client::ping()`` method which returns a boolean depending on the +connection status. + +Advanced Usage +-------------- + +Changing The Hostname and the Port Of The Web Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to change the host and/or the port used by the built-in web server, +pass the ``hostname`` and ``port`` to the ``$options`` parameter of the +``createPantherClient()`` method:: + + $client = self::createPantherClient([ + 'hostname' => 'example.com', // defaults to 127.0.0.1 + 'port' => 8080, // defaults to 9080 + ]); + +Using Browser-Kit Clients +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Panther also gives access to other BrowserKit-based implementations of +``Client`` and ``Crawler``. Unlike Panther's native client, these alternative +clients don't support JavaScript, CSS and screenshot capturing, but are way +faster. Two alternative clients are available: + +* The first directly manipulates the Symfony kernel provided by + ``WebTestCase``. It is the fastest client available, but it + is only available for Symfony applications. +* The second leverages :class:`Symfony\\Component\\BrowserKit\\HttpBrowser`. + It is an intermediate between Symfony's kernel and Panther's test clients. + ``HttpBrowser`` sends real HTTP requests using the + :doc:`HttpClient component </http_client>`. It is fast and is able to browse + any webpage, not only the ones of the application under test. + However, HttpBrowser doesn't support JavaScript and other advanced features + because it is entirely written in PHP. This one can be used in any PHP + application. + +Because all clients implement the exact same API, you can switch from one to +another just by calling the appropriate factory method, resulting in a good +trade-off for every single test case: if JavaScript is needed or not, if an +authentication against an external SSO has to be done, etc. + +Here is how to retrieve instances of these clients:: + + namespace App\Tests; + + use Symfony\Component\Panther\Client; + use Symfony\Component\Panther\PantherTestCase; + + class AppTest extends PantherTestCase + { + public function testMyApp(): void + { + // retrieve an existing client + $symfonyClient = static::createClient(); + $httpBrowserClient = static::createHttpBrowserClient(); + $pantherClient = static::createPantherClient(); + $firefoxClient = static::createPantherClient(['browser' => static::FIREFOX]); + + // create a custom client + $customChromeClient = Client::createChromeClient(null, null, [], 'https://example.com'); + $customFirefoxClient = Client::createFirefoxClient(null, null, [], 'https://example.com'); + $customSeleniumClient = Client::createSeleniumClient('http://127.0.0.1:4444/wd/hub', null, 'https://example.com'); + + // if you are testing a Symfony app, you also have access to the kernel + $kernel = static::createKernel(); + + // ... + } + } + +.. note:: + + When initializing a custom client, the integrated web server **is not** started + automatically. Use ``PantherTestCase::startWebServer()`` or the ``WebServerManager`` + class if you want to start it manually. + +Testing Real-Time Applications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Panther provides a convenient way to test applications with real-time +capabilities which use `Mercure`_, `WebSocket`_ and similar technologies. + +The ``PantherTestCase::createAdditionalPantherClient()`` method can create +additional, isolated browsers which can interact with other ones. For instance, +this can be useful to test a chat application having several users +connected simultaneously:: + + use Symfony\Component\Panther\PantherTestCase; + + class ChatTest extends PantherTestCase + { + public function testChat(): void + { + $client1 = self::createPantherClient(); + $client1->request('GET', '/chat'); + + // connect a 2nd user using an isolated browser + $client2 = self::createAdditionalPantherClient(); + $client2->request('GET', '/chat'); + $client2->submitForm('Post message', ['message' => 'Hi folks !']); + + // wait for the message to be received by the first client + $client1->waitFor('.message'); + + // Symfony Assertions are *always* executed in the primary browser + $this->assertSelectorTextContains('.message', 'Hi folks !'); + } + } + +Accessing Browser Console Logs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If needed, you can use Panther to access the content of the console:: + + use Symfony\Component\Panther\PantherTestCase; + + class ConsoleTest extends PantherTestCase + { + public function testConsole(): void + { + $client = self::createPantherClient( + [], + [], + [ + 'capabilities' => [ + 'goog:loggingPrefs' => [ + 'browser' => 'ALL', // calls to console.* methods + 'performance' => 'ALL', // performance data + ], + ], + ] + ); + + $client->request('GET', '/'); + + $consoleLogs = $client->getWebDriver()->manage()->getLog('browser'); + $performanceLogs = $client->getWebDriver()->manage()->getLog('performance'); // performance logs + } + } + +Passing Arguments to ChromeDriver +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If needed, you can configure `the arguments`_ to pass to the ``chromedriver`` binary:: + + use Symfony\Component\Panther\PantherTestCase; + + class MyTest extends PantherTestCase + { + public function testLogging(): void + { + $client = self::createPantherClient( + [], + [], + [ + 'chromedriver_arguments' => [ + '--log-path=myfile.log', + '--log-level=DEBUG' + ], + ] + ); + + $client->request('GET', '/'); + } + } + +Using a Proxy +~~~~~~~~~~~~~ + +To use a proxy server, you have to set the ``PANTHER_CHROME_ARGUMENTS``: + +.. code-block:: bash + + # .env.test + PANTHER_CHROME_ARGUMENTS='--proxy-server=socks://127.0.0.1:9050' + +Accepting Self-Signed SSL Certificates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To force Chrome to accept invalid and self-signed certificates, you can set the +following environment variable: ``PANTHER_CHROME_ARGUMENTS='--ignore-certificate-errors'``. + +.. caution:: + + This option is insecure, use it only for testing in development environments, + never in production (e.g. for web crawlers). + +For Firefox, instantiate the client like this, you can do this at client +creation:: + + $client = Client::createFirefoxClient(null, null, ['capabilities' => ['acceptInsecureCerts' => true]]); + +Using An External Web Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes, it's convenient to reuse an existing web server configuration +instead of starting the built-in PHP one. To do so, set the +``external_base_uri`` option when creating your client:: + + namespace App\Tests; + + use Symfony\Component\Panther\PantherTestCase; + + class E2eTest extends PantherTestCase + { + public function testMyApp(): void + { + $pantherClient = static::createPantherClient(['external_base_uri' => 'https://localhost']); + + // ... + } + } + +.. note:: + + When using an external web server, Panther will not start the built-in + PHP web server. + +Having a Multi-domain Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It happens that your PHP/Symfony application might serve several different +domain names. As Panther saves the client in memory between tests to improve +performance, you will have to run your tests in separate +processes if you write several tests using Panther for different domain names. + +To do so, you can use the native ``@runInSeparateProcess`` PHPUnit annotation. +Here is an example using the ``external_base_uri`` option to determine the +domain name used by the client when using separate processes:: + + // tests/FirstDomainTest.php + namespace App\Tests; + + use Symfony\Component\Panther\PantherTestCase; + + class FirstDomainTest extends PantherTestCase + { + /** + * @runInSeparateProcess + */ + public function testMyApp(): void + { + $pantherClient = static::createPantherClient([ + 'external_base_uri' => 'http://mydomain.localhost:8000', + ]); + + // ... + } + } + + // tests/SecondDomainTest.php + namespace App\Tests; + + use Symfony\Component\Panther\PantherTestCase; + + class SecondDomainTest extends PantherTestCase + { + /** + * @runInSeparateProcess + */ + public function testMyApp(): void + { + $pantherClient = static::createPantherClient([ + 'external_base_uri' => 'http://anotherdomain.localhost:8000', + ]); + + // ... + } + } + +Usage With Other Testing Tools +------------------------------ + +If you want to use Panther with other testing tools like `LiipFunctionalTestBundle`_ +or if you just need to use a different base class, you can use the +``Symfony\Component\Panther\PantherTestCaseTrait`` to enhance your existing +test-infrastructure with some Panther mechanisms:: + + namespace App\Tests\Controller; + + use Liip\FunctionalTestBundle\Test\WebTestCase; + use Symfony\Component\Panther\PantherTestCaseTrait; + + class DefaultControllerTest extends WebTestCase + { + use PantherTestCaseTrait; + + public function testWithFixtures(): void + { + $this->loadFixtures([]); // load your fixtures + $client = self::createPantherClient(); // create your panther client + + $client->request('GET', '/'); + + // ... + } + } + +Configuring Panther Through Environment Variables +------------------------------------------------- + +The following environment variables can be set to change some Panther's +behavior: + +``PANTHER_NO_HEADLESS`` + Disable the browser's headless mode (will display the testing window, useful to debug) +``PANTHER_WEB_SERVER_DIR`` + Change the project's document root (default to ``./public/``, relative paths **must start** by ``./``) +``PANTHER_WEB_SERVER_PORT`` + Change the web server's port (default to ``9080``) +``PANTHER_WEB_SERVER_ROUTER`` + Use a web server router script which is run at the start of each HTTP request +``PANTHER_EXTERNAL_BASE_URI`` + Use an external web server (the PHP built-in web server will not be started) +``PANTHER_APP_ENV`` + Override the ``APP_ENV`` variable passed to the web server running the PHP app +``PANTHER_ERROR_SCREENSHOT_DIR`` + Set a base directory for your failure/error screenshots (e.g. ``./var/error-screenshots``) +``PANTHER_DEVTOOLS`` + Toggle the browser's dev tools (default ``enabled``, useful to debug) +``PANTHER_ERROR_SCREENSHOT_ATTACH`` + Add screenshots mentioned above to test output in junit attachment format + +Chrome Specific Environment Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``PANTHER_NO_SANDBOX`` + Disable `Chrome's sandboxing`_ (unsafe, but allows to use Panther in containers) +``PANTHER_CHROME_ARGUMENTS`` + Customize Chrome arguments. You need to set ``PANTHER_NO_HEADLESS`` to fully customize +``PANTHER_CHROME_BINARY`` + To use another ``google-chrome`` binary + +Firefox Specific Environment Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``PANTHER_FIREFOX_ARGUMENTS`` + Customize Firefox arguments. You need to set ``PANTHER_NO_HEADLESS`` to fully customize +``PANTHER_FIREFOX_BINARY`` + To use another ``firefox`` binary + +.. _panther_interactive-mode: + +Interactive Mode +---------------- + +Panther can make a pause in your tests suites after a failure. +Thanks to this break time, you can investigate the encountered problem through +the web browser. To enable this mode, you need the ``--debug`` PHPUnit option +without the headless mode: + +.. code-block:: terminal + + $ PANTHER_NO_HEADLESS=1 bin/phpunit --debug + + Test 'App\AdminTest::testLogin' started + Error: something is wrong. + + Press enter to continue... + +To use the interactive mode, the +:ref:`PHPUnit extension <panther_phpunit-extension>` has to be registered. + +Docker Integration +------------------ + +Here is a minimal Docker image that can run Panther with both Chrome and +Firefox: + +.. code-block:: dockerfile + + FROM php:alpine + + # Chromium and ChromeDriver + ENV PANTHER_NO_SANDBOX 1 + # Not mandatory, but recommended + ENV PANTHER_CHROME_ARGUMENTS='--disable-dev-shm-usage' + RUN apk add --no-cache chromium chromium-chromedriver + + # Firefox and GeckoDriver (optional) + ARG GECKODRIVER_VERSION=0.28.0 + RUN apk add --no-cache firefox libzip-dev; \ + docker-php-ext-install zip + RUN wget -q https://github.com/mozilla/geckodriver/releases/download/v$GECKODRIVER_VERSION/geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz; \ + tar -zxf geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz -C /usr/bin; \ + rm geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz + +You can then build and run your image: + +.. code-block:: bash + + $ docker build . -t myproject + $ docker run -it -v "$PWD":/srv/myproject -w /srv/myproject myproject bin/phpunit + +Integrating Panther In Your CI +------------------------------ + +Github Actions +~~~~~~~~~~~~~~ + +Panther works out of the box with `GitHub Actions`_. +Here is a minimal ``.github/workflows/panther.yaml`` file to run Panther tests: + +.. code-block:: yaml + + name: Run Panther tests + + on: [ push, pull_request ] + + jobs: + tests: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: "ramsey/composer-install@v2" + + - name: Install dependencies + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + + - name: Run test suite + run: bin/phpunit + +Travis CI +~~~~~~~~~ + +Panther will work out of the box with `Travis CI`_ if you add the Chrome addon. +Here is a minimal ``.travis.yaml`` file to run Panther tests: + +.. code-block:: yaml + + language: php + addons: + # If you don't use Chrome, or Firefox, remove the corresponding line + chrome: stable + firefox: latest + + php: + - 8.0 + + script: + - bin/phpunit + +Gitlab CI +~~~~~~~~~ + +Here is a minimal ``.gitlab-ci.yaml`` file to run Panther tests +with `Gitlab CI`_: + +.. code-block:: yaml + + image: ubuntu + + before_script: + - apt-get update + - apt-get install software-properties-common -y + - ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime + - apt-get install curl wget php php-cli php8.1 php8.1-common php8.1-curl php8.1-intl php8.1-xml php8.1-opcache php8.1-mbstring php8.1-zip libfontconfig1 fontconfig libxrender-dev libfreetype6 libxrender1 zlib1g-dev xvfb chromium-chromedriver firefox-geckodriver -y -qq + - export PANTHER_NO_SANDBOX=1 + - export PANTHER_WEB_SERVER_PORT=9080 + - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" + - php composer-setup.php --install-dir=/usr/local/bin --filename=composer + - php -r "unlink('composer-setup.php');" + - composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + + test: + script: + - bin/phpunit + +AppVeyor +~~~~~~~~ + +Panther will work out of the box with `AppVeyor`_ as long as Google Chrome +is installed. Here is a minimal ``appveyor.yaml`` file to run Panther tests: + +.. code-block:: yaml + + build: false + platform: x86 + clone_folder: c:\projects\myproject + + cache: + - '%LOCALAPPDATA%\Composer\files' + + install: + - ps: Set-Service wuauserv -StartupType Manual + - cinst -y php composer googlechrome chromedriver firfox selenium-gecko-driver + - refreshenv + - cd c:\tools\php80 + - copy php.ini-production php.ini /Y + - echo date.timezone="UTC" >> php.ini + - echo extension_dir=ext >> php.ini + - echo extension=php_openssl.dll >> php.ini + - echo extension=php_mbstring.dll >> php.ini + - echo extension=php_curl.dll >> php.ini + - echo memory_limit=3G >> php.ini + - cd %APPVEYOR_BUILD_FOLDER% + - composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + + test_script: + - cd %APPVEYOR_BUILD_FOLDER% + - php bin\phpunit + +Known Limitations and Troubleshooting +------------------------------------- + +The following features are not currently supported: + +* Crawling XML documents (only HTML is supported) +* Updating existing documents (browsers are mostly used to consume data, not to create webpages) +* Setting form values using the multidimensional PHP array syntax +* Methods returning an instance of ``\DOMElement`` (because this library uses ``WebDriverElement`` internally) +* Selecting invalid choices in select + +Also, there is a known issue if you are using Bootstrap 5. It implements a +scrolling effect which tends to mislead Panther. To fix this, we advise you to +deactivate this effect by setting the Bootstrap 5 ``$enable-smooth-scroll`` +variable to ``false`` in your style file: + +.. code-block:: scss + + $enable-smooth-scroll: false; + +Additional Documentation +------------------------ + +Since Panther implements the API of popular libraries, you can find even more +documentation: + +* For the ``Client`` class, by reading the + :doc:`BrowserKit component </components/browser_kit>` page +* For the ``Crawler`` class, by reading the + :doc:`DomCrawler component </components/dom_crawler>` page +* For WebDriver, by reading the `PHP WebDriver documentation`_ + +.. _`dbrekelmans/browser-driver-installer`: https://github.com/dbrekelmans/browser-driver-installer +.. _`ChromeDriver`: https://sites.google.com/chromium.org/driver/ +.. _`GeckoDriver`: https://github.com/mozilla/geckodriver +.. _`PHPUnit`: https://phpunit.de/ +.. _`Mercure`: https://mercure.rocks/ +.. _`WebSocket`: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API +.. _`the arguments`: https://chromedriver.chromium.org/logging#TOC-All-languages +.. _`PHP WebDriver documentation`: https://github.com/php-webdriver/php-webdriver +.. _`Chrome's sandboxing`: https://chromium.googlesource.com/chromium/src/+/b4730a0c2773d8f6728946013eb812c6d3975bec/docs/design/sandbox.md +.. _`GitHub Actions`: https://help.github.com/en/actions +.. _`Travis CI`: https://travis-ci.com/ +.. _`Gitlab CI`: https://docs.gitlab.com/ee/ci/ +.. _`AppVeyor`: https://www.appveyor.com/ +.. _`LiipFunctionalTestBundle`: https://github.com/liip/LiipFunctionalTestBundle diff --git a/testing/http_authentication.rst b/testing/http_authentication.rst deleted file mode 100644 index 46ddb82b87d..00000000000 --- a/testing/http_authentication.rst +++ /dev/null @@ -1,14 +0,0 @@ -How to Simulate HTTP Authentication in a Functional Test -======================================================== - -.. caution:: - - Starting from Symfony 5.1, a ``loginUser()`` method was introduced to - ease testing secured applications. See :ref:`testing_logging_in_users` - for more information about this. - - If you are still using an older version of Symfony, view - `previous versions of this article`_ for information on how to simulate - HTTP authentication. - -.. _previous versions of this article: https://symfony.com/doc/5.0/testing/http_authentication.html diff --git a/testing/profiling.rst b/testing/profiling.rst index f7e2d8e54da..085cd100c2d 100644 --- a/testing/profiling.rst +++ b/testing/profiling.rst @@ -46,7 +46,7 @@ tests significantly. That's why Symfony disables it by default: // config/packages/test/web_profiler.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { // ... $framework->profiler() ->enabled(true) @@ -73,7 +73,7 @@ provided by the collectors obtained through the ``$client->getProfile()`` call:: class LuckyControllerTest extends WebTestCase { - public function testRandomNumber() + public function testRandomNumber(): void { $client = static::createClient(); diff --git a/translation.rst b/translation.rst index 15c34460d86..f1704ddccb2 100644 --- a/translation.rst +++ b/translation.rst @@ -95,7 +95,7 @@ are located: // config/packages/translation.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { // ... $framework ->defaultLocale('en') @@ -104,6 +104,11 @@ are located: ; }; +.. tip:: + + You can also define the :ref:`enabled_locales option <reference-translator-enabled-locales>` + to restrict the locales that your application is available in. + .. _translation-basic: Basic Translation @@ -119,7 +124,7 @@ controller:: // ... use Symfony\Contracts\Translation\TranslatorInterface; - public function index(TranslatorInterface $translator) + public function index(TranslatorInterface $translator): Response { $translated = $translator->trans('Symfony is great'); @@ -256,9 +261,10 @@ using the ``trans()`` method: #. A catalog of translated messages is loaded from translation resources defined for the ``locale`` (e.g. ``fr_FR``). Messages from the - :ref:`fallback locale <translation-fallback>` are also loaded and added to - the catalog if they don't already exist. The end result is a large - "dictionary" of translations. + :ref:`fallback locale <translation-fallback>` and the + :ref:`enabled locales <reference-translator-enabled-locales>` are also + loaded and added to the catalog if they don't already exist. The end result + is a large "dictionary" of translations. #. If the message is located in the catalog, the translation is returned. If not, the translator returns the original message. @@ -295,10 +301,6 @@ using PHP's :phpclass:`MessageFormatter` class. Read more about this in Translatable Objects -------------------- -.. versionadded:: 5.2 - - Translatable objects were introduced in Symfony 5.2. - Sometimes translating contents in templates is cumbersome because you need the original message, the translation parameters and the translation domain for each content. Making the translation in the controller or services simplifies @@ -326,6 +328,10 @@ Templates are now much simpler because you can pass translatable objects to the <h1>{{ message|trans }}</h1> <p>{{ status|trans }}</p> +.. tip:: + + The translation parameters can also be a :class:`Symfony\\Component\\Translation\\TranslatableMessage`. + .. tip:: There's also a :ref:`function called t() <reference-twig-function-t>`, @@ -438,27 +444,28 @@ with these tasks: # check out the command help to see its options (prefix, output format, domain, sorting, etc.) $ php bin/console translation:extract --help -.. deprecated:: 5.4 - - In previous Symfony versions, the ``translation:extract`` command was called - ``translation:update``, but that name was deprecated in Symfony 5.4 - and it will be removed in Symfony 6.0. - The ``translation:extract`` command looks for missing translations in: * Templates stored in the ``templates/`` directory (or any other directory defined in the :ref:`twig.default_path <config-twig-default-path>` and :ref:`twig.paths <config-twig-paths>` config options); * Any PHP file/class that injects or :doc:`autowires </service_container/autowiring>` - the ``translator`` service and makes calls to the ``trans()`` method. + the ``translator`` service and makes calls to the ``trans()`` method; * Any PHP file/class stored in the ``src/`` directory that creates :ref:`translatable objects <translatable-objects>` using the constructor or - the ``t()`` method or calls the ``trans()`` method. + the ``t()`` method or calls the ``trans()`` method; +* Any PHP file/class stored in the ``src/`` directory that uses + :ref:`Constraints Attributes <validation-constraints>` with ``*message`` named argument(s). + +.. tip:: -.. versionadded:: 5.3 + Install the ``nikic/php-parser`` package in your project to improve the + results of the ``translation:extract`` command. This package enables an + `AST`_ parser that can find many more translatable items: + + .. code-block:: terminal - Support for extracting Translatable objects has been introduced in - Symfony 5.3. + $ composer require nikic/php-parser .. _translation-resource-locations: @@ -468,7 +475,8 @@ Translation Resource/File Names and Locations Symfony looks for message files (i.e. translations) in the following default locations: * the ``translations/`` directory (at the root of the project); -* the ``Resources/translations/`` directory inside of any bundle. +* the ``translations/`` directory inside of any bundle (and also their + ``Resources/translations/`` directory, which is no longer recommended for bundles). The locations are listed here with the highest priority first. That is, you can override the translation messages of a bundle in the first directory. @@ -489,18 +497,18 @@ must be named according to the following path: ``domain.locale.loader``: ``php``, ``yaml``, etc). The loader can be the name of any registered loader. By default, Symfony -provides many loaders: +provides many loaders which are selected based on the following file extensions: -* ``.yaml``: YAML file -* ``.xlf``: XLIFF file; -* ``.php``: Returning a PHP array; +* ``.yaml``: YAML file (you can also use the ``.yml`` file extension); +* ``.xlf``: XLIFF file (you can also use the ``.xliff`` file extension); +* ``.php``: a PHP file that returns an array with the translations; * ``.csv``: CSV file; * ``.json``: JSON file; * ``.ini``: INI file; -* ``.dat``, ``.res``: ICU resource bundle; -* ``.mo``: Machine object format; -* ``.po``: Portable object format; -* ``.qt``: QT Translations XML file; +* ``.dat``, ``.res``: `ICU resource bundle`_; +* ``.mo``: `Machine object format`_; +* ``.po``: `Portable object format`_; +* ``.qt``: `QT Translations TS XML`_ file; The choice of which loader to use is entirely up to you and is a matter of taste. The recommended option is to use YAML for simple projects and use XLIFF @@ -555,7 +563,7 @@ if you're generating translations with specialized programs or teams. // config/packages/translation.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->translator() ->paths(['%kernel.project_dir%/custom/path/to/translations']) ; @@ -582,10 +590,6 @@ interface. See the :ref:`dic-tags-translation-loader` tag for more information. Translation Providers --------------------- -.. versionadded:: 5.3 - - Translation providers were introduced in Symfony 5.3. - When using external translators to translate your application, you must send them the new contents to translate frequently and merge the results back in the application. @@ -607,6 +611,7 @@ Provider Install with `Crowdin`_ ``composer require symfony/crowdin-translation-provider`` `Loco (localise.biz)`_ ``composer require symfony/loco-translation-provider`` `Lokalise`_ ``composer require symfony/lokalise-translation-provider`` +`Phrase`_ ``composer require symfony/phrase-translation-provider`` ====================== =========================================================== Each library includes a :ref:`Symfony Flex recipe <symfony-flex>` that will add @@ -635,9 +640,10 @@ This table shows the full list of available DSN formats for each provider: ====================== ============================================================== Provider DSN ====================== ============================================================== -`Crowdin`_ ``crowdin://PROJECT_ID:API_TOKEN@ORGANIZATION_DOMAIN.default`` +`Crowdin`_ ``crowdin://PROJECT_ID:API_TOKEN@ORGANIZATION_DOMAIN.default`` `Loco (localise.biz)`_ ``loco://API_KEY@default`` `Lokalise`_ ``lokalise://PROJECT_ID:API_KEY@default`` +`Phrase`_ ``phrase://PROJECT_ID:API_TOKEN@default?userAgent=myProject`` ====================== ============================================================== To enable a translation provider, customize the DSN in your ``.env`` file and @@ -696,6 +702,21 @@ configure the ``providers`` option: ], ]); +.. important:: + + If you use Phrase as a provider you must configure a user agent in your dsn. See + `Identification via User-Agent`_ for reasoning and some examples. + + Also make the locale _names_ in Phrase should be as defined in RFC4646 (e.g. pt-BR rather than pt_BR). + Not doing so will result in Phrase creating a new locale for the imported keys. + +.. tip:: + + If you use Crowdin as a provider and some of your locales are different from + the `Crowdin Language Codes`_, you have to set the `Custom Language Codes`_ in the Crowdin project + for each of your locales, in order to override the default value. You need to select the + "locale" placeholder and specify the custom code in the "Custom Code" field. + .. tip:: If you use Lokalise as a provider and a locale format following the `ISO @@ -705,6 +726,12 @@ configure the ``providers`` option: capital letters that specifies the national variety (e.g. "GB" or "US" according to `ISO 3166-1 alpha-2`_)). +.. tip:: + + The Phrase provider uses Phrase's tag feature to map translations to Symfony's translation + domains. If you need some assistance with organising your tags in Phrase, you might want + to consider the `Phrase Tag Bundle`_ which provides some commands helping you with that. + Pushing and Pulling Translations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -746,6 +773,10 @@ now use the following commands to push (upload) and pull (download) translations # check out the command help to see its options (format, domains, locales, intl-icu, etc.) $ php bin/console translation:pull --help + # the "--as-tree" option will write YAML messages as a tree-like structure instead + # of flat keys + $ php bin/console translation:pull loco --force --as-tree + Creating Custom Providers ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -769,7 +800,7 @@ is stored in the request and is accessible via the ``Request`` object:: use Symfony\Component\HttpFoundation\Request; - public function index(Request $request) + public function index(Request $request): void { $locale = $request->getLocale(); } @@ -778,7 +809,7 @@ To set the user's locale, you may want to create a custom event listener so that it's set before any other parts of the system (i.e. the translator) need it:: - public function onKernelRequest(RequestEvent $event) + public function onKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); @@ -824,28 +855,6 @@ A better policy is to include the locale in the URL using the .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/ContactController.php - namespace App\Controller; - - // ... - class ContactController extends AbstractController - { - /** - * @Route( - * "/{_locale}/contact", - * name="contact", - * requirements={ - * "_locale": "en|fr|de", - * } - * ) - */ - public function contact() - { - } - } - .. code-block:: php-attributes // src/Controller/ContactController.php @@ -861,8 +870,9 @@ A better policy is to include the locale in the URL using the '_locale' => 'en|fr|de', ], )] - public function contact() + public function contact(): Response { + // ... } } @@ -896,7 +906,7 @@ A better policy is to include the locale in the URL using the use App\Controller\ContactController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return function (RoutingConfigurator $routes): void { $routes->add('contact', '/{_locale}/contact') ->controller([ContactController::class, 'index']) ->requirements([ @@ -956,13 +966,38 @@ the framework: // config/packages/translation.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $framework->defaultLocale('en'); }; This ``default_locale`` is also relevant for the translator, as shown in the next section. +Selecting the Language Preferred by the User +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your application supports multiple languages, the first time a user visits your +site it's common to redirect them to the best possible language according to their +preferences. This is achieved with the ``getPreferredLanguage()`` method of the +:ref:`Request object <controller-request-argument>`:: + + // get the Request object somehow (e.g. as a controller argument) + $request = ... + // pass an array of the locales (their script and region parts are optional) supported + // by your application and the method returns the best locale for the current user + $locale = $request->getPreferredLanguage(['pt', 'fr_Latn_CH', 'en_US'] ); + +Symfony finds the best possible language based on the locales passed as argument +and the value of the ``Accept-Language`` HTTP header. If it can't find a perfect +match between them, Symfony will try to find a partial match based on the language +(e.g. ``fr_CA`` would match ``fr_Latn_CH`` because their language is the same). +If there's no perfect or partial match, this method returns the first locale passed +as argument (that's why the order of the passed locales is important). + +.. versionadded:: 7.1 + + The feature to match locales partially was introduced in Symfony 7.1. + .. _translation-fallback: Fallback Translation Locales @@ -1021,7 +1056,7 @@ checks translation resources for several locales: // config/packages/translation.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { // ... $framework->translator() ->fallbacks(['en']) @@ -1034,6 +1069,72 @@ checks translation resources for several locales: add the missing translation to the log file. For details, see :ref:`reference-framework-translator-logging`. +.. _locale-switcher: + +Switch Locale Programmatically +------------------------------ + +Sometimes you need to change the locale of the application dynamically +just to run some code. Imagine a console command that renders Twig templates +of emails in different languages. You need to change the locale only to +render those templates. + +The ``LocaleSwitcher`` class allows you to change at once the locale +of: + +* All the services that are tagged with ``kernel.locale_aware``; +* ``\Locale::setDefault()``; +* If the ``RequestContext`` service is available, the ``_locale`` + parameter (so urls are generated with the new locale):: + + use Symfony\Component\Translation\LocaleSwitcher; + + class SomeService + { + public function __construct( + private LocaleSwitcher $localeSwitcher, + ) { + } + + public function someMethod(): void + { + // you can get the current application locale like this: + $currentLocale = $this->localeSwitcher->getLocale(); + + // you can set the locale for the entire application like this: + // (from now on, the application will use 'fr' (French) as the + // locale; including the default locale used to translate Twig templates) + $this->localeSwitcher->setLocale('fr'); + + // reset the current locale of your application to the configured default locale + // in config/packages/translation.yaml, by option 'default_locale' + $this->localeSwitcher->reset(); + + // you can also run some code with a certain locale, without + // changing the locale for the rest of the application + $this->localeSwitcher->runWithLocale('es', function() { + + // e.g. render here some Twig templates using 'es' (Spanish) locale + + }); + + // you can optionally declare an argument in your callback to receive the + // injected locale + $this->localeSwitcher->runWithLocale('es', function(string $locale) { + + // here, the $locale argument will be set to 'es' + + }); + + // ... + } + } + +When using :ref:`autowiring <services-autowire>`, type-hint any controller or +service argument with the :class:`Symfony\\Component\\Translation\\LocaleSwitcher` +class to inject the locale switcher service. Otherwise, configure your services +manually and inject the ``translation.locale_switcher`` service. + .. _translation-debug: How to Find Missing or Unused Translation Messages @@ -1244,10 +1345,6 @@ These constants are defined as "bit masks", so you can combine them as follows:: // ... there are missing and/or unused translations } -.. versionadded:: 5.1 - - The exit codes were introduced in Symfony 5.1 - .. _translation-lint: How to Find Errors in Translation Files @@ -1289,11 +1386,6 @@ adapted to the format required by GitHub, but you can force that format too: $ php bin/console lint:yaml translations/ --format=github $ php bin/console lint:xliff translations/ --format=github -.. versionadded:: 5.3 - - The ``github`` output format was introduced in Symfony 5.3 for ``lint:yaml`` - and in Symfony 5.4 for ``lint:xliff``. - .. tip:: The Yaml component provides a stand-alone ``yaml-lint`` binary allowing @@ -1303,17 +1395,9 @@ adapted to the format required by GitHub, but you can force that format too: $ php vendor/bin/yaml-lint translations/ - .. versionadded:: 5.1 - - The ``yaml-lint`` binary was introduced in Symfony 5.1. - Pseudo-localization translator ------------------------------ -.. versionadded:: 5.2 - - The pseudolocalization translator was introduced in Symfony 5.2. - .. note:: The pseudolocalization translator is meant to be used for development only. @@ -1474,9 +1558,19 @@ Learn more .. _`Translatable Extension`: https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/translatable.md .. _`Translatable Behavior`: https://github.com/KnpLabs/DoctrineBehaviors .. _`Custom Language Name setting`: https://docs.lokalise.com/en/articles/1400492-uploading-files#custom-language-codes +.. _`ICU resource bundle`: https://github.com/unicode-org/icu-docs/blob/main/design/bnf_rb.txt +.. _`Portable object format`: https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html +.. _`Machine object format`: https://www.gnu.org/software/gettext/manual/html_node/MO-Files.html +.. _`QT Translations TS XML`: https://doc.qt.io/qt-5/linguist-ts-file-format.html .. _`GitHub Actions`: https://docs.github.com/en/free-pro-team@latest/actions .. _`pseudolocalization`: https://en.wikipedia.org/wiki/Pseudolocalization .. _`Symfony Demo`: https://github.com/symfony/demo +.. _`Crowdin Language Codes`: https://developer.crowdin.com/language-codes +.. _`Custom Language Codes`: https://support.crowdin.com/project-settings/#languages +.. _`Identification via User-Agent`: https://developers.phrase.com/api/#overview--identification-via-user-agent +.. _`Phrase Tag Bundle`: https://github.com/wickedOne/phrase-tag-bundle .. _`Crowdin`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Translation/Bridge/Crowdin/README.md .. _`Loco (localise.biz)`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Translation/Bridge/Loco/README.md .. _`Lokalise`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Translation/Bridge/Lokalise/README.md +.. _`Phrase`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Translation/Bridge/Phrase/README.md +.. _`AST`: https://en.wikipedia.org/wiki/Abstract_syntax_tree diff --git a/validation.rst b/validation.rst index 8a68f29391a..3f9b16785d1 100644 --- a/validation.rst +++ b/validation.rst @@ -16,7 +16,7 @@ install the validator before using it: .. code-block:: terminal - $ composer require symfony/validator doctrine/annotations + $ composer require symfony/validator .. note:: @@ -36,7 +36,7 @@ your application:: class Author { - private $name; + private string $name; } So far, this is an ordinary class that serves some purpose inside your @@ -44,7 +44,7 @@ application. The goal of validation is to tell you if the data of an object is valid. For this to work, you'll configure a list of rules (called :ref:`constraints <validation-constraints>`) that the object must follow in order to be valid. These rules are usually defined using PHP code or -annotations but they can also be defined as ``.yaml`` or ``.xml`` files inside +attributes but they can also be defined as ``.yaml`` or ``.xml`` files inside the ``config/validator/`` directory: For example, to indicate that the ``$name`` property must not be empty, add the @@ -52,22 +52,6 @@ following: .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/Author.php - namespace App\Entity; - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\NotBlank - */ - private $name; - } - .. code-block:: php-attributes // src/Entity/Author.php @@ -79,7 +63,7 @@ following: class Author { #[Assert\NotBlank] - private $name; + private string $name; } .. code-block:: yaml @@ -116,9 +100,9 @@ following: class Author { - private $name; + private string $name; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('name', new NotBlank()); } @@ -152,7 +136,7 @@ returned. Take this simple example from inside a controller:: use Symfony\Component\Validator\Validator\ValidatorInterface; // ... - public function author(ValidatorInterface $validator) + public function author(ValidatorInterface $validator): Response { $author = new Author(); @@ -215,7 +199,9 @@ Inside the template, you can output the list of errors exactly as needed: .. note:: Each validation error (called a "constraint violation"), is represented by - a :class:`Symfony\\Component\\Validator\\ConstraintViolation` object. + a :class:`Symfony\\Component\\Validator\\ConstraintViolation` object. This + object allows you, among other things, to get the constraint that caused this + violation thanks to the ``ConstraintViolation::getConstraint()`` method. Validation Callables ~~~~~~~~~~~~~~~~~~~~ @@ -231,14 +217,6 @@ when :ref:`validating OptionsResolver values <optionsresolver-validate-value>`): :method:`Symfony\\Component\\Validator\\Validation::createIsValidCallable` This returns a closure that returns ``false`` when the constraints aren't matched. -.. versionadded:: 5.1 - - ``Validation::createCallable()`` was introduced in Symfony 5.1. - -.. versionadded:: 5.3 - - ``Validation::createIsValidCallable()`` was introduced in Symfony 5.3. - .. _validation-constraints: Constraints @@ -278,27 +256,6 @@ literature genre mostly associated with the author, which can be set to either .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/Author.php - namespace App\Entity; - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\Choice( - * choices = {"fiction", "non-fiction"}, - * message = "Choose a valid genre." - * ) - */ - private $genre; - - // ... - } - .. code-block:: php-attributes // src/Entity/Author.php @@ -313,7 +270,7 @@ literature genre mostly associated with the author, which can be set to either choices: ['fiction', 'non-fiction'], message: 'Choose a valid genre.', )] - private $genre; + private string $genre; // ... } @@ -362,11 +319,11 @@ literature genre mostly associated with the author, which can be set to either class Author { - private $genre; + private string $genre; // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { // ... @@ -386,24 +343,6 @@ options can be specified in this way. .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/Author.php - namespace App\Entity; - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\Choice({"fiction", "non-fiction"}) - */ - private $genre; - - // ... - } - .. code-block:: php-attributes // src/Entity/Author.php @@ -415,7 +354,7 @@ options can be specified in this way. class Author { #[Assert\Choice(['fiction', 'non-fiction'])] - private $genre; + private string $genre; // ... } @@ -461,9 +400,9 @@ options can be specified in this way. class Author { - private $genre; + private string $genre; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { // ... @@ -487,7 +426,7 @@ Constraints in Form Classes Constraints can be defined while building the form via the ``constraints`` option of the form fields:: - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('myField', TextType::class, [ @@ -520,22 +459,6 @@ class to have at least 3 characters. .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/Author.php - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\NotBlank - * @Assert\Length(min=3) - */ - private $firstName; - } - .. code-block:: php-attributes // src/Entity/Author.php @@ -547,7 +470,7 @@ class to have at least 3 characters. { #[Assert\NotBlank] #[Assert\Length(min: 3)] - private $firstName; + private string $firstName; } .. code-block:: yaml @@ -590,9 +513,9 @@ class to have at least 3 characters. class Author { - private $firstName; + private string $firstName; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', new Assert\NotBlank()); $metadata->addPropertyConstraint( @@ -624,25 +547,6 @@ this method must return ``true``: .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/Author.php - namespace App\Entity; - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\IsTrue(message="The password cannot match your first name") - */ - public function isPasswordSafe() - { - // ... return true or false - } - } - .. code-block:: php-attributes // src/Entity/Author.php @@ -654,7 +558,7 @@ this method must return ``true``: class Author { #[Assert\IsTrue(message: 'The password cannot match your first name')] - public function isPasswordSafe() + public function isPasswordSafe(): bool { // ... return true or false } @@ -697,7 +601,7 @@ this method must return ``true``: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue([ 'message' => 'The password cannot match your first name', @@ -707,7 +611,7 @@ this method must return ``true``: Now, create the ``isPasswordSafe()`` method and include the logic you need:: - public function isPasswordSafe() + public function isPasswordSafe(): bool { return $this->firstName !== $this->password; } @@ -747,10 +651,6 @@ and then select the appropriate group when validating each object. Debugging the Constraints ------------------------- -.. versionadded:: 5.2 - - The ``debug:validator`` command was introduced in Symfony 5.2. - Use the ``debug:validator`` command to list the validation constraints of a given class: diff --git a/validation/custom_constraint.rst b/validation/custom_constraint.rst index 549de6e3234..9f0ca4ca07b 100644 --- a/validation/custom_constraint.rst +++ b/validation/custom_constraint.rst @@ -13,23 +13,6 @@ First you need to create a Constraint class and extend :class:`Symfony\\Componen .. configuration-block:: - .. code-block:: php-annotations - - // src/Validator/ContainsAlphanumeric.php - namespace App\Validator; - - use Symfony\Component\Validator\Constraint; - - /** - * @Annotation - */ - class ContainsAlphanumeric extends Constraint - { - public string $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.'; - // If the constraint has configuration options, define them as public properties - public string $mode = 'strict'; - } - .. code-block:: php-attributes // src/Validator/ContainsAlphanumeric.php @@ -40,8 +23,8 @@ First you need to create a Constraint class and extend :class:`Symfony\\Componen #[\Attribute] class ContainsAlphanumeric extends Constraint { - public $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.'; - public $mode = 'strict'; + public string $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.'; + public string $mode = 'strict'; // all configurable options must be passed to the constructor public function __construct(?string $mode = null, ?string $message = null, ?array $groups = null, $payload = null) @@ -53,14 +36,31 @@ First you need to create a Constraint class and extend :class:`Symfony\\Componen } } -Add ``@Annotation`` or ``#[\Attribute]`` to the constraint class if you want to -use it as an annotation/attribute in other classes. +Add ``#[\Attribute]`` to the constraint class if you want to +use it as an attribute in other classes. -.. versionadded:: 5.2 +You can use ``#[HasNamedArguments]`` to make some constraint options required:: - The ability to use PHP attributes to configure constraints was introduced in - Symfony 5.2. Prior to this, Doctrine Annotations were the only way to - annotate constraints. + // src/Validator/ContainsAlphanumeric.php + namespace App\Validator; + + use Symfony\Component\Validator\Attribute\HasNamedArguments; + use Symfony\Component\Validator\Constraint; + + #[\Attribute] + class ContainsAlphanumeric extends Constraint + { + public string $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.'; + + #[HasNamedArguments] + public function __construct( + public string $mode, + ?array $groups = null, + mixed $payload = null, + ) { + parent::__construct([], $groups, $payload); + } + } Creating the Validator itself ----------------------------- @@ -71,7 +71,7 @@ class is specified by the constraint's ``validatedBy()`` method, which has this default logic:: // in the base Symfony\Component\Validator\Constraint class - public function validatedBy() + public function validatedBy(): string { return static::class.'Validator'; } @@ -92,7 +92,7 @@ The validator class only has one required method ``validate()``:: class ContainsAlphanumericValidator extends ConstraintValidator { - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof ContainsAlphanumeric) { throw new UnexpectedTypeException($constraint, ContainsAlphanumeric::class); @@ -142,27 +142,6 @@ You can use custom validators like the ones provided by Symfony itself: .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/User.php - namespace App\Entity; - - use App\Validator as AcmeAssert; - use Symfony\Component\Validator\Constraints as Assert; - - class User - { - // ... - - /** - * @Assert\NotBlank - * @AcmeAssert\ContainsAlphanumeric(mode="loose") - */ - protected string $name = ''; - - // ... - } - .. code-block:: php-attributes // src/Entity/AcmeEntity.php @@ -177,7 +156,7 @@ You can use custom validators like the ones provided by Symfony itself: #[Assert\NotBlank] #[AcmeAssert\ContainsAlphanumeric(mode: 'loose')] - protected $name; + protected string $name; // ... } @@ -248,106 +227,50 @@ Constraint Validators with Custom Options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you want to add some configuration options to your custom constraint, first -define those options as public properties on the constraint class: +define those options as public properties on the constraint class:: -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Validator/Foo.php - namespace App\Validator; - - use Symfony\Component\Validator\Constraint; - - /** - * @Annotation - */ - class Foo extends Constraint - { - public $mandatoryFooOption; - public $message = 'This value is invalid'; - public $optionalBarOption = false; - - public function __construct( - $mandatoryFooOption, - ?string $message = null, - ?bool $optionalBarOption = null, - ?array $groups = null, - $payload = null, - array $options = [] - ) { - if (\is_array($mandatoryFooOption)) { - $options = array_merge($mandatoryFooOption, $options); - } elseif (null !== $mandatoryFooOption) { - $options['value'] = $mandatoryFooOption; - } - - parent::__construct($options, $groups, $payload); - - $this->message = $message ?? $this->message; - $this->optionalBarOption = $optionalBarOption ?? $this->optionalBarOption; - } + // src/Validator/Foo.php + namespace App\Validator; - public function getDefaultOption() - { - // If no associative array is passed to the constructor this - // property is set instead. + use Symfony\Component\Validator\Constraint; - return 'mandatoryFooOption'; + #[\Attribute] + class Foo extends Constraint + { + public $mandatoryFooOption; + public $message = 'This value is invalid'; + public $optionalBarOption = false; + + public function __construct( + $mandatoryFooOption, + ?string $message = null, + ?bool $optionalBarOption = null, + ?array $groups = null, + $payload = null, + array $options = [] + ) { + if (\is_array($mandatoryFooOption)) { + $options = array_merge($mandatoryFooOption, $options); + } elseif (null !== $mandatoryFooOption) { + $options['value'] = $mandatoryFooOption; } - public function getRequiredOptions() - { - // return names of options which must be set. + parent::__construct($options, $groups, $payload); - return ['mandatoryFooOption']; - } + $this->message = $message ?? $this->message; + $this->optionalBarOption = $optionalBarOption ?? $this->optionalBarOption; } - .. code-block:: php-attributes - - // src/Validator/Foo.php - namespace App\Validator; - - use Symfony\Component\Validator\Constraint; - - #[\Attribute] - class Foo extends Constraint + public function getDefaultOption(): string { - public $mandatoryFooOption; - public $message = 'This value is invalid'; - public $optionalBarOption = false; - - public function __construct( - $mandatoryFooOption, - ?string $message = null, - ?bool $optionalBarOption = null, - ?array $groups = null, - $payload = null, - array $options = [] - ) { - if (\is_array($mandatoryFooOption)) { - $options = array_merge($mandatoryFooOption, $options); - } elseif (null !== $mandatoryFooOption) { - $options['value'] = $mandatoryFooOption; - } - - parent::__construct($options, $groups, $payload); - - $this->message = $message ?? $this->message; - $this->optionalBarOption = $optionalBarOption ?? $this->optionalBarOption; - } - - public function getDefaultOption() - { - return 'mandatoryFooOption'; - } + return 'mandatoryFooOption'; + } - public function getRequiredOptions() - { - return ['mandatoryFooOption']; - } + public function getRequiredOptions(): array + { + return ['mandatoryFooOption']; } + } Then, inside the validator class you can access these options directly via the constraint class passes to the ``validate()`` method:: @@ -370,30 +293,6 @@ the custom options like you pass any other option in built-in constraints: .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/AcmeEntity.php - namespace App\Entity; - - use App\Validator as AcmeAssert; - use Symfony\Component\Validator\Constraints as Assert; - - class AcmeEntity - { - // ... - - /** - * @Assert\NotBlank - * @AcmeAssert\Foo( - * mandatoryFooOption="bar", - * optionalBarOption=true - * ) - */ - protected $name; - - // ... - } - .. code-block:: php-attributes // src/Entity/AcmeEntity.php @@ -475,10 +374,6 @@ Create a Reusable Set of Constraints In case you need to consistently apply a common set of constraints across your application, you can extend the :doc:`Compound constraint </reference/constraints/Compound>`. -.. versionadded:: 5.1 - - The ``Compound`` constraint was introduced in Symfony 5.1. - Class Constraint Validator ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -494,9 +389,7 @@ email. First, create a constraint and override the ``getTargets()`` method:: use Symfony\Component\Validator\Constraint; - /** - * @Annotation - */ + #[\Attribute] class ConfirmedPaymentReceipt extends Constraint { public string $userDoesNotMatchMessage = 'User\'s e-mail address does not match that of the receipt'; @@ -554,21 +447,6 @@ A class constraint validator must be applied to the class itself: .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/PaymentReceipt.php - namespace App\Entity; - - use App\Validator\ConfirmedPaymentReceipt; - - /** - * @ConfirmedPaymentReceipt - */ - class PaymentReceipt - { - // ... - } - .. code-block:: php-attributes // src/Entity/AcmeEntity.php @@ -632,16 +510,17 @@ class to simplify writing unit tests for your custom constraints:: use App\Validator\ContainsAlphanumeric; use App\Validator\ContainsAlphanumericValidator; + use Symfony\Component\Validator\ConstraintValidatorInterface; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; class ContainsAlphanumericValidatorTest extends ConstraintValidatorTestCase { - protected function createValidator() + protected function createValidator(): ConstraintValidatorInterface { return new ContainsAlphanumericValidator(); } - public function testNullIsValid() + public function testNullIsValid(): void { $this->validator->validate(null, new ContainsAlphanumeric()); @@ -651,7 +530,7 @@ class to simplify writing unit tests for your custom constraints:: /** * @dataProvider provideInvalidConstraints */ - public function testTrueIsInvalid(ContainsAlphanumeric $constraint) + public function testTrueIsInvalid(ContainsAlphanumeric $constraint): void { $this->validator->validate('...', $constraint); diff --git a/validation/groups.rst b/validation/groups.rst index 8be6e8f81b6..3842c781969 100644 --- a/validation/groups.rst +++ b/validation/groups.rst @@ -12,33 +12,6 @@ user registers and when a user updates their contact information later: .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/User.php - namespace App\Entity; - - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Validator\Constraints as Assert; - - class User implements UserInterface - { - /** - * @Assert\Email(groups={"registration"}) - */ - private $email; - - /** - * @Assert\NotBlank(groups={"registration"}) - * @Assert\Length(min=7, groups={"registration"}) - */ - private $password; - - /** - * @Assert\Length(min=2) - */ - private $city; - } - .. code-block:: php-attributes // src/Entity/User.php @@ -50,14 +23,14 @@ user registers and when a user updates their contact information later: class User implements UserInterface { #[Assert\Email(groups: ['registration'])] - private $email; + private string $email; #[Assert\NotBlank(groups: ['registration'])] #[Assert\Length(min: 7, groups: ['registration'])] - private $password; + private string $password; #[Assert\Length(min: 2)] - private $city; + private string $city; } .. code-block:: yaml @@ -126,7 +99,7 @@ user registers and when a user updates their contact information later: class User { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('email', new Assert\Email([ 'groups' => ['registration'], diff --git a/validation/raw_values.rst b/validation/raw_values.rst index b863d9ee3ed..9c900ff2b36 100644 --- a/validation/raw_values.rst +++ b/validation/raw_values.rst @@ -10,7 +10,7 @@ address. From inside a controller, it looks like this:: use Symfony\Component\Validator\Validator\ValidatorInterface; // ... - public function addEmail($email, ValidatorInterface $validator) + public function addEmail(string $email, ValidatorInterface $validator): void { $emailConstraint = new Assert\Email(); // all constraint "options" can be set this way diff --git a/validation/sequence_provider.rst b/validation/sequence_provider.rst index f0fe22ce4df..55ff96acda2 100644 --- a/validation/sequence_provider.rst +++ b/validation/sequence_provider.rst @@ -11,38 +11,6 @@ username and the password are different only if all other validation passes .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/User.php - namespace App\Entity; - - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Validator\Constraints as Assert; - - /** - * @Assert\GroupSequence({"User", "Strict"}) - */ - class User implements UserInterface - { - /** - * @Assert\NotBlank - */ - private $username; - - /** - * @Assert\NotBlank - */ - private $password; - - /** - * @Assert\IsTrue(message="The password cannot match your username", groups={"Strict"}) - */ - public function isPasswordSafe() - { - return ($this->username !== $this->password); - } - } - .. code-block:: php-attributes // src/Entity/User.php @@ -55,16 +23,16 @@ username and the password are different only if all other validation passes class User implements UserInterface { #[Assert\NotBlank] - private $username; + private string $username; #[Assert\NotBlank] - private $password; + private string $password; #[Assert\IsTrue( message: 'The password cannot match your username', groups: ['Strict'], )] - public function isPasswordSafe() + public function isPasswordSafe(): bool { return ($this->username !== $this->password); } @@ -131,7 +99,7 @@ username and the password are different only if all other validation passes class User { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('username', new Assert\NotBlank()); $metadata->addPropertyConstraint('password', new Assert\NotBlank()); @@ -183,7 +151,7 @@ You can also define a group sequence in the ``validation_groups`` form option:: class MyType extends AbstractType { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'validation_groups' => new GroupSequence(['First', 'Second']), @@ -202,31 +170,6 @@ entity and a new constraint group called ``Premium``: .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/User.php - namespace App\Entity; - - use Symfony\Component\Validator\Constraints as Assert; - - class User - { - /** - * @Assert\NotBlank - */ - private $name; - - /** - * @Assert\CardScheme( - * schemes={"VISA"}, - * groups={"Premium"}, - * ) - */ - private $creditCard; - - // ... - } - .. code-block:: php-attributes // src/Entity/User.php @@ -237,13 +180,13 @@ entity and a new constraint group called ``Premium``: class User { #[Assert\NotBlank] - private $name; + private string $name; #[Assert\CardScheme( schemes: [Assert\CardScheme::VISA], groups: ['Premium'], )] - private $creditCard; + private string $creditCard; // ... } @@ -298,12 +241,12 @@ entity and a new constraint group called ``Premium``: class User { - private $name; - private $creditCard; + private string $name; + private string $creditCard; // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('name', new Assert\NotBlank()); $metadata->addPropertyConstraint('creditCard', new Assert\CardScheme([ @@ -329,7 +272,7 @@ method, which should return an array of groups to use:: { // ... - public function getGroupSequence() + public function getGroupSequence(): array|GroupSequence { // when returning a simple array, if there's a violation in any group // the rest of the groups are not validated. E.g. if 'User' fails, @@ -348,30 +291,97 @@ provides a sequence of groups to be validated: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/User.php namespace App\Entity; // ... - /** - * @Assert\GroupSequenceProvider - */ + #[Assert\GroupSequenceProvider] class User implements GroupSequenceProviderInterface { // ... } - .. code-block:: php-attributes + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\User: + group_sequence_provider: true + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping + https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\User"> + <group-sequence-provider/> + <!-- ... --> + </class> + </constraint-mapping> + + .. code-block:: php // src/Entity/User.php namespace App\Entity; // ... + use Symfony\Component\Validator\Mapping\ClassMetadata; - #[Assert\GroupSequenceProvider] class User implements GroupSequenceProviderInterface + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->setGroupSequenceProvider(true); + // ... + } + } + +Advanced Validation Group Provider +---------------------------------- + +In the previous section, you learned how to change the sequence of groups +dynamically based on the state of your entity. However, in more advanced cases +you might need to use some external configuration or service to define that +sequence of groups. + +Managing the entity initialization and manually setting its dependencies can +be cumbersome, and the implementation might not align with the entity +responsibilities. To solve this, you can configure the implementation of the +:class:`Symfony\\Component\\Validator\\GroupProviderInterface` outside of the +entity, and even register the group provider as a service. + +Here's how you can achieve this: + + 1) **Define a Separate Group Provider Class:** create a class that implements + the :class:`Symfony\\Component\\Validator\\GroupProviderInterface` + and handles the dynamic group sequence logic; + 2) **Configure the User with the Provider:** use the ``provider`` option within + the :class:`Symfony\\Component\\Validator\\Constraints\\GroupSequenceProvider` + attribute to link the entity with the provider class; + 3) **Autowiring or Manual Tagging:** if :doc:` autowiring </service_container/autowiring>` + is enabled, your custom provider will be automatically linked. Otherwise, you must + :doc:`tag your service </service_container/tags>` manually with the ``validator.group_provider`` tag. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity; + + // ... + use App\Validator\UserGroupProvider; + + #[Assert\GroupSequenceProvider(provider: UserGroupProvider::class)] + class User { // ... } @@ -380,7 +390,7 @@ provides a sequence of groups to be validated: # config/validator/validation.yaml App\Entity\User: - group_sequence_provider: true + group_sequence_provider: App\Validator\UserGroupProvider .. code-block:: xml @@ -392,7 +402,9 @@ provides a sequence of groups to be validated: https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> <class name="App\Entity\User"> - <group-sequence-provider/> + <group-sequence-provider> + <value>App\Validator\UserGroupProvider</value> + </group-sequence-provider> <!-- ... --> </class> </constraint-mapping> @@ -403,26 +415,27 @@ provides a sequence of groups to be validated: namespace App\Entity; // ... + use App\Validator\UserGroupProvider; use Symfony\Component\Validator\Mapping\ClassMetadata; - class User implements GroupSequenceProviderInterface + class User { // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { + $metadata->setGroupProvider(UserGroupProvider::class); $metadata->setGroupSequenceProvider(true); // ... } } +With this approach, you can maintain a clean separation between the entity +structure and the group sequence logic, allowing for more advanced use cases. + How to Sequentially Apply Constraints on a Single Property ---------------------------------------------------------- Sometimes, you may want to apply constraints sequentially on a single property. The :doc:`Sequentially constraint </reference/constraints/Sequentially>` can solve this for you in a more straightforward way than using a ``GroupSequence``. - -.. versionadded:: 5.1 - - The ``Sequentially`` constraint was introduced in Symfony 5.1. diff --git a/validation/severity.rst b/validation/severity.rst index 9692bc942cd..632a99519d9 100644 --- a/validation/severity.rst +++ b/validation/severity.rst @@ -21,31 +21,6 @@ Use the ``payload`` option to configure the error level for each constraint: .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/User.php - namespace App\Entity; - - use Symfony\Component\Validator\Constraints as Assert; - - class User - { - /** - * @Assert\NotBlank(payload={"severity"="error"}) - */ - protected $username; - - /** - * @Assert\NotBlank(payload={"severity"="error"}) - */ - protected $password; - - /** - * @Assert\Iban(payload={"severity"="warning"}) - */ - protected $bankAccountNumber; - } - .. code-block:: php-attributes // src/Entity/User.php @@ -56,13 +31,13 @@ Use the ``payload`` option to configure the error level for each constraint: class User { #[Assert\NotBlank(payload: ['severity' => 'error'])] - protected $username; + protected string $username; #[Assert\NotBlank(payload: ['severity' => 'error'])] - protected $password; + protected string $password; #[Assert\Iban(payload: ['severity' => 'warning'])] - protected $bankAccountNumber; + protected string $bankAccountNumber; } .. code-block:: yaml @@ -126,7 +101,9 @@ Use the ``payload`` option to configure the error level for each constraint: class User { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('username', new Assert\NotBlank([ 'payload' => ['severity' => 'error'], diff --git a/validation/translations.rst b/validation/translations.rst index 721273562c1..9a4ece17736 100644 --- a/validation/translations.rst +++ b/validation/translations.rst @@ -20,7 +20,7 @@ your application:: class Author { - public $name; + public string $name; } Add constraints through any of the supported methods. Set the message option @@ -29,21 +29,6 @@ property is not empty, add the following: .. configuration-block:: - .. code-block:: php-annotations - - // src/Entity/Author.php - namespace App\Entity; - - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\NotBlank(message="author.name.not_blank") - */ - public $name; - } - .. code-block:: php-attributes // src/Entity/Author.php @@ -54,7 +39,7 @@ property is not empty, add the following: class Author { #[Assert\NotBlank(message: 'author.name.not_blank')] - public $name; + public string $name; } .. code-block:: yaml @@ -94,9 +79,9 @@ property is not empty, add the following: class Author { - public $name; + public string $name; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('name', new NotBlank([ 'message' => 'author.name.not_blank', @@ -138,6 +123,37 @@ Now, create a ``validators`` catalog file in the ``translations/`` directory: You may need to clear your cache (even in the dev environment) after creating this file for the first time. +.. tip:: + + Symfony will also create translation files for the built-in validation messages. + You can optionally set the :ref:`enabled_locales <reference-translator-enabled-locales>` + option to restrict the available locales in your application. This will improve + performance a bit because Symfony will only generate the translation files + for those locales instead of all of them. + +You can also use :class:`Symfony\\Component\\Translation\\TranslatableMessage` to build your violation message:: + + use Symfony\Component\Translation\TranslatableMessage; + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Context\ExecutionContextInterface; + + #[Assert\Callback] + public function validate(ExecutionContextInterface $context, mixed $payload): void + { + // somehow you have an array of "fake names" + $fakeNames = [/* ... */]; + + // check if the name is actually a fake name + if (in_array($this->getFirstName(), $fakeNames, true)) { + $context->buildViolation(new TranslatableMessage('author.name.fake', [], 'validators')) + ->atPath('firstName') + ->addViolation() + ; + } + } + +You can learn more about translatable messages in :ref:`the dedicated section <translatable-objects>`. + Custom Translation Domain ------------------------- diff --git a/web_link.rst b/web_link.rst index fb81376cba3..7a09e6273d6 100644 --- a/web_link.rst +++ b/web_link.rst @@ -59,13 +59,23 @@ correct prioritization and the content security policy: <head> <!-- ... --> - <link rel="preload" href="{{ preload('/app.css', { as: 'style' }) }}"> + {# note that you must add two <link> tags per asset: + one to link to it and the other one to tell the browser to preload it #} + <link rel="preload" href="{{ preload('/app.css', {as: 'style'}) }}" as="style"> + <link rel="stylesheet" href="/app.css"> </head> If you reload the page, the perceived performance will improve because the server responded with both the HTML page and the CSS file when the browser only requested the HTML page. +.. tip:: + + When using the :doc:`AssetMapper component </frontend/asset_mapper>` to link + to assets (e.g. ``importmap('app')``), there's no need to add the ``<link rel="preload">`` + tag. The ``importmap()`` Twig function automatically adds the ``Link`` HTTP + header for you when the WebLink component is available. + .. note:: You can preload an asset by wrapping it with the ``preload()`` function: @@ -74,7 +84,8 @@ requested the HTML page. <head> <!-- ... --> - <link rel="preload" href="{{ preload(asset('build/app.css')) }}"> + <link rel="preload" href="{{ preload(asset('build/app.css')) }}" as="style"> + <!-- ... --> </head> Additionally, according to `the Priority Hints specification`_, you can signal @@ -84,7 +95,8 @@ the priority of the resource to download using the ``importance`` attribute: <head> <!-- ... --> - <link rel="preload" href="{{ preload('/app.css', { as: 'style', importance: 'low' }) }}"> + <link rel="preload" href="{{ preload('/app.css', {as: 'style', importance: 'low'}) }}" as="style"> + <!-- ... --> </head> How does it work? @@ -108,7 +120,8 @@ issuing an early separate HTTP request, use the ``nopush`` option: <head> <!-- ... --> - <link rel="preload" href="{{ preload('/app.css', { as: 'style', nopush: true }) }}"> + <link rel="preload" href="{{ preload('/app.css', {as: 'style', nopush: true}) }}" as="style"> + <!-- ... --> </head> Resource Hints @@ -142,7 +155,8 @@ any link implementing the `PSR-13`_ standard. For instance, any <head> <!-- ... --> <link rel="alternate" href="{{ link('/index.jsonld', 'alternate') }}"> - <link rel="preload" href="{{ preload('/app.css', { as: 'style', nopush: true }) }}"> + <link rel="preload" href="{{ preload('/app.css', {as: 'style', nopush: true}) }}" as="style"> + <!-- ... --> </head> The previous snippet will result in this HTTP header being sent to the client: @@ -155,15 +169,16 @@ You can also add links to the HTTP response directly from controllers and servic use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\WebLink\GenericLinkProvider; use Symfony\Component\WebLink\Link; class BlogController extends AbstractController { - public function index(Request $request) + public function index(Request $request): Response { // using the addLink() shortcut provided by AbstractController - $this->addLink($request, new Link('preload', '/app.css')); + $this->addLink($request, (new Link('preload', '/app.css'))->withAttribute('as', 'style')); // alternative if you don't want to use the addLink() shortcut $linkProvider = $request->attributes->get('_links', new GenericLinkProvider()); diff --git a/webhook.rst b/webhook.rst new file mode 100644 index 00000000000..38f3a4b1004 --- /dev/null +++ b/webhook.rst @@ -0,0 +1,199 @@ +Webhook +======= + +The Webhook component is used to respond to remote webhooks to trigger actions +in your application. This document focuses on using webhooks to listen to remote +events in other Symfony components. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/webhook + +Usage in Combination with the Mailer Component +---------------------------------------------- + +When using a third-party mailer provider, you can use the Webhook component to +receive webhook calls from this provider. + +Currently, the following third-party mailer providers support webhooks: + +============== ============================================ +Mailer Service Parser service name +============== ============================================ +Brevo ``mailer.webhook.request_parser.brevo`` +MailerSend ``mailer.webhook.request_parser.mailersend`` +Mailgun ``mailer.webhook.request_parser.mailgun`` +Mailjet ``mailer.webhook.request_parser.mailjet`` +Postmark ``mailer.webhook.request_parser.postmark`` +Resend ``mailer.webhook.request_parser.resend`` +Sendgrid ``mailer.webhook.request_parser.sendgrid`` +============== ============================================ + +.. versionadded:: 7.1 + + The support for ``Resend`` and ``MailerSend`` were introduced in Symfony 7.1. + +.. note:: + + Install the third-party mailer provider you want to use as described in the + documentation of the :ref:`Mailer component <mailer_3rd_party_transport>`. + Mailgun is used as the provider in this document as an example. + +To connect the provider to your application, you need to configure the Webhook +component routing: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + webhook: + routing: + mailer_mailgun: + service: 'mailer.webhook.request_parser.mailgun' + secret: '%env(MAILER_MAILGUN_SECRET)%' + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + <framework:config> + <framework:webhook enabled="true"> + <framework:routing type="mailer_mailgun"> + <framework:service>mailer.webhook.request_parser.mailgun</framework:service> + <framework:secret>%env(MAILER_MAILGUN_SECRET)%</framework:secret> + </framework:routing> + </framework:webhook> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use App\Webhook\MailerWebhookParser; + use Symfony\Config\FrameworkConfig; + return static function (FrameworkConfig $frameworkConfig): void { + $webhookConfig = $frameworkConfig->webhook(); + $webhookConfig + ->routing('mailer_mailgun') + ->service('mailer.webhook.request_parser.mailgun') + ->secret('%env(MAILER_MAILGUN_SECRET)%') + ; + }; + +In this example, we are using ``mailer_mailgun`` as the webhook routing name. +The routing name must be unique as this is what connects the provider with your +webhook consumer code. + +The webhook routing name is part of the URL you need to configure at the +third-party mailer provider. The URL is the concatenation of your domain name +and the routing name you chose in the configuration (like +``https://example.com/webhook/mailer_mailgun``. + +For Mailgun, you will get a secret for the webhook. Store this secret as +MAILER_MAILGUN_SECRET (in the :doc:`secrets management system +</configuration/secrets>` or in a ``.env`` file). + +When done, add a :class:`Symfony\\Component\\RemoteEvent\\RemoteEvent` consumer +to react to incoming webhooks (the webhook routing name is what connects your +class to the provider). + +For mailer webhooks, react to the +:class:`Symfony\\Component\\RemoteEvent\\Event\\Mailer\\MailerDeliveryEvent` or +:class:`Symfony\\Component\\RemoteEvent\\Event\\Mailer\\MailerEngagementEvent` +events:: + + use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer; + use Symfony\Component\RemoteEvent\Consumer\ConsumerInterface; + use Symfony\Component\RemoteEvent\Event\Mailer\MailerDeliveryEvent; + use Symfony\Component\RemoteEvent\Event\Mailer\MailerEngagementEvent; + use Symfony\Component\RemoteEvent\RemoteEvent; + + #[AsRemoteEventConsumer('mailer_mailgun')] + class WebhookListener implements ConsumerInterface + { + public function consume(RemoteEvent $event): void + { + if ($event instanceof MailerDeliveryEvent) { + $this->handleMailDelivery($event); + } elseif ($event instanceof MailerEngagementEvent) { + $this->handleMailEngagement($event); + } else { + // This is not an email event + return; + } + } + + private function handleMailDelivery(MailerDeliveryEvent $event): void + { + // Handle the mail delivery event + } + + private function handleMailEngagement(MailerEngagementEvent $event): void + { + // Handle the mail engagement event + } + } + +Usage in Combination with the Notifier Component +------------------------------------------------ + +The usage of the Webhook component when using a third-party transport in +the Notifier is very similar to the usage with the Mailer. + +Currently, the following third-party SMS transports support webhooks: + +============ ========================================== +SMS service Parser service name +============ ========================================== +Twilio ``notifier.webhook.request_parser.twilio`` +Vonage ``notifier.webhook.request_parser.vonage`` +============ ========================================== + +For SMS webhooks, react to the +:class:`Symfony\\Component\\RemoteEvent\\Event\\Sms\\SmsEvent` event:: + + use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer; + use Symfony\Component\RemoteEvent\Consumer\ConsumerInterface; + use Symfony\Component\RemoteEvent\Event\Sms\SmsEvent; + use Symfony\Component\RemoteEvent\RemoteEvent; + + #[AsRemoteEventConsumer('notifier_twilio')] + class WebhookListener implements ConsumerInterface + { + public function consume(RemoteEvent $event): void + { + if ($event instanceof SmsEvent) { + $this->handleSmsEvent($event); + } else { + // This is not an SMS event + return; + } + } + + private function handleSmsEvent(SmsEvent $event): void + { + // Handle the SMS event + } + } + +Creating a Custom Webhook +------------------------- + +.. tip:: + + Starting in `MakerBundle`_ ``v1.58.0``, you can run ``php bin/console make:webhook`` + to generate the request parser and consumer files needed to create your own + Webhook. + +.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html diff --git a/workflow.rst b/workflow.rst index f3f04b3feea..52997531409 100644 --- a/workflow.rst +++ b/workflow.rst @@ -60,7 +60,7 @@ follows: supports: - App\Entity\BlogPost initial_marking: draft - places: + places: # defining places manually is optional - draft - reviewed - rejected @@ -97,10 +97,13 @@ follows: </framework:marking-store> <framework:support>App\Entity\BlogPost</framework:support> <framework:initial-marking>draft</framework:initial-marking> + + <!-- defining places manually is optional --> <framework:place>draft</framework:place> <framework:place>reviewed</framework:place> <framework:place>rejected</framework:place> <framework:place>published</framework:place> + <framework:transition name="to_review"> <framework:from>draft</framework:from> <framework:to>reviewed</framework:to> @@ -123,7 +126,7 @@ follows: use App\Entity\BlogPost; use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $blogPublishing = $framework->workflows()->workflows('blog_publishing'); $blogPublishing ->type('workflow') // or 'state_machine' @@ -135,6 +138,7 @@ follows: ->type('method') ->property('currentPlace'); + // defining places manually is optional $blogPublishing->place()->name('draft'); $blogPublishing->place()->name('reviewed'); $blogPublishing->place()->name('rejected'); @@ -168,6 +172,17 @@ follows: ``'draft'`` or ``!php/const App\Entity\BlogPost::TRANSITION_TO_REVIEW`` instead of ``'to_review'``. +.. tip:: + + You can omit the ``places`` option if your transitions define all the places + that are used in the workflow. Symfony will automatically extract the places + from the transitions. + + .. versionadded:: 7.1 + + The support for omitting the ``places`` option was introduced in + Symfony 7.1. + The configured property will be used via its implemented getter/setter methods by the marking store:: // src/Entity/BlogPost.php @@ -176,17 +191,17 @@ The configured property will be used via its implemented getter/setter methods b class BlogPost { // the configured marking store property must be declared - private $currentPlace; - private $title; - private $content; + private string $currentPlace; + private string $title; + private string $content; // getter/setter methods must exist for property access by the marking store - public function getCurrentPlace() + public function getCurrentPlace(): string { return $this->currentPlace; } - public function setCurrentPlace($currentPlace, $context = []) + public function setCurrentPlace(string $currentPlace, array $context = []): void { $this->currentPlace = $currentPlace; } @@ -195,6 +210,37 @@ The configured property will be used via its implemented getter/setter methods b // this is configured in the workflow with the 'initial_marking' option } +It is also possible to use public properties for the marking store. The above +class would become the following:: + + // src/Entity/BlogPost.php + namespace App\Entity; + + class BlogPost + { + // the configured marking store property must be declared + public string $currentPlace; + public string $title; + public string $content; + } + +When using public properties, context is not supported. In order to support it, +you must declare a setter to write your property:: + + // src/Entity/BlogPost.php + namespace App\Entity; + + class BlogPost + { + public string $currentPlace; + // ... + + public function setCurrentPlace(string $currentPlace, array $context = []): void + { + // assign the property and do something with the context + } + } + .. note:: The marking store type could be "multiple_state" or "single_state". A single @@ -296,15 +342,13 @@ machine type, use ``camelCased workflow name + StateMachine``:: class MyClass { - private $blogPublishingWorkflow; - - // Symfony will inject the 'blog_publishing' workflow configured before - public function __construct(WorkflowInterface $blogPublishingWorkflow) - { - $this->blogPublishingWorkflow = $blogPublishingWorkflow; + public function __construct( + // Symfony will inject the 'blog_publishing' workflow configured before + private WorkflowInterface $blogPublishingWorkflow, + ) { } - public function toReview(BlogPost $post) + public function toReview(BlogPost $post): void { // Update the currentState on the post try { @@ -316,11 +360,63 @@ machine type, use ``camelCased workflow name + StateMachine``:: } } +To get the enabled transition of a Workflow, you can use +:method:`Symfony\\Component\\Workflow\\WorkflowInterface::getEnabledTransition` +method. + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\Workflow\\WorkflowInterface::getEnabledTransition` + method was introduced in Symfony 7.1. + +Workflows can also be injected thanks to their name and the +:class:`Symfony\\Component\\DependencyInjection\\Attribute\\Target` +attribute:: + + use App\Entity\BlogPost; + use Symfony\Component\DependencyInjection\Attribute\Target; + use Symfony\Component\Workflow\WorkflowInterface; + + class MyClass + { + public function __construct( + #[Target('blog_publishing')] + private WorkflowInterface $workflow + ) { + } + + // ... + } + +This allows you to decorrelate the argument name of any implementation +name. + +.. tip:: + + If you want to retrieve all workflows, for documentation purposes for example, + you can :doc:`inject all services </service_container/service_subscribers_locators>` + with the following tag: + + * ``workflow``: all workflows and all state machine; + * ``workflow.workflow``: all workflows; + * ``workflow.state_machine``: all state machines. + + Note that workflow metadata are attached to tags under the ``metadata`` key, + giving you more context and information about the workflow at disposal. + Learn more about :ref:`tag attributes <tags_additional-attributes>` and + :ref:`storing workflow metadata <workflow_storing-metadata>`. + + .. versionadded:: 7.1 + + The attached configuration to the tag was introduced in Symfony 7.1. + .. tip:: You can find the list of available workflow services with the ``php bin/console debug:autowiring workflow`` command. +.. _workflow_using-events: + Using Events ------------ @@ -415,22 +511,6 @@ order: $workflow->apply($subject, $transitionName, [Workflow::DISABLE_ANNOUNCE_EVENT => true]); - .. versionadded:: 5.1 - - The ``Workflow::DISABLE_ANNOUNCE_EVENT`` constant was introduced in Symfony 5.1. - -.. versionadded:: 5.2 - - In Symfony 5.2, the context is customizable for all events except for - ``workflow.guard`` events, which will not receive the custom ``$context``:: - - // $context must be an array - $context = ['context_key' => 'context_value']; - $workflow->apply($subject, $transitionName, $context); - - // in an event listener - $context = $event->getContext(); // returns ['context'] - .. note:: The leaving and entering events are triggered even for transitions that stay @@ -451,17 +531,16 @@ workflow leaves a place:: use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Workflow\Event\Event; + use Symfony\Component\Workflow\Event\LeaveEvent; class WorkflowLoggerSubscriber implements EventSubscriberInterface { - private $logger; - - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; + public function __construct( + private LoggerInterface $logger, + ) { } - public function onLeave(Event $event) + public function onLeave(Event $event): void { $this->logger->alert(sprintf( 'Blog post (id: "%s") performed transition "%s" from "%s" to "%s"', @@ -472,14 +551,27 @@ workflow leaves a place:: )); } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ - 'workflow.blog_publishing.leave' => 'onLeave', + LeaveEvent::getName('blog_publishing') => 'onLeave', + // if you prefer, you can write the event name manually like this: + // 'workflow.blog_publishing.leave' => 'onLeave', ]; } } +.. tip:: + + All built-in workflow events define the ``getName(?string $workflowName, ?string $transitionOrPlaceName)`` + method to build the full event name without having to deal with strings. + You can also use this method in your custom events via the + :class:`Symfony\\Component\\Workflow\\Event\\EventNameTrait`. + + .. versionadded:: 7.1 + + The ``getName()`` method was introduced in Symfony 7.1. + If some listeners update the context during a transition, you can retrieve it via the marking:: @@ -488,9 +580,35 @@ it via the marking:: // contains the new value $marking->getContext(); -.. versionadded:: 5.4 +It is also possible to listen to these events by declaring event listeners +with the following attributes: + +* :class:`Symfony\\Component\\Workflow\\Attribute\\AsAnnounceListener` +* :class:`Symfony\\Component\\Workflow\\Attribute\\AsCompletedListener` +* :class:`Symfony\\Component\\Workflow\\Attribute\\AsEnterListener` +* :class:`Symfony\\Component\\Workflow\\Attribute\\AsEnteredListener` +* :class:`Symfony\\Component\\Workflow\\Attribute\\AsGuardListener` +* :class:`Symfony\\Component\\Workflow\\Attribute\\AsLeaveListener` +* :class:`Symfony\\Component\\Workflow\\Attribute\\AsTransitionListener` + +These attributes do work like the +:class:`Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener` +attributes:: + + class ArticleWorkflowEventListener + { + #[AsTransitionListener(workflow: 'my-workflow', transition: 'published')] + public function onPublishedTransition(TransitionEvent $event): void + { + // ... + } + + // ... + } - The ability to get the new value from the marking was introduced in Symfony 5.4. +You may refer to the documentation about +:ref:`defining event listeners with PHP attributes <event-dispatcher_event-listener-attributes>` +for further use. .. _workflow-usage-guard-events: @@ -519,7 +637,7 @@ missing a title:: class BlogPostReviewSubscriber implements EventSubscriberInterface { - public function guardReview(GuardEvent $event) + public function guardReview(GuardEvent $event): void { /** @var BlogPost $post */ $post = $event->getSubject(); @@ -530,7 +648,7 @@ missing a title:: } } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ 'workflow.blog_publishing.guard.to_review' => ['guardReview'], @@ -538,19 +656,11 @@ missing a title:: } } -.. versionadded:: 5.1 - - The optional second argument of ``setBlocked()`` was introduced in Symfony 5.1. - .. _workflow-chosing-events-to-dispatch: Choosing which Events to Dispatch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.2 - - Ability to choose which events to dispatch was introduced in Symfony 5.2. - If you prefer to control which events are fired when performing each transition, use the ``events_to_dispatch`` configuration option. This option does not apply to :ref:`Guard events <workflow-usage-guard-events>`, which are always fired: @@ -600,7 +710,7 @@ to :ref:`Guard events <workflow-usage-guard-events>`, which are always fired: // config/packages/workflow.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { // ... $blogPublishing = $framework->workflows()->workflows('blog_publishing'); @@ -641,13 +751,7 @@ events specified in the workflow configuration. In the above example the ``workflow.leave`` event will not be fired, even if it has been specified as an event to be dispatched for all transitions in the workflow configuration. -.. versionadded:: 5.1 - - The ``Workflow::DISABLE_ANNOUNCE_EVENT`` constant was introduced in Symfony 5.1. - -.. versionadded:: 5.2 - - The constants for other events (as seen below) were introduced in Symfony 5.2. +These are all the available constants: * ``Workflow::DISABLE_LEAVE_EVENT`` * ``Workflow::DISABLE_TRANSITION_EVENT`` @@ -780,7 +884,7 @@ transition. The value of this option is any valid expression created with the // config/packages/workflow.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $blogPublishing = $framework->workflows()->workflows('blog_publishing'); // ... previous configuration @@ -825,7 +929,7 @@ place:: class BlogPostPublishSubscriber implements EventSubscriberInterface { - public function guardPublish(GuardEvent $event) + public function guardPublish(GuardEvent $event): void { $eventTransition = $event->getTransition(); $hourLimit = $event->getMetadata('hour_limit', $eventTransition); @@ -840,7 +944,7 @@ place:: $event->addTransitionBlocker(new TransitionBlocker($explanation , '0')); } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ 'workflow.blog_publishing.guard.publish' => ['guardPublish'], @@ -992,6 +1096,8 @@ The following example shows these functions in action: <span class="error">{{ blocker.message }}</span> {% endfor %} +.. _workflow_storing-metadata: + Storing Metadata ---------------- @@ -1074,7 +1180,7 @@ be only the title of the workflow or very complex objects: // config/packages/workflow.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $blogPublishing = $framework->workflows()->workflows('blog_publishing'); // ... previous configuration @@ -1117,7 +1223,7 @@ Then you can access this metadata in your controller as follows:: use Symfony\Component\Workflow\WorkflowInterface; // ... - public function myAction(WorkflowInterface $blogPublishingWorkflow, BlogPost $post) + public function myAction(WorkflowInterface $blogPublishingWorkflow, BlogPost $post): Response { $title = $blogPublishingWorkflow ->getMetadataStore() diff --git a/workflow/dumping-workflows.rst b/workflow/dumping-workflows.rst index d06c83edae5..8262fefd6c1 100644 --- a/workflow/dumping-workflows.rst +++ b/workflow/dumping-workflows.rst @@ -9,10 +9,6 @@ applications needed to generate the images: * `Mermaid CLI`_, provides the ``mmdc`` command; * `PlantUML`_, provides the ``plantuml.jar`` file (which requires Java). -.. versionadded:: 5.3 - - The ``mermaid`` dump format was introduced in Symfony 5.3. - If you are defining the workflow inside a Symfony application, run this command to dump it as an image: @@ -69,6 +65,21 @@ files and ``PlantUmlDumper`` to create the PlantUML files:: Styling ------- +You can use ``--with-metadata`` option in the ``workflow:dump`` command to include places, transitions and +workflow's metadata. + +The DOT image will look like this : + +.. image:: /_images/components/workflow/blogpost_metadata.png + +.. note:: + + The ``--with-metadata`` option only works for the DOT dumper for now. + +.. note:: + + The ``label`` metadata is not included in the dumped metadata, because it is used as a place's title. + You can use ``metadata`` with the following keys to style the workflow: * for places: @@ -253,7 +264,7 @@ Below is the configuration for the pull request state machine with styling added // config/packages/workflow.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { // ... $pullRequest = $framework->workflows()->workflows('pull_request'); diff --git a/workflow/workflow-and-state-machine.rst b/workflow/workflow-and-state-machine.rst index 14ab7d0320a..3a034b97357 100644 --- a/workflow/workflow-and-state-machine.rst +++ b/workflow/workflow-and-state-machine.rst @@ -196,7 +196,7 @@ Below is the configuration for the pull request state machine. // config/packages/workflow.php use Symfony\Config\FrameworkConfig; - return static function (FrameworkConfig $framework) { + return static function (FrameworkConfig $framework): void { $pullRequest = $framework->workflows()->workflows('pull_request'); $pullRequest @@ -252,28 +252,39 @@ Below is the configuration for the pull request state machine. ->to(['review']); }; +.. tip:: + + You can omit the ``places`` option if your transitions define all the places + that are used in the workflow. Symfony will automatically extract the places + from the transitions. + + .. versionadded:: 7.1 + + The support for omitting the ``places`` option was introduced in + Symfony 7.1. + Symfony automatically creates a service for each workflow (:class:`Symfony\\Component\\Workflow\\Workflow`) or state machine (:class:`Symfony\\Component\\Workflow\\StateMachine`) you -have defined in your configuration. This means that you can use ``workflow.pull_request`` -or ``state_machine.pull_request`` respectively in your service definitions -to access the proper service:: +have defined in your configuration. You can use the workflow inside a class by using +:doc:`service autowiring </service_container/autowiring>` and using +``camelCased workflow name + Workflow`` as parameter name. If it is a state +machine type, use ``camelCased workflow name + StateMachine``:: // ... use App\Entity\PullRequest; - use Symfony\Component\Workflow\StateMachine; + use Symfony\Component\Workflow\WorkflowInterface; class SomeService { - private $stateMachine; - - public function __construct(StateMachine $stateMachine) - { - $this->stateMachine = $stateMachine; + public function __construct( + // Symfony will inject the 'pull_request' state machine configured before + private WorkflowInterface $pullRequestStateMachine, + ) { } - public function someMethod(PullRequest $pullRequest) + public function someMethod(PullRequest $pullRequest): void { - $this->stateMachine->apply($pullRequest, 'wait_for_review', [ + $this->pullRequestStateMachine->apply($pullRequest, 'wait_for_review', [ 'log_comment' => 'My logging comment for the wait for review transition.', ]); // ...