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_CONSTANTApp\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 = htmlspecialchars(isset($name) ? $name : 'World', ENT_QUOTES, 'UTF-8') ?>
+ Hello = htmlspecialchars($name ?? 'World', ENT_QUOTES, 'UTF-8') ?>
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_rangesx-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*
- ``