diff --git a/.doctor-rst.yaml b/.doctor-rst.yaml
index 2ecd4fc7d2d..dfb85021586 100644
--- a/.doctor-rst.yaml
+++ b/.doctor-rst.yaml
@@ -8,6 +8,7 @@ rules:
correct_code_block_directive_based_on_the_content: ~
deprecated_directive_should_have_version: ~
ensure_bash_prompt_before_composer_command: ~
+ ensure_class_constant: ~
ensure_correct_format_for_phpfunction: ~
ensure_exactly_one_space_before_directive_type: ~
ensure_exactly_one_space_between_link_definition_and_link: ~
@@ -49,6 +50,7 @@ rules:
no_namespace_after_use_statements: ~
no_php_open_tag_in_code_block_php_directive: ~
no_space_before_self_xml_closing_tag: ~
+ no_typographic_quotes: ~
non_static_phpunit_assertions: ~
only_backslashes_in_namespace_in_php_code_block: ~
only_backslashes_in_use_statements_in_php_code_block: ~
@@ -72,7 +74,6 @@ rules:
versionadded_directive_should_have_version: ~
yaml_instead_of_yml_suffix: ~
- # master
versionadded_directive_major_version:
major_version: 7
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 497dfd9b430..42770d55fe3 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -72,7 +72,7 @@ jobs:
key: ${{ runner.os }}-doctor-rst-${{ steps.extract_base_branch.outputs.branch }}
- name: "Run DOCtor-RST"
- uses: docker://oskarstark/doctor-rst:1.67.0
+ uses: docker://oskarstark/doctor-rst:1.70.0
with:
args: --short --error-format=github --cache-file=/github/workspace/.cache/doctor-rst.cache
diff --git a/README.md b/README.md
index 5c063058c02..84f91fbbbbc 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
diff --git a/_build/redirection_map b/_build/redirection_map
index ee14c191025..c30723eac58 100644
--- a/_build/redirection_map
+++ b/_build/redirection_map
@@ -435,6 +435,7 @@
/setup/composer /setup
/security/security_checker /setup
/setup/built_in_web_server /setup/symfony_server
+/setup/symfony_server /setup/symfony_cli
/service_container/parameters /configuration
/routing/generate_url_javascript /routing
/routing/slash_in_parameter /routing
@@ -574,3 +575,7 @@
/doctrine/reverse_engineering /doctrine#doctrine-adding-mapping
/components/serializer /serializer
/serializer/custom_encoder /serializer/encoders#serializer-custom-encoder
+/components/string /string
+/form/button_based_validation /form/validation_groups
+/form/data_based_validation /form/validation_groups
+/form/validation_group_service_resolver /form/validation_groups
diff --git a/best_practices.rst b/best_practices.rst
index 2c393cae9c6..7ca5590036a 100644
--- a/best_practices.rst
+++ b/best_practices.rst
@@ -10,7 +10,7 @@ You can even ignore them completely and continue using your own best practices
and methodologies. Symfony is flexible enough to adapt to your needs.
This article assumes that you already have experience developing Symfony
-applications. If you don't, read first the :doc:`Getting Started `
+applications. If you don't, first read the :doc:`Getting Started `
section of the documentation.
.. tip::
@@ -95,7 +95,7 @@ Use Secrets for Sensitive Information
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When your application has sensitive configuration, like an API key, you should
-store those securely via :doc:`Symfony’s secrets management system `.
+store those securely via :doc:`Symfony's secrets management system `.
Use Parameters for Application Configuration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -118,7 +118,7 @@ Use Short and Prefixed Parameter Names
Consider using ``app.`` as the prefix of your :ref:`parameters `
to avoid collisions with Symfony and third-party bundles/libraries parameters.
-Then, use just one or two words to describe the purpose of the parameter:
+Then, use only one or two words to describe the purpose of the parameter:
.. code-block:: yaml
@@ -362,10 +362,6 @@ Unless you have two legitimately different authentication systems and users
(e.g. form login for the main site and a token system for your API only), it's
recommended to have only one firewall to keep things simple.
-Additionally, you should use the ``anonymous`` key under your firewall. If you
-require users to be logged in for different sections of your site, use the
-:doc:`access_control ` option.
-
Use the ``auto`` Password Hasher
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/bundles.rst b/bundles.rst
index 878bee3af4a..3e590a4e2aa 100644
--- a/bundles.rst
+++ b/bundles.rst
@@ -11,7 +11,7 @@ The Bundle System
A bundle is similar to a plugin in other software, but even better. The core
features of Symfony framework are implemented with bundles (FrameworkBundle,
-SecurityBundle, DebugBundle, etc.) They are also used to add new features in
+SecurityBundle, DebugBundle, etc.) Bundles are also used to add new features in
your application via `third-party bundles`_.
Bundles used in your applications must be enabled per
@@ -42,7 +42,7 @@ file::
Creating a Bundle
-----------------
-This section creates and enables a new bundle to show there are only a few steps required.
+This section creates and enables a new bundle to show that only a few steps are required.
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. AbcBlogBundle for some company named ``Abc``).
diff --git a/bundles/best_practices.rst b/bundles/best_practices.rst
index 37dc386b8e4..34bf24308ef 100644
--- a/bundles/best_practices.rst
+++ b/bundles/best_practices.rst
@@ -188,32 +188,31 @@ the ``tests/`` directory. Tests should follow the following principles:
.. note::
A test suite must not contain ``AllTests.php`` scripts, but must rely on the
- existence of a ``phpunit.xml.dist`` file.
+ existence of a ``phpunit.dist.xml`` file.
Continuous Integration
----------------------
Testing bundle code continuously, including all its commits and pull requests,
is a good practice called Continuous Integration. There are several services
-providing this feature for free for open source projects, like `GitHub Actions`_
-and `Travis CI`_.
+providing this feature for free for open source projects, like `GitHub Actions`_.
A bundle should at least test:
* The lower bound of their dependencies (by running ``composer update --prefer-lowest``);
* The supported PHP versions;
-* All supported major Symfony versions (e.g. both ``4.x`` and ``5.x`` if
+* All supported major Symfony versions (e.g. both ``6.4`` and ``7.x`` if
support is claimed for both).
-Thus, a bundle supporting PHP 7.3, 7.4 and 8.0, and Symfony 4.4 and 5.x should
+Thus, a bundle supporting PHP 7.4, 8.3 and 8.4, and Symfony 6.4 and 7.x should
have at least this test matrix:
=========== =============== ===================
PHP version Symfony version Composer flags
=========== =============== ===================
-7.3 ``4.*`` ``--prefer-lowest``
-7.4 ``5.*``
-8.0 ``5.*``
+7.4 ``6.4`` ``--prefer-lowest``
+8.3 ``7.*``
+8.4 ``7.*``
=========== =============== ===================
.. tip::
@@ -233,10 +232,10 @@ with Symfony Flex to install a specific Symfony version:
.. code-block:: bash
- # this requires Symfony 5.x for all Symfony packages
- export SYMFONY_REQUIRE=5.*
+ # this requires Symfony 7.x for all Symfony packages
+ export SYMFONY_REQUIRE=7.*
# alternatively you can run this command to update composer.json config
- # composer config extra.symfony.require "5.*"
+ # composer config extra.symfony.require "7.*"
# install Symfony Flex in the CI environment
composer global config --no-plugins allow-plugins.symfony/flex true
@@ -560,6 +559,7 @@ Learn more
* :doc:`/bundles/extension`
* :doc:`/bundles/configuration`
+* :doc:`/frontend/create_ux_bundle`
.. _`PSR-4`: https://www.php-fig.org/psr/psr-4/
.. _`Symfony Flex recipe`: https://github.com/symfony/recipes
@@ -568,4 +568,3 @@ Learn more
.. _`choose any license`: https://choosealicense.com/
.. _`valid license identifier`: https://spdx.org/licenses/
.. _`GitHub Actions`: https://docs.github.com/en/free-pro-team@latest/actions
-.. _`Travis CI`: https://docs.travis-ci.com/
diff --git a/cache.rst b/cache.rst
index 83bb5b4cedc..9379511fde8 100644
--- a/cache.rst
+++ b/cache.rst
@@ -32,12 +32,11 @@ You can read more about these at the :doc:`component documentation cache()
diff --git a/components/browser_kit.rst b/components/browser_kit.rst
index bcb8f7b3c8e..8cf0772298c 100644
--- a/components/browser_kit.rst
+++ b/components/browser_kit.rst
@@ -143,7 +143,7 @@ field values, etc.) before submitting it::
$crawler = $client->request('GET', 'https://github.com/login');
// find the form with the 'Log in' button and submit it
- // 'Log in' can be the text content, id, value or name of a or
+ // 'Log in' can be the text content, id or name of a or
$client->submitForm('Log in');
// the second optional argument lets you override the default form field values
diff --git a/components/cache.rst b/components/cache.rst
index 4873fb7abc7..44ba8b5c151 100644
--- a/components/cache.rst
+++ b/components/cache.rst
@@ -84,6 +84,69 @@ generate and return the value::
Use cache tags to delete more than one key at the time. Read more at
:doc:`/components/cache/cache_invalidation`.
+Creating Sub-Namespaces
+-----------------------
+
+.. versionadded:: 7.3
+
+ Cache sub-namespaces were introduced in Symfony 7.3.
+
+Sometimes you need to create context-dependent variations of data that should be
+cached. For example, the data used to render a dashboard page may be expensive
+to generate and unique per user, so you can't cache the same data for everyone.
+
+In such cases, Symfony allows you to create different cache contexts using
+namespaces. A cache namespace is an arbitrary string that identifies a set of
+related cache items. All cache adapters provided by the component implement the
+:class:`Symfony\\Contracts\\Cache\\NamespacedPoolInterface`, which provides the
+:method:`Symfony\\Contracts\\Cache\\NamespacedPoolInterface::withSubNamespace`
+method.
+
+This method allows you to namespace cached items by transparently prefixing their keys::
+
+ $userCache = $cache->withSubNamespace(sprintf('user-%d', $user->getId()));
+
+ $userCache->get('dashboard_data', function (ItemInterface $item): string {
+ $item->expiresAfter(3600);
+
+ return '...';
+ });
+
+In this example, the cache item uses the ``dashboard_data`` key, but it will be
+stored internally under a namespace based on the current user ID. This is handled
+automatically, so you **don't** need to manually prefix keys like ``user-27.dashboard_data``.
+
+There are no guidelines or restrictions on how to define cache namespaces.
+You can make them as granular or as generic as your application requires::
+
+ $localeCache = $cache->withSubNamespace($request->getLocale());
+
+ $flagCache = $cache->withSubNamespace(
+ $featureToggle->isEnabled('new_checkout') ? 'checkout-v2' : 'checkout-v1'
+ );
+
+ $channel = $request->attributes->get('_route')?->startsWith('api_') ? 'api' : 'web';
+ $channelCache = $cache->withSubNamespace($channel);
+
+.. tip::
+
+ You can combine cache namespaces with :ref:`cache tags `
+ for more advanced needs.
+
+There is no built-in way to invalidate caches by namespace. Instead, the recommended
+approach is to change the namespace itself. For this reason, it's common to include
+static or dynamic versioning data in the cache namespace::
+
+ // for simple applications, an incrementing static version number may be enough
+ $userCache = $cache->withSubNamespace(sprintf('v1-user-%d', $user->getId()));
+
+ // other applications may use dynamic versioning based on the date (e.g. monthly)
+ $userCache = $cache->withSubNamespace(sprintf('%s-user-%d', date('Ym'), $user->getId()));
+
+ // or even invalidate the cache when the user data changes
+ $checksum = hash('xxh128', $user->getUpdatedAt()->format(DATE_ATOM));
+ $userCache = $cache->withSubNamespace(sprintf('user-%d-%s', $user->getId(), $checksum));
+
.. _cache_stampede-prevention:
Stampede Prevention
diff --git a/components/cache/adapters/redis_adapter.rst b/components/cache/adapters/redis_adapter.rst
index 3362f4cc2db..ac32e1bbd39 100644
--- a/components/cache/adapters/redis_adapter.rst
+++ b/components/cache/adapters/redis_adapter.rst
@@ -8,7 +8,8 @@ Redis Cache Adapter
:ref:`Symfony Cache configuration `
article if you are using it in a Symfony application.
-This adapter stores the values in-memory using one (or more) `Redis server`_ instances.
+This adapter stores the values in-memory using one (or more) `Redis server`_
+or `Valkey`_ server instances.
Unlike the :doc:`APCu adapter `, and similarly to the
:doc:`Memcached adapter `, it is not limited to the current server's
@@ -19,9 +20,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``, ``\Relay\Relay`` or ``\Predis``.
+ ``\Redis``, ``\RedisArray``, ``RedisCluster``, ``\Relay\Relay``, ``\Relay\Cluster`` or ``\Predis``.
-This adapter expects a `Redis`_, `RedisArray`_, `RedisCluster`_, `Relay`_ or `Predis`_ instance to be
+This adapter expects a `Redis`_, `RedisArray`_, `RedisCluster`_, `Relay`_, `RelayCluster`_ 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::
@@ -47,6 +48,10 @@ as the second and third parameters::
?MarshallerInterface $marshaller = null
);
+.. versionadded:: 7.3
+
+ Support for ``Relay\Cluster`` was introduced in Symfony 7.3.
+
Configure the Connection
------------------------
@@ -61,6 +66,11 @@ helper method allows creating and configuring the Redis client class instance us
'redis://localhost'
);
+.. versionadded:: 7.3
+
+ Starting in Symfony 7.3, when using Valkey servers you can use the
+ ``valkey[s]:`` scheme instead of the ``redis[s]:`` one in your DSNs.
+
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").
@@ -220,11 +230,34 @@ Available Options
``ssl`` (type: ``array``, default: ``null``)
SSL context options. See `php.net/context.ssl`_ for more information.
+``relay_cluster_context`` (type: ``array``, default: ``[]``)
+ Defines configuration options specific to ``\Relay\Cluster``. For example, to
+ user a self-signed certificate for testing in local environment::
+
+ $options = [
+ // ...
+ 'relay_cluster_context' => [
+ // ...
+ 'stream' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'allow_self_signed' => true,
+ 'local_cert' => '/valkey.crt',
+ 'local_pk' => '/valkey.key',
+ 'cafile' => '/valkey.crt',
+ ],
+ ],
+ ];
+
.. versionadded:: 7.1
- The option `sentinel_master` as an alias for `redis_sentinel` was introduced
+ The option ``sentinel_master`` as an alias for ``redis_sentinel`` was introduced
in Symfony 7.1.
+.. versionadded:: 7.3
+
+ The ``relay_cluster_context`` option was introduced in Symfony 7.3.
+
.. note::
When using the `Predis`_ library some additional Predis-specific options are available.
@@ -348,10 +381,12 @@ Supports key rotation, ensuring secure decryption with both old and new keys::
.. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name
.. _`Redis server`: https://redis.io/
+.. _`Valkey`: https://valkey.io/
.. _`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/
+.. _`RelayCluster`: https://relay.so/docs/1.x/connections#cluster
.. _`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/config/caching.rst b/components/config/caching.rst
index 18620c0d8cf..80e23a4fdfb 100644
--- a/components/config/caching.rst
+++ b/components/config/caching.rst
@@ -3,7 +3,7 @@ Caching based on Resources
When all configuration resources are loaded, you may want to process the
configuration values and combine them all in one file. This file acts
-like a cache. Its contents don’t have to be regenerated every time the
+like a cache. Its contents don't have to be regenerated every time the
application runs – only when the configuration resources are modified.
For example, the Symfony Routing component allows you to load all routes,
diff --git a/components/config/definition.rst b/components/config/definition.rst
index 4848af33ffe..2b1841bc24a 100644
--- a/components/config/definition.rst
+++ b/components/config/definition.rst
@@ -186,14 +186,14 @@ The configuration can now be written like this::
->end()
;
-You can also use the ``enumClass()`` method to pass the FQCN of an enum
+You can also use the ``enumFqcn()`` method to pass the FQCN of an enum
class to the node. This will automatically set the values of the node to
the cases of the enum::
$rootNode
->children()
->enumNode('delivery')
- ->enumClass(Delivery::class)
+ ->enumFqcn(Delivery::class)
->end()
->end()
;
@@ -203,7 +203,7 @@ to one of the enum cases if possible.
.. versionadded:: 7.3
- The ``enumClass()`` method was introduced in Symfony 7.3.
+ The ``enumFqcn()`` method was introduced in Symfony 7.3.
Array Nodes
~~~~~~~~~~~
diff --git a/components/console/changing_default_command.rst b/components/console/changing_default_command.rst
index c69995ea395..2195bbd2697 100644
--- a/components/console/changing_default_command.rst
+++ b/components/console/changing_default_command.rst
@@ -9,20 +9,14 @@ name to the ``setDefaultCommand()`` method::
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
- use Symfony\Component\Console\Input\InputInterface;
- use Symfony\Component\Console\Output\OutputInterface;
+ use Symfony\Component\Console\Style\SymfonyStyle;
- #[AsCommand(name: 'hello:world')]
+ #[AsCommand(name: 'hello:world', description: 'Outputs "Hello World"')]
class HelloWorldCommand extends Command
{
- protected function configure(): void
+ public function __invoke(SymfonyStyle $io): int
{
- $this->setDescription('Outputs "Hello World"');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int
- {
- $output->writeln('Hello World');
+ $io->writeln('Hello World');
return Command::SUCCESS;
}
diff --git a/components/console/events.rst b/components/console/events.rst
index e550025b7dd..699ba444747 100644
--- a/components/console/events.rst
+++ b/components/console/events.rst
@@ -209,36 +209,32 @@ method::
for these constants to be available.
If you use the Console component inside a Symfony application, commands can
-handle signals themselves. To do so, implement the
-:class:`Symfony\\Component\\Console\\Command\\SignalableCommandInterface` and subscribe to one or more signals::
+handle signals themselves by subscribing to the :class:`Symfony\\Component\\Console\\Event\\ConsoleSignalEvent` event::
- // src/Command/SomeCommand.php
+ // src/Command/MyCommand.php
namespace App\Command;
- use Symfony\Component\Console\Command\Command;
- use Symfony\Component\Console\Command\SignalableCommandInterface;
+ use Symfony\Component\Console\Attribute\AsCommand;
+ use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
- class SomeCommand extends Command implements SignalableCommandInterface
+ #[AsCommand(name: 'app:my-command')]
+ class MyCommand
{
// ...
- public function getSubscribedSignals(): array
+ #[AsEventListener(ConsoleSignalEvent::class)]
+ public function handleSignal(ConsoleSignalEvent $event): void
{
- // return here any of the constants defined by PCNTL extension
- return [\SIGINT, \SIGTERM];
- }
-
- public function handleSignal(int $signal): int|false
- {
- if (\SIGINT === $signal) {
+ // set here any of the constants defined by PCNTL extension
+ if (in_array($event->getHandlingSignal(), [\SIGINT, \SIGTERM], true)) {
// ...
}
// ...
- // return an integer to set the exit code, or
+ // set an integer exit code, or
// false to continue normal execution
- return 0;
+ $event->setExitCode(0);
}
}
diff --git a/components/console/helpers/cursor.rst b/components/console/helpers/cursor.rst
index c5cab6c6d0b..63045f178ad 100644
--- a/components/console/helpers/cursor.rst
+++ b/components/console/helpers/cursor.rst
@@ -13,16 +13,16 @@ of the output:
// src/Command/MyCommand.php
namespace App\Command;
- use Symfony\Component\Console\Command\Command;
+ use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Cursor;
- use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
- class MyCommand extends Command
+ #[AsCommand(name: 'app:my-command')]
+ class MyCommand
{
// ...
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(OutputInterface $output): int
{
// ...
diff --git a/components/console/helpers/debug_formatter.rst b/components/console/helpers/debug_formatter.rst
index 10d3c67a79a..8fa59c319c9 100644
--- a/components/console/helpers/debug_formatter.rst
+++ b/components/console/helpers/debug_formatter.rst
@@ -10,15 +10,14 @@ this:
.. image:: /_images/components/console/debug_formatter.png
:alt: Console output, with the first line showing "RUN Running figlet", followed by lines showing the output of the command prefixed with "OUT" and "RES Finished the command" as last line in the output.
-Using the debug_formatter
+Using the Debug Formatter
-------------------------
-The formatter is included in the default helper set and you can get it by
-calling :method:`Symfony\\Component\\Console\\Command\\Command::getHelper`::
+The debug formatter helper can be instantiated directly as shown::
- $debugFormatter = $this->getHelper('debug_formatter');
+ $debugFormatter = new DebugFormatterHelper();
-The formatter accepts strings and returns a formatted string, which you then
+It accepts strings and returns a formatted string, which you then
output to the console (or even log the information or do anything else).
All methods of this helper have an identifier as the first argument. This is a
diff --git a/components/console/helpers/formatterhelper.rst b/components/console/helpers/formatterhelper.rst
index d2b19915a3a..cf9bacdeb9c 100644
--- a/components/console/helpers/formatterhelper.rst
+++ b/components/console/helpers/formatterhelper.rst
@@ -1,15 +1,11 @@
Formatter Helper
================
-The Formatter helper provides functions to format the output with colors.
-You can do more advanced things with this helper than you can in
-:doc:`/console/coloring`.
+The :class:`Symfony\\Component\\Console\\Helper\\FormatterHelper` helper provides
+functions to format the output with colors. You can do more advanced things with
+this helper than you can with the :doc:`basic colors and styles `::
-The :class:`Symfony\\Component\\Console\\Helper\\FormatterHelper` is included
-in the default helper set and you can get it by calling
-:method:`Symfony\\Component\\Console\\Command\\Command::getHelper`::
-
- $formatter = $this->getHelper('formatter');
+ $formatter = new FormatterHelper();
The methods return a string, which you'll usually render to the console by
passing it to the
diff --git a/components/console/helpers/map.rst.inc b/components/console/helpers/map.rst.inc
index b190d1dce44..73d5d4da7a0 100644
--- a/components/console/helpers/map.rst.inc
+++ b/components/console/helpers/map.rst.inc
@@ -1,6 +1,7 @@
* :doc:`/components/console/helpers/formatterhelper`
* :doc:`/components/console/helpers/processhelper`
* :doc:`/components/console/helpers/progressbar`
+* :doc:`/components/console/helpers/progressindicator`
* :doc:`/components/console/helpers/questionhelper`
* :doc:`/components/console/helpers/table`
* :doc:`/components/console/helpers/tree`
diff --git a/components/console/helpers/processhelper.rst b/components/console/helpers/processhelper.rst
index b46d9f2e95f..df9a8efe45b 100644
--- a/components/console/helpers/processhelper.rst
+++ b/components/console/helpers/processhelper.rst
@@ -11,7 +11,7 @@ a very verbose verbosity (e.g. ``-vv``)::
use Symfony\Component\Process\Process;
- $helper = $this->getHelper('process');
+ $helper = new ProcessHelper();
$process = new Process(['figlet', 'Symfony']);
$helper->run($output, $process);
diff --git a/components/console/helpers/questionhelper.rst b/components/console/helpers/questionhelper.rst
index c7e064b16ca..6d22a2de2af 100644
--- a/components/console/helpers/questionhelper.rst
+++ b/components/console/helpers/questionhelper.rst
@@ -2,11 +2,9 @@ Question Helper
===============
The :class:`Symfony\\Component\\Console\\Helper\\QuestionHelper` provides
-functions to ask the user for more information. It is included in the default
-helper set and you can get it by calling
-:method:`Symfony\\Component\\Console\\Command\\Command::getHelper`::
+functions to ask the user for more information::
- $helper = $this->getHelper('question');
+ $helper = new QuestionHelper();
The Question Helper has a single method
:method:`Symfony\\Component\\Console\\Helper\\QuestionHelper::ask` that needs an
@@ -27,18 +25,18 @@ Suppose you want to confirm an action before actually executing it. Add
the following to your 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;
use Symfony\Component\Console\Question\ConfirmationQuestion;
- class YourCommand extends Command
+ #[AsCommand(name: 'app:my-command')]
+ class MyCommand
{
- // ...
-
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(InputInterface $input, OutputInterface $output): int
{
- $helper = $this->getHelper('question');
+ $helper = new QuestionHelper();
$question = new ConfirmationQuestion('Continue with this action?', false);
if (!$helper->ask($input, $output, $question)) {
@@ -91,7 +89,7 @@ if you want to know a bundle name, you can add this to your command::
use Symfony\Component\Console\Question\Question;
// ...
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(InputInterface $input, OutputInterface $output): int
{
// ...
$question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle');
@@ -121,10 +119,10 @@ but ``red`` could be set instead (could be more explicit)::
use Symfony\Component\Console\Question\ChoiceQuestion;
// ...
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(InputInterface $input, OutputInterface $output): int
{
// ...
- $helper = $this->getHelper('question');
+ $helper = new QuestionHelper();
$question = new ChoiceQuestion(
'Please select your favorite color (defaults to red)',
// choices can also be PHP objects that implement __toString() method
@@ -184,10 +182,10 @@ this use :method:`Symfony\\Component\\Console\\Question\\ChoiceQuestion::setMult
use Symfony\Component\Console\Question\ChoiceQuestion;
// ...
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(InputInterface $input, OutputInterface $output): int
{
// ...
- $helper = $this->getHelper('question');
+ $helper = new QuestionHelper();
$question = new ChoiceQuestion(
'Please select your favorite colors (defaults to red and blue)',
['red', 'blue', 'yellow'],
@@ -218,10 +216,10 @@ will be autocompleted as the user types::
use Symfony\Component\Console\Question\Question;
// ...
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(InputInterface $input, OutputInterface $output): int
{
// ...
- $helper = $this->getHelper('question');
+ $helper = new QuestionHelper();
$bundles = ['AcmeDemoBundle', 'AcmeBlogBundle', 'AcmeStoreBundle'];
$question = new Question('Please enter the name of a bundle', 'FooBundle');
@@ -241,9 +239,9 @@ provide a callback function to dynamically generate suggestions::
use Symfony\Component\Console\Question\Question;
// ...
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(InputInterface $input, OutputInterface $output): int
{
- $helper = $this->getHelper('question');
+ $helper = new QuestionHelper();
// This function is called whenever the input changes and new
// suggestions are needed.
@@ -282,10 +280,10 @@ You can also specify if you want to not trim the answer by setting it directly w
use Symfony\Component\Console\Question\Question;
// ...
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(InputInterface $input, OutputInterface $output): int
{
// ...
- $helper = $this->getHelper('question');
+ $helper = new QuestionHelper();
$question = new Question('What is the name of the child?');
$question->setTrimmable(false);
@@ -308,10 +306,10 @@ the response to a question should allow multiline answers by passing ``true`` to
use Symfony\Component\Console\Question\Question;
// ...
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(InputInterface $input, OutputInterface $output): int
{
// ...
- $helper = $this->getHelper('question');
+ $helper = new QuestionHelper();
$question = new Question('How do you solve world peace?');
$question->setMultiline(true);
@@ -335,10 +333,10 @@ convenient for passwords::
use Symfony\Component\Console\Question\Question;
// ...
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(InputInterface $input, OutputInterface $output): int
{
// ...
- $helper = $this->getHelper('question');
+ $helper = new QuestionHelper();
$question = new Question('What is the database password?');
$question->setHidden(true);
@@ -372,10 +370,10 @@ convenient for passwords::
use Symfony\Component\Console\Question\ChoiceQuestion;
// ...
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(InputInterface $input, OutputInterface $output): int
{
// ...
- $helper = $this->getHelper('question');
+ $helper = new QuestionHelper();
QuestionHelper::disableStty();
// ...
@@ -396,10 +394,10 @@ method::
use Symfony\Component\Console\Question\Question;
// ...
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(InputInterface $input, OutputInterface $output): int
{
// ...
- $helper = $this->getHelper('question');
+ $helper = new QuestionHelper();
$question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle');
$question->setNormalizer(function (string $value): string {
@@ -434,10 +432,10 @@ method::
use Symfony\Component\Console\Question\Question;
// ...
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(InputInterface $input, OutputInterface $output): int
{
// ...
- $helper = $this->getHelper('question');
+ $helper = new QuestionHelper();
$question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle');
$question->setValidator(function (string $answer): string {
@@ -494,10 +492,10 @@ You can also use a validator with a hidden question::
use Symfony\Component\Console\Question\Question;
// ...
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(InputInterface $input, OutputInterface $output): int
{
// ...
- $helper = $this->getHelper('question');
+ $helper = new QuestionHelper();
$question = new Question('Please enter your password');
$question->setNormalizer(function (?string $value): string {
diff --git a/components/console/helpers/table.rst b/components/console/helpers/table.rst
index 9d6fdb0ee61..e36b1570b70 100644
--- a/components/console/helpers/table.rst
+++ b/components/console/helpers/table.rst
@@ -1,36 +1,25 @@
-Table
-=====
+Table Helper
+============
-When building a console application it may be useful to display tabular data:
-
-.. code-block:: terminal
-
- +---------------+--------------------------+------------------+
- | ISBN | Title | Author |
- +---------------+--------------------------+------------------+
- | 99921-58-10-7 | Divine Comedy | Dante Alighieri |
- | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
- | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
- | 80-902734-1-6 | And Then There Were None | Agatha Christie |
- +---------------+--------------------------+------------------+
-
-.. note::
-
- As an alternative, consider using the
- :ref:`SymfonyStyle ` to display a table.
+When building console applications, Symfony provides several utilities for
+rendering tabular data. The simplest option is to use the table methods from
+:ref:`Symfony Style `. While convenient, this approach
+doesn't allow customization of the table's design. For more control and advanced
+features, use the ``Table`` console helper explained in this article.
To display a table, use :class:`Symfony\\Component\\Console\\Helper\\Table`,
set the headers, set the rows and then render the table::
+ use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
- use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
// ...
- class SomeCommand extends Command
+ #[AsCommand(name: 'app:my-command')]
+ class MyCommand
{
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(OutputInterface $output): int
{
$table = new Table($output);
$table
@@ -48,6 +37,22 @@ set the headers, set the rows and then render the table::
}
}
+This outputs:
+
+.. code-block:: terminal
+
+ +---------------+--------------------------+------------------+
+ | ISBN | Title | Author |
+ +---------------+--------------------------+------------------+
+ | 99921-58-10-7 | Divine Comedy | Dante Alighieri |
+ | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
+ | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
+ | 80-902734-1-6 | And Then There Were None | Agatha Christie |
+ +---------------+--------------------------+------------------+
+
+Adding Table Separators
+-----------------------
+
You can add a table separator anywhere in the output by passing an instance of
:class:`Symfony\\Component\\Console\\Helper\\TableSeparator` as a row::
@@ -61,6 +66,8 @@ You can add a table separator anywhere in the output by passing an instance of
['80-902734-1-6', 'And Then There Were None', 'Agatha Christie'],
]);
+This outputs:
+
.. code-block:: terminal
+---------------+--------------------------+------------------+
@@ -73,6 +80,9 @@ You can add a table separator anywhere in the output by passing an instance of
| 80-902734-1-6 | And Then There Were None | Agatha Christie |
+---------------+--------------------------+------------------+
+Adding Table Titles
+-------------------
+
You can optionally display titles at the top and the bottom of the table::
// ...
@@ -80,6 +90,8 @@ You can optionally display titles at the top and the bottom of the table::
$table->setFooterTitle('Page 1/2');
$table->render();
+This outputs:
+
.. code-block:: terminal
+---------------+----------- Books --------+------------------+
@@ -92,6 +104,9 @@ You can optionally display titles at the top and the bottom of the table::
| 80-902734-1-6 | And Then There Were None | Agatha Christie |
+---------------+--------- Page 1/2 -------+------------------+
+Setting the Column Widths Explicitly
+------------------------------------
+
By default, the width of the columns is calculated automatically based on their
contents. Use the :method:`Symfony\\Component\\Console\\Helper\\Table::setColumnWidths`
method to set the column widths explicitly::
@@ -114,7 +129,7 @@ argument is the column width::
$table->setColumnWidth(2, 30);
$table->render();
-The output of this command will be:
+This outputs:
.. code-block:: terminal
@@ -141,7 +156,7 @@ If you prefer to wrap long contents in multiple rows, use the
$table->setColumnMaxWidth(1, 10);
$table->render();
-The output of this command will be:
+This outputs:
.. code-block:: terminal
@@ -154,6 +169,9 @@ The output of this command will be:
| (the rest of the rows...) |
+-------+------------+--------------------------------+
+Rendering Vertical Tables
+-------------------------
+
By default, table contents are displayed horizontally. You can change this behavior
via the :method:`Symfony\\Component\\Console\\Helper\\Table::setVertical` method::
@@ -161,7 +179,7 @@ via the :method:`Symfony\\Component\\Console\\Helper\\Table::setVertical` method
$table->setVertical();
$table->render();
-The output of this command will be:
+This outputs:
.. code-block:: terminal
@@ -175,37 +193,24 @@ The output of this command will be:
| Author: Charles Dickens |
+------------------------------+
+Customizing the Table Style
+---------------------------
+
The table style can be changed to any built-in styles via
:method:`Symfony\\Component\\Console\\Helper\\Table::setStyle`::
- // same as calling nothing
+ // this 'default' style is the one used when no style is specified
$table->setStyle('default');
- // changes the default style to markdown
- $table->setStyle('markdown');
- $table->render();
+Built-in Table Styles
+~~~~~~~~~~~~~~~~~~~~~
-This outputs the table in the Markdown format:
-
-.. code-block:: terminal
-
- | ISBN | Title | Author |
- |---------------|--------------------------|------------------|
- | 99921-58-10-7 | Divine Comedy | Dante Alighieri |
- | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
- | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
- | 80-902734-1-6 | And Then There Were None | Agatha Christie |
-
-.. versionadded:: 7.3
-
- The ``markdown`` style was introduced in Symfony 7.3.
-
-You can also set the style to ``compact``::
+**Compact**::
$table->setStyle('compact');
$table->render();
-The output of this command will be:
+This outputs:
.. code-block:: terminal
@@ -215,12 +220,12 @@ The output of this command will be:
960-425-059-0 The Lord of the Rings J. R. R. Tolkien
80-902734-1-6 And Then There Were None Agatha Christie
-You can also set the style to ``borderless``::
+**Borderless**::
$table->setStyle('borderless');
$table->render();
-which outputs:
+This outputs:
.. code-block:: terminal
@@ -233,12 +238,12 @@ which outputs:
80-902734-1-6 And Then There Were None Agatha Christie
=============== ========================== ==================
-You can also set the style to ``box``::
+**Box**::
$table->setStyle('box');
$table->render();
-which outputs:
+This outputs:
.. code-block:: terminal
@@ -251,12 +256,12 @@ which outputs:
│ 80-902734-1-6 │ And Then There Were None │ Agatha Christie │
└───────────────┴──────────────────────────┴──────────────────┘
-You can also set the style to ``box-double``::
+**Double box**::
$table->setStyle('box-double');
$table->render();
-which outputs:
+This outputs:
.. code-block:: terminal
@@ -269,7 +274,30 @@ which outputs:
║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║
╚═══════════════╧══════════════════════════╧══════════════════╝
-If the built-in styles do not fit your need, define your own::
+**Markdown**::
+
+ $table->setStyle('markdown');
+ $table->render();
+
+This outputs:
+
+.. code-block:: terminal
+
+ | ISBN | Title | Author |
+ |---------------|--------------------------|------------------|
+ | 99921-58-10-7 | Divine Comedy | Dante Alighieri |
+ | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
+ | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
+ | 80-902734-1-6 | And Then There Were None | Agatha Christie |
+
+.. versionadded:: 7.3
+
+ The ``markdown`` style was introduced in Symfony 7.3.
+
+Making a Custom Table Style
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If the built-in styles do not fit your needs, define your own::
use Symfony\Component\Console\Helper\TableStyle;
@@ -359,7 +387,7 @@ To make a table cell that spans multiple columns you can use a :class:`Symfony\\
;
$table->render();
-This results in:
+This outputs:
.. code-block:: terminal
@@ -382,7 +410,7 @@ This results in:
]);
// ...
- This generates:
+ This outputs:
.. code-block:: terminal
@@ -445,9 +473,10 @@ The only requirement to append rows is that the table must be rendered inside a
use Symfony\Component\Console\Helper\Table;
// ...
- class SomeCommand extends Command
+ #[AsCommand(name: 'app:my-command')]
+ class MyCommand
{
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(OutputInterface $output): int
{
$section = $output->section();
$table = new Table($section);
@@ -461,7 +490,7 @@ The only requirement to append rows is that the table must be rendered inside a
}
}
-This will display the following table in the terminal:
+This outputs:
.. code-block:: terminal
@@ -482,7 +511,7 @@ This will display the following table in the terminal:
$table->render();
// ...
- This will display:
+ This outputs:
.. code-block:: terminal
diff --git a/components/console/helpers/tree.rst b/components/console/helpers/tree.rst
index 1161d00e942..5e08e684e51 100644
--- a/components/console/helpers/tree.rst
+++ b/components/console/helpers/tree.rst
@@ -26,22 +26,17 @@ inside your console command::
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
- use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\TreeHelper;
use Symfony\Component\Console\Helper\TreeNode;
- use Symfony\Component\Console\Input\InputInterface;
- use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
- #[AsCommand(name: 'app:some-command', description: '...')]
- class SomeCommand extends Command
+ #[AsCommand(name: 'app:my-command', description: '...')]
+ class MyCommand
{
// ...
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(SymfonyStyle $io): int
{
- $io = new SymfonyStyle($input, $output);
-
$node = TreeNode::fromValues([
'config/',
'public/',
@@ -96,25 +91,51 @@ The above code will output the following tree:
└── templates
└── base.html.twig
-Building and Rendering a Tree
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Building a Tree Programmatically
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-You can build a tree by creating a new instance of the
-:class:`Symfony\\Component\\Console\\Helper\\Tree` class and adding nodes to it::
+If you don't know the tree elements beforehand, you can build the tree programmatically
+by creating a new instance of the :class:`Symfony\\Component\\Console\\Helper\\Tree`
+class and adding nodes to it::
use Symfony\Component\Console\Helper\TreeHelper;
use Symfony\Component\Console\Helper\TreeNode;
- $node = TreeNode::fromValues([
- 'Command',
- 'Controller' => [
- 'DefaultController.php',
- ],
- 'Kernel.php',
- ]);
- $node->addChild('templates');
- $node->addChild('tests');
+ $root = new TreeNode('my-project/');
+ // you can pass a string directly or create a TreeNode object
+ $root->addChild('src/');
+ $root->addChild(new TreeNode('templates/'));
+
+ // create nested structures by adding child nodes to other nodes
+ $testsNode = new TreeNode('tests/');
+ $functionalTestsNode = new TreeNode('Functional/');
+ $testsNode->addChild($functionalTestsNode);
+ $root->addChild($testsNode);
+
+ $tree = TreeHelper::createTree($io, $root);
+ $tree->render();
+
+This example outputs:
+
+.. code-block:: terminal
+
+ my-project/
+ ├── src/
+ ├── templates/
+ └── tests/
+ └── Functional/
+
+If you prefer, you can build the array of elements programmatically and then
+create and render the tree like this::
+
+ $tree = TreeHelper::createTree($io, null, $array);
+ $tree->render();
+
+You can also build part of the tree from an array and then add other nodes::
+ $node = TreeNode::fromValues($array);
+ $node->addChild('templates');
+ // ...
$tree = TreeHelper::createTree($io, $node);
$tree->render();
@@ -125,15 +146,13 @@ Built-in Tree Styles
~~~~~~~~~~~~~~~~~~~~
The tree helper provides a few built-in styles that you can use to customize the
-output of the tree::
+output of the tree.
- use Symfony\Component\Console\Helper\TreeStyle;
- // ...
+**Default**::
- $tree = TreeHelper::createTree($io, $node, [], TreeStyle::compact());
- $tree->render();
+ TreeHelper::createTree($io, $node, [], TreeStyle::default());
-``TreeHelper::createTree($io, $node, [], TreeStyle::default())`` (`details`_)
+This outputs:
.. code-block:: terminal
@@ -150,7 +169,11 @@ output of the tree::
└── templates
└── base.html.twig
-``TreeHelper::createTree($io, $node, [], TreeStyle::box())`` (`details`_)
+**Box**::
+
+ TreeHelper::createTree($io, $node, [], TreeStyle::box());
+
+This outputs:
.. code-block:: terminal
@@ -167,7 +190,11 @@ output of the tree::
┗╸ templates
┗╸ base.html.twig
-``TreeHelper::createTree($io, $node, [], TreeStyle::doubleBox())`` (`details`_)
+**Double box**::
+
+ TreeHelper::createTree($io, $node, [], TreeStyle::doubleBox());
+
+This outputs:
.. code-block:: terminal
@@ -184,7 +211,11 @@ output of the tree::
╚═ templates
╚═ base.html.twig
-``TreeHelper::createTree($io, $node, [], TreeStyle::compact())`` (`details`_)
+**Compact**::
+
+ TreeHelper::createTree($io, $node, [], TreeStyle::compact());
+
+This outputs:
.. code-block:: terminal
@@ -201,7 +232,11 @@ output of the tree::
└ templates
└ base.html.twig
-``TreeHelper::createTree($io, $node, [], TreeStyle::light())`` (`details`_)
+**Light**::
+
+ TreeHelper::createTree($io, $node, [], TreeStyle::light());
+
+This outputs:
.. code-block:: terminal
@@ -218,7 +253,11 @@ output of the tree::
`-- templates
`-- base.html.twig
-``TreeHelper::createTree($io, $node, [], TreeStyle::minimal())`` (`details`_)
+**Minimal**::
+
+ TreeHelper::createTree($io, $node, [], TreeStyle::minimal());
+
+This outputs:
.. code-block:: terminal
@@ -235,7 +274,11 @@ output of the tree::
. templates
. base.html.twig
-``TreeHelper::createTree($io, $node, [], TreeStyle::rounded())`` (`details`_)
+**Rounded**::
+
+ TreeHelper::createTree($io, $node, [], TreeStyle::rounded());
+
+This outputs:
.. code-block:: terminal
@@ -291,5 +334,3 @@ The above code will output the following tree:
🔵 🟢 🟠 🟡 Kernel.php
🔵 🟠 🟡 templates
🔵 🔴 🟠 🟡 base.html.twig
-
-.. _`details`: https://github.com/symfony/symfony/blob/7.3/src/Symfony/Component/Console/Helper/TreeStyle.php
diff --git a/components/console/logger.rst b/components/console/logger.rst
index c3d5c447a89..cc182821a0a 100644
--- a/components/console/logger.rst
+++ b/components/console/logger.rst
@@ -34,7 +34,6 @@ You can rely on the logger to use this dependency inside a 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;
@@ -42,9 +41,9 @@ You can rely on the logger to use this dependency inside a command::
name: 'my:command',
description: 'Use an external dependency requiring a PSR-3 logger'
)]
- class MyCommand extends Command
+ class MyCommand
{
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(OutputInterface $output): int
{
$logger = new ConsoleLogger($output);
diff --git a/components/console/single_command_tool.rst b/components/console/single_command_tool.rst
index 97cb09bf030..9c6b06537e2 100644
--- a/components/console/single_command_tool.rst
+++ b/components/console/single_command_tool.rst
@@ -9,19 +9,18 @@ it is possible to remove this need by declaring a single command application::
setName('My Super Command') // Optional
->setVersion('1.0.0') // Optional
- ->addArgument('foo', InputArgument::OPTIONAL, 'The directory')
- ->addOption('bar', null, InputOption::VALUE_REQUIRED)
- ->setCode(function (InputInterface $input, OutputInterface $output): int {
+ ->setCode(function (OutputInterface $output, #[Argument] string $foo = 'The directory', #[Option] string $bar = ''): int {
// output arguments and options
+
+ return 0;
})
->run();
diff --git a/components/dependency_injection.rst b/components/dependency_injection.rst
index 93e8af711cf..d146f553a0c 100644
--- a/components/dependency_injection.rst
+++ b/components/dependency_injection.rst
@@ -180,16 +180,16 @@ You can override this behavior as follows::
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.
+* ``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
------------------------------------------------------
diff --git a/components/dependency_injection/compilation.rst b/components/dependency_injection/compilation.rst
index 7f991e85b72..c79281b5c27 100644
--- a/components/dependency_injection/compilation.rst
+++ b/components/dependency_injection/compilation.rst
@@ -497,11 +497,12 @@ serves at dumping the compiled container::
.. tip::
- The ``file_put_contents()`` function is not atomic. That could cause issues
- in a production environment with multiple concurrent requests. Instead, use
- the :ref:`dumpFile() method ` from Symfony Filesystem
- component or other methods provided by Symfony (e.g. ``$containerConfigCache->write()``)
- which are atomic.
+ The ``file_put_contents()`` function is not atomic. This can cause issues in
+ production environments with multiple concurrent requests. Instead, use the
+ :ref:`dumpFile() method ` from the
+ :doc:`Filesystem component ` or other atomic methods
+ provided by Symfony (e.g. the ``$containerConfigCache->write()`` method from
+ the :doc:`Config component `).
``ProjectServiceContainer`` is the default name given to the dumped container
class. However, you can change this with the ``class`` option when you
@@ -607,3 +608,25 @@ have the cache will be considered stale.
In the full-stack framework the compilation and caching of the container
is taken care of for you.
+
+.. _resolving-env-vars-at-compile-time:
+
+Resolving Environment Variable At Compile Time
+----------------------------------------------
+
+.. warning::
+
+ **This practice is discouraged**. Use it only if you fully understand the implications.
+
+By default, environment variables are resolved at runtime. However, you can
+force their resolution at compile time using the following code::
+
+ $parameterValue = $container->resolveEnvPlaceholders(
+ $container->getParameter('%env(ENV_VAR_NAME)%'),
+ true // resolve to actual values
+ );
+
+However, a **major drawback** of this approach is that you must manually clear
+the cache when changing the value of an environment variable. This goes
+against the typical behavior of environment variables, which are designed
+to be dynamic and not require cache invalidation.
diff --git a/components/event_dispatcher.rst b/components/event_dispatcher.rst
index 8cd676dd5fe..62a3707bb39 100644
--- a/components/event_dispatcher.rst
+++ b/components/event_dispatcher.rst
@@ -277,8 +277,9 @@ Dispatch the Event
The :method:`Symfony\\Component\\EventDispatcher\\EventDispatcher::dispatch`
method notifies all listeners of the given event. It takes two arguments:
-the ``Event`` instance to pass to each listener of that event and the name
-of the event to dispatch::
+the ``Event`` instance to pass to each listener of that event and optionally the
+name of the event to dispatch. If it's not defined, the class of the ``Event``
+instance will be used::
use Acme\Store\Event\OrderPlacedEvent;
use Acme\Store\Order;
diff --git a/components/filesystem.rst b/components/filesystem.rst
index dabf3f81872..4eae6aaad27 100644
--- a/components/filesystem.rst
+++ b/components/filesystem.rst
@@ -337,7 +337,8 @@ Dealing with file paths usually involves some difficulties:
- Platform differences: file paths look different on different platforms. UNIX
file paths start with a slash ("/"), while Windows file paths start with a
system drive ("C:"). UNIX uses forward slashes, while Windows uses backslashes
- by default.
+ by default. However, Windows also accepts forward slashes, so both types of
+ separators generally work.
- Absolute/relative paths: web applications frequently need to deal with absolute
and relative paths. Converting one to the other properly is tricky and repetitive.
@@ -375,6 +376,45 @@ Malformed paths are returned unchanged::
echo Path::canonicalize('C:Programs/PHP/php.ini');
// => C:Programs/PHP/php.ini
+Joining Paths
+~~~~~~~~~~~~~
+
+The :method:`Symfony\\Component\\Filesystem\\Path::join` method concatenates
+the given paths and normalizes separators. It's a cleaner alternative to
+string concatenation for building file paths::
+
+ echo Path::join('/var/www', 'vhost', 'config.ini');
+ // => /var/www/vhost/config.ini
+
+ echo Path::join('C:\\Program Files', 'PHP', 'php.ini');
+ // => C:/Program Files/PHP/php.ini
+ // (both forward slashes and backslashes work on Windows)
+
+The ``join()`` method handles multiple scenarios correctly:
+
+Empty parts are ignored::
+
+ echo Path::join('/var/www', '', 'config.ini');
+ // => /var/www/config.ini
+
+Leading slashes in subsequent arguments are removed::
+
+ echo Path::join('/var/www', '/etc', 'config.ini');
+ // => /var/www/etc/config.ini
+
+Trailing slashes are preserved only for root paths::
+
+ echo Path::join('/var/www', 'vhost/');
+ // => /var/www/vhost
+
+ echo Path::join('/', '');
+ // => /
+
+Works with any number of arguments::
+
+ echo Path::join('/var', 'www', 'vhost', 'symfony', 'config', 'config.ini');
+ // => /var/www/vhost/symfony/config/config.ini
+
Converting Absolute/Relative Paths
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/components/json_path.rst b/components/json_path.rst
new file mode 100644
index 00000000000..9db8e48885e
--- /dev/null
+++ b/components/json_path.rst
@@ -0,0 +1,330 @@
+The JsonPath Component
+======================
+
+.. versionadded:: 7.3
+
+ The JsonPath component was introduced in Symfony 7.3 as an
+ :doc:`experimental feature `.
+
+The JsonPath component lets you query and extract data from JSON structures.
+It implements the `RFC 9535 – JSONPath`_ standard, allowing you to navigate
+complex JSON data.
+
+Similar to the :doc:`DomCrawler component `, which lets
+you navigate and query HTML or XML documents with XPath, the JsonPath component
+offers the same convenience for traversing and searching JSON structures through
+JSONPath expressions. The component also provides an abstraction layer for data
+extraction.
+
+Installation
+------------
+
+You can install the component in your project using Composer:
+
+.. code-block:: terminal
+
+ $ composer require symfony/json-path
+
+.. include:: /components/require_autoload.rst.inc
+
+Usage
+-----
+
+To start querying a JSON document, first create a :class:`Symfony\\Component\\JsonPath\\JsonCrawler`
+object from a JSON string. The following examples use this sample "bookstore"
+JSON data::
+
+ use Symfony\Component\JsonPath\JsonCrawler;
+
+ $json = <<<'JSON'
+ {
+ "store": {
+ "book": [
+ {
+ "category": "reference",
+ "author": "Nigel Rees",
+ "title": "Sayings of the Century",
+ "price": 8.95
+ },
+ {
+ "category": "fiction",
+ "author": "Evelyn Waugh",
+ "title": "Sword of Honour",
+ "price": 12.99
+ },
+ {
+ "category": "fiction",
+ "author": "Herman Melville",
+ "title": "Moby Dick",
+ "isbn": "0-553-21311-3",
+ "price": 8.99
+ },
+ {
+ "category": "fiction",
+ "author": "John Ronald Reuel Tolkien",
+ "title": "The Lord of the Rings",
+ "isbn": "0-395-19395-8",
+ "price": 22.99
+ }
+ ],
+ "bicycle": {
+ "color": "red",
+ "price": 399
+ }
+ }
+ }
+ JSON;
+
+ $crawler = new JsonCrawler($json);
+
+Once you have the crawler instance, use its :method:`Symfony\\Component\\JsonPath\\JsonCrawler::find`
+method to start querying the data. This method returns an array of matching values.
+
+Querying with Expressions
+-------------------------
+
+The primary way to query the JSON is by passing a JSONPath expression string
+to the :method:`Symfony\\Component\\JsonPath\\JsonCrawler::find` method.
+
+Accessing a Specific Property
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Use dot notation for object keys and square brackets for array indices. The root
+of the document is represented by ``$``::
+
+ // get the title of the first book in the store
+ $titles = $crawler->find('$.store.book[0].title');
+
+ // $titles is ['Sayings of the Century']
+
+Dot notation is the default, but JSONPath provides other syntaxes for cases
+where it doesn't work. Use bracket notation (``['...']``) when a key contains
+spaces or special characters::
+
+ // this is equivalent to the previous example
+ $titles = $crawler->find('$["store"]["book"][0]["title"]');
+
+ // this expression requires brackets because some keys use dots or spaces
+ $titles = $crawler->find('$["store"]["book collection"][0]["title.original"]');
+
+ // you can combine both notations
+ $titles = $crawler->find('$["store"].book[0].title');
+
+Searching with the Descendant Operator
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The descendant operator (``..``) recursively searches for a given key, allowing
+you to find values without specifying the full path::
+
+ // get all authors from anywhere in the document
+ $authors = $crawler->find('$..author');
+
+ // $authors is ['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'John Ronald Reuel Tolkien']
+
+Filtering Results
+~~~~~~~~~~~~~~~~~
+
+JSONPath includes a filter syntax (``?(expression)``) to select items based on
+a condition. The current item within the filter is referenced by ``@``::
+
+ // get all books with a price less than 10
+ $cheapBooks = $crawler->find('$.store.book[?(@.price < 10)]');
+
+Building Queries Programmatically
+---------------------------------
+
+For more dynamic or complex query building, use the fluent API provided
+by the :class:`Symfony\\Component\\JsonPath\\JsonPath` class. This lets you
+construct a query object step by step. The ``JsonPath`` object can then be passed
+to the crawler's :method:`Symfony\\Component\\JsonPath\\JsonCrawler::find` method.
+
+The main advantage of the programmatic builder is that it automatically handles
+escaping of keys and values, preventing syntax errors::
+
+ use Symfony\Component\JsonPath\JsonPath;
+
+ $path = (new JsonPath())
+ ->key('store') // selects the 'store' key
+ ->key('book') // then the 'book' key
+ ->index(1); // then the second item (indexes start at 0)
+
+ // the created $path object is equivalent to the string '$["store"]["book"][1]'
+ $book = $crawler->find($path);
+
+ // $book contains the book object for "Sword of Honour"
+
+The :class:`Symfony\\Component\\JsonPath\\JsonPath` class provides several
+methods to build your query:
+
+* :method:`Symfony\\Component\\JsonPath\\JsonPath::key`
+ Adds a key selector. The key name is properly escaped::
+
+ // creates the path '$["key\"with\"quotes"]'
+ $path = (new JsonPath())->key('key"with"quotes');
+
+* :method:`Symfony\\Component\\JsonPath\\JsonPath::deepScan`
+ Adds the descendant operator ``..`` to perform a recursive search from the
+ current point in the path::
+
+ // get all prices in the store: '$["store"]..["price"]'
+ $path = (new JsonPath())->key('store')->deepScan()->key('price');
+
+* :method:`Symfony\\Component\\JsonPath\\JsonPath::all`
+ Adds the wildcard operator ``[*]`` to select all items in an array or object::
+
+ // creates the path '$["store"]["book"][*]'
+ $path = (new JsonPath())->key('store')->key('book')->all();
+
+* :method:`Symfony\\Component\\JsonPath\\JsonPath::index`
+ Adds an array index selector. Index numbers start at ``0``.
+
+* :method:`Symfony\\Component\\JsonPath\\JsonPath::first` /
+ :method:`Symfony\\Component\\JsonPath\\JsonPath::last`
+ Shortcuts for ``index(0)`` and ``index(-1)`` respectively::
+
+ // get the last book: '$["store"]["book"][-1]'
+ $path = (new JsonPath())->key('store')->key('book')->last();
+
+* :method:`Symfony\\Component\\JsonPath\\JsonPath::slice`
+ Adds an array slice selector ``[start:end:step]``::
+
+ // get books from index 1 up to (but not including) index 3
+ // creates the path '$["store"]["book"][1:3]'
+ $path = (new JsonPath())->key('store')->key('book')->slice(1, 3);
+
+ // get every second book from the first four books
+ // creates the path '$["store"]["book"][0:4:2]'
+ $path = (new JsonPath())->key('store')->key('book')->slice(0, 4, 2);
+
+* :method:`Symfony\\Component\\JsonPath\\JsonPath::filter`
+ Adds a filter expression. The expression string is the part that goes inside
+ the ``?()`` syntax::
+
+ // get expensive books: '$["store"]["book"][?(@.price > 20)]'
+ $path = (new JsonPath())
+ ->key('store')
+ ->key('book')
+ ->filter('@.price > 20');
+
+Advanced Querying
+-----------------
+
+For a complete overview of advanced operators like wildcards and functions within
+filters, refer to the `Querying with Expressions`_ section above. All these
+features are supported and can be combined with the programmatic builder when
+appropriate (e.g., inside a ``filter()`` expression).
+
+Testing with JSON Assertions
+----------------------------
+
+The component provides a set of PHPUnit assertions to make testing JSON data
+more convenient. Use the :class:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait`
+in your test class::
+
+ use PHPUnit\Framework\TestCase;
+ use Symfony\Component\JsonPath\Test\JsonPathAssertionsTrait;
+
+ class MyTest extends TestCase
+ {
+ use JsonPathAssertionsTrait;
+
+ public function testSomething(): void
+ {
+ $json = '{"books": [{"title": "A"}, {"title": "B"}]}';
+
+ self::assertJsonPathCount(2, '$.books[*]', $json);
+ }
+ }
+
+The trait provides the following assertion methods:
+
+* :method:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait::assertJsonPathCount`
+ Asserts that the number of elements found by the JSONPath expression matches
+ an expected count::
+
+ $json = '{"a": [1, 2, 3]}';
+ self::assertJsonPathCount(3, '$.a[*]', $json);
+
+* :method:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait::assertJsonPathEquals`
+ Asserts that the result of a JSONPath expression is equal to an expected
+ value. The comparison uses ``==`` (type coercion) instead of ``===``::
+
+ $json = '{"a": [1, 2, 3]}';
+
+ // passes because "1" == 1
+ self::assertJsonPathEquals(['1'], '$.a[0]', $json);
+
+* :method:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait::assertJsonPathNotEquals`
+ Asserts that the result of a JSONPath expression is not equal to an expected
+ value. The comparison uses ``!=`` (type coercion) instead of ``!==``::
+
+ $json = '{"a": [1, 2, 3]}';
+ self::assertJsonPathNotEquals([42], '$.a[0]', $json);
+
+* :method:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait::assertJsonPathSame`
+ Asserts that the result of a JSONPath expression is identical (``===``) to an
+ expected value. This is a strict comparison and does not perform type
+ coercion::
+
+ $json = '{"a": [1, 2, 3]}';
+
+ // fails because "1" !== 1
+ // self::assertJsonPathSame(['1'], '$.a[0]', $json);
+
+ self::assertJsonPathSame([1], '$.a[0]', $json);
+
+* :method:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait::assertJsonPathNotSame`
+ Asserts that the result of a JSONPath expression is not identical (``!==``) to
+ an expected value::
+
+ $json = '{"a": [1, 2, 3]}';
+ self::assertJsonPathNotSame(['1'], '$.a[0]', $json);
+
+* :method:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait::assertJsonPathContains`
+ Asserts that a given value is found within the array of results from the
+ JSONPath expression::
+
+ $json = '{"tags": ["php", "symfony", "json"]}';
+ self::assertJsonPathContains('symfony', '$.tags[*]', $json);
+
+* :method:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait::assertJsonPathNotContains`
+ Asserts that a given value is NOT found within the array of results from the
+ JSONPath expression::
+
+ $json = '{"tags": ["php", "symfony", "json"]}';
+ self::assertJsonPathNotContains('java', '$.tags[*]', $json);
+
+Error Handling
+--------------
+
+The component throws specific exceptions for invalid input or queries:
+
+* :class:`Symfony\\Component\\JsonPath\\Exception\\InvalidArgumentException`:
+ Thrown if the input to the ``JsonCrawler`` constructor is not a valid JSON string;
+* :class:`Symfony\\Component\\JsonPath\\Exception\\InvalidJsonStringInputException`:
+ Thrown during a ``find()`` call if the JSON string is malformed (e.g., syntax error);
+* :class:`Symfony\\Component\\JsonPath\\Exception\\JsonCrawlerException`:
+ Thrown for errors within the JsonPath expression itself, such as using an
+ unknown function
+
+Example of handling errors::
+
+ use Symfony\Component\JsonPath\Exception\InvalidJsonStringInputException;
+ use Symfony\Component\JsonPath\Exception\JsonCrawlerException;
+
+ try {
+ // the following line contains malformed JSON
+ $crawler = new JsonCrawler('{"store": }');
+ $crawler->find('$..*');
+ } catch (InvalidJsonStringInputException $e) {
+ // ... handle error
+ }
+
+ try {
+ // the following line contains an invalid query
+ $crawler->find('$.store.book[?unknown_function(@.price)]');
+ } catch (JsonCrawlerException $e) {
+ // ... handle error
+ }
+
+.. _`RFC 9535 – JSONPath`: https://datatracker.ietf.org/doc/html/rfc9535
diff --git a/components/lock.rst b/components/lock.rst
index b8ba38c8fc7..e9fe61ecd1a 100644
--- a/components/lock.rst
+++ b/components/lock.rst
@@ -404,8 +404,9 @@ Store Scope Blocking Ex
.. tip::
Symfony includes two other special stores that are mostly useful for testing:
- ``InMemoryStore``, which saves locks in memory during a process, and ``NullStore``,
- which doesn't persist anything.
+
+ * ``InMemoryStore`` (``LOCK_DSN=in-memory``), which saves locks in memory during a process;
+ * ``NullStore`` (``LOCK_DSN=null``) which doesn't persist anything.
.. versionadded:: 7.2
@@ -612,9 +613,9 @@ RedisStore
~~~~~~~~~~
The RedisStore saves locks on a Redis server, it requires a Redis connection
-implementing the ``\Redis``, ``\RedisArray``, ``\RedisCluster``, ``\Relay\Relay`` or
-``\Predis`` classes. This store does not support blocking, and expects a TTL to
-avoid stalled locks::
+implementing the ``\Redis``, ``\RedisArray``, ``\RedisCluster``, ``\Relay\Relay``,
+``\Relay\Cluster`` or ``\Predis`` classes. This store does not support blocking,
+and expects a TTL to avoid stalled locks::
use Symfony\Component\Lock\Store\RedisStore;
@@ -623,6 +624,10 @@ avoid stalled locks::
$store = new RedisStore($redis);
+.. versionadded:: 7.3
+
+ Support for ``Relay\Cluster`` was introduced in Symfony 7.3.
+
.. _lock-store-semaphore:
SemaphoreStore
diff --git a/components/messenger.rst b/components/messenger.rst
index 8d6652fb160..a8ff1e5290e 100644
--- a/components/messenger.rst
+++ b/components/messenger.rst
@@ -361,5 +361,5 @@ Learn more
/messenger
/messenger/*
-.. _`blog posts about command buses`: https://matthiasnoback.nl/tags/command%20bus/
+.. _`blog posts about command buses`: https://matthiasnoback.nl/tags/command-bus/
.. _`SimpleBus project`: https://docs.simplebus.io/en/latest/
diff --git a/components/options_resolver.rst b/components/options_resolver.rst
index 6f3a6751f28..17ec46c2fc9 100644
--- a/components/options_resolver.rst
+++ b/components/options_resolver.rst
@@ -936,7 +936,7 @@ can change your code to do the configuration only once per class::
public function __construct(array $options = [])
{
// What type of Mailer is this, a Mailer, a GoogleMailer, ... ?
- $class = get_class($this);
+ $class = $this::class;
// Was configureOptions() executed before for this class?
if (!isset(self::$resolversByClass[$class])) {
diff --git a/components/process.rst b/components/process.rst
index 7552537e82e..9c25c931510 100644
--- a/components/process.rst
+++ b/components/process.rst
@@ -430,11 +430,14 @@ 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\Console\Attribute\AsCommand;
+ use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Process\Process;
- class MyCommand extends Command
+ #[AsCommand(name: 'app:my-command')]
+ class MyCommand
{
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(SymfonyStyle $io): 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)
@@ -444,6 +447,8 @@ the :class:`Symfony\\Component\\Process\\PhpSubprocess` class to run the command
// 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']);
+
+ return 0;
}
}
diff --git a/components/property_info.rst b/components/property_info.rst
index 39019657ced..865a36c5941 100644
--- a/components/property_info.rst
+++ b/components/property_info.rst
@@ -131,7 +131,7 @@ class exposes public methods to extract several types of information:
$propertyInfo->getProperties($awesomeObject);
// Good!
- $propertyInfo->getProperties(get_class($awesomeObject));
+ $propertyInfo->getProperties($awesomeObject::class);
$propertyInfo->getProperties('Example\Namespace\YourAwesomeClass');
$propertyInfo->getProperties(YourAwesomeClass::class);
diff --git a/components/runtime.rst b/components/runtime.rst
index 4eb75de2a75..770ea102563 100644
--- a/components/runtime.rst
+++ b/components/runtime.rst
@@ -36,6 +36,9 @@ So how does this front-controller work? At first, the special
the component. This file runs the following logic:
#. It instantiates a :class:`Symfony\\Component\\Runtime\\RuntimeInterface`;
+#. The front-controller script (e.g. ``public/index.php``) is included by the
+ runtime, making it run again. Ensure this doesn't produce any side effects
+ in your code;
#. The callable (returned by ``public/index.php``) is passed to the Runtime, whose job
is to resolve the arguments (in this example: ``array $context``);
#. Then, this callable is called to get the application (``App\Kernel``);
diff --git a/components/uid.rst b/components/uid.rst
index 6c92fff0af9..b4083765436 100644
--- a/components/uid.rst
+++ b/components/uid.rst
@@ -32,13 +32,13 @@ 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 `__).
+(`read the 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();
+ // $uuid is an instance of Symfony\Component\Uid\UuidV1
.. tip::
@@ -48,7 +48,7 @@ Both are obtained automatically, so you don't have to pass any constructor argum
**UUID v2** (DCE security)
Similar to UUIDv1 but with a very high likelihood of ID collision
-(`read UUIDv2 spec `__).
+(`read the 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.
@@ -56,7 +56,7 @@ 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 `__).
+(`read the 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::
@@ -69,8 +69,8 @@ the namespace and the name::
// $namespace = Uuid::v4();
// $name can be any arbitrary string
- // $uuid is an instance of Symfony\Component\Uid\UuidV3
$uuid = Uuid::v3($namespace, $name);
+ // $uuid is an instance of Symfony\Component\Uid\UuidV3
These are the default namespaces defined by the standard:
@@ -81,20 +81,20 @@ These are the default namespaces defined by the standard:
**UUID v4** (random)
-Generates a random UUID (`read UUIDv4 spec `__).
+Generates a random UUID (`read the 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 is an instance of Symfony\Component\Uid\UuidV4
$uuid = Uuid::v4();
+ // $uuid is an instance of Symfony\Component\Uid\UuidV4
**UUID v5** (name-based, SHA-1)
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 `__).
+``md5`` to hash the given namespace and name (`read the UUIDv5 spec `__).
This makes it more secure and less prone to hash collisions.
.. _uid-uuid-v6:
@@ -103,12 +103,12 @@ This makes it more secure and less prone to hash collisions.
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 `__)::
+(`read the UUIDv6 spec `__)::
use Symfony\Component\Uid\Uuid;
- // $uuid is an instance of Symfony\Component\Uid\UuidV6
$uuid = Uuid::v6();
+ // $uuid is an instance of Symfony\Component\Uid\UuidV6
.. tip::
@@ -121,26 +121,28 @@ sortable (like :ref:`ULIDs `). It's more efficient for database indexing
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 `__).
+(`read the 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 is an instance of Symfony\Component\Uid\UuidV7
**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::
+Provides an RFC-compatible format intended for experimental or vendor-specific use cases
+(`read the UUIDv8 spec `__).
+You must generate the UUID value yourself. The only requirement is to set the
+variant and version bits of the UUID correctly. The rest of the UUID content is
+implementation-specific, and no particular format should be assumed::
use Symfony\Component\Uid\Uuid;
+ // pass your custom UUID value as the argument
+ $uuid = Uuid::v8('d9e7a184-5d5b-11ea-a62a-3499710062d0');
// $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::
@@ -367,6 +369,7 @@ entity primary keys::
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
+ use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;
@@ -375,7 +378,7 @@ entity primary keys::
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
- #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
+ #[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id;
public function getId(): ?Uuid
@@ -555,6 +558,7 @@ entity primary keys::
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
+ use Symfony\Bridge\Doctrine\IdGenerator\UlidGenerator;
use Symfony\Bridge\Doctrine\Types\UlidType;
use Symfony\Component\Uid\Ulid;
@@ -563,7 +567,7 @@ entity primary keys::
#[ORM\Id]
#[ORM\Column(type: UlidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
- #[ORM\CustomIdGenerator(class: 'doctrine.ulid_generator')]
+ #[ORM\CustomIdGenerator(class: UlidGenerator::class)]
private ?Ulid $id;
public function getId(): ?Ulid
diff --git a/components/var_exporter.rst b/components/var_exporter.rst
index fc6b34868db..c7ec9cd90d0 100644
--- a/components/var_exporter.rst
+++ b/components/var_exporter.rst
@@ -180,18 +180,50 @@ populated by using the special ``"\0"`` property name to define their internal v
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.
+Lazy objects are objects instantiated empty and populated on demand. This is
+particularly useful when, for example, a class has properties that require
+heavy computation to determine their values. In such cases, you may want to
+trigger the computation only when the property is actually accessed. This way,
+the expensive processing is avoided entirely if the property is never used.
+
+Since version 8.4, PHP provides support for lazy objects via the reflection API.
+This native API works with concrete classes, but not with abstract or internal ones.
+This component provides helpers to generate lazy objects using the decorator
+pattern, which also works with abstract classes, internal classes, and interfaces::
+
+ $proxyCode = ProxyHelper::generateLazyProxy(new \ReflectionClass(SomeInterface::class));
+ // $proxyCode should be dumped into a file in production environments
+ eval('class ProxyDecorator'.$proxyCode);
+
+ $proxy = ProxyDecorator::createLazyProxy(initializer: function (): SomeInterface {
+ // use whatever heavy logic you need here
+ // to compute the $dependencies of the proxied class
+ $instance = new SomeHeavyClass(...$dependencies);
+ // call setters, etc. if needed
+
+ return $instance;
+ });
+
+Use this mechanism only when native lazy objects cannot be leveraged
+(otherwise you'll get a deprecation notice).
+
+Legacy Creation of Lazy Objects
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When using a PHP version earlier than 8.4, native lazy objects are not available.
+In these cases, the VarExporter component provides two traits that help you
+implement lazy-loading mechanisms in your classes.
.. _var-exporter_ghost-objects:
LazyGhostTrait
-~~~~~~~~~~~~~~
+..............
+
+.. deprecated:: 7.3
+
+ ``LazyGhostTrait`` is deprecated since Symfony 7.3. Use PHP 8.4's native lazy
+ objects instead. Note that using the trait with PHP versions earlier than 8.4
+ does not trigger a deprecation, to ease the transition.
Ghost objects are empty objects, which see their properties populated the first
time any method is called. Thanks to :class:`Symfony\\Component\\VarExporter\\LazyGhostTrait`,
@@ -271,7 +303,13 @@ of :ref:`Virtual Proxies `.
.. _var-exporter_virtual-proxies:
LazyProxyTrait
-~~~~~~~~~~~~~~
+..............
+
+.. deprecated:: 7.3
+
+ ``LazyProxyTrait`` is deprecated since Symfony 7.3. Use PHP 8.4's native lazy
+ objects instead. Note that using the trait with PHP versions earlier than 8.4
+ does not trigger a deprecation, to ease the transition.
The purpose of virtual proxies in the same one as
:ref:`ghost objects `, but their internal behavior is
diff --git a/components/yaml.rst b/components/yaml.rst
index 471b59dcf5c..efaf84f04e6 100644
--- a/components/yaml.rst
+++ b/components/yaml.rst
@@ -298,7 +298,7 @@ You can make it convert to a ``DateTime`` instance by using the ``PARSE_DATETIME
flag::
$date = Yaml::parse('2016-05-27', Yaml::PARSE_DATETIME);
- var_dump(get_class($date)); // DateTime
+ var_dump($date::class); // DateTime
Dumping Multi-line Literal Blocks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/configuration.rst b/configuration.rst
index 35bc2fb7eec..4b1e75dcabe 100644
--- a/configuration.rst
+++ b/configuration.rst
@@ -37,7 +37,7 @@ example, this is the default file created by the "API Platform" bundle:
mapping:
paths: ['%kernel.project_dir%/src/Entity']
-Splitting the configuration into lots of small files might appear intimidating for some
+Splitting the configuration into lots of small files might seem intimidating to some
Symfony newcomers. However, you'll get used to them quickly and you rarely need
to change these files after package installation.
diff --git a/configuration/env_var_processors.rst b/configuration/env_var_processors.rst
index 2e82104db66..936d93c1061 100644
--- a/configuration/env_var_processors.rst
+++ b/configuration/env_var_processors.rst
@@ -951,3 +951,9 @@ To enable the new processor in the app, register it as a service and
tag. If you're using the
:ref:`default services.yaml configuration `,
this is already done for you, thanks to :ref:`autoconfiguration `.
+
+Resolving Environment Variable At Compile Time
+----------------------------------------------
+
+Environment variables are resolved at runtime, but you can also resolve them
+:ref:`at compile time `.
diff --git a/configuration/micro_kernel_trait.rst b/configuration/micro_kernel_trait.rst
index f919b1f7a74..542532ee1af 100644
--- a/configuration/micro_kernel_trait.rst
+++ b/configuration/micro_kernel_trait.rst
@@ -100,8 +100,7 @@ Next, create an ``index.php`` file that defines the kernel class and runs it:
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
-That's it! To test it, start the :doc:`Symfony Local Web Server
-`:
+That's it! To test it, start the :ref:`Symfony local web server `:
.. code-block:: terminal
@@ -297,8 +296,8 @@ Now it looks like this::
{
// import the WebProfilerRoutes, only if the bundle is enabled
if (isset($this->bundles['WebProfilerBundle'])) {
- $routes->import('@WebProfilerBundle/Resources/config/routing/wdt.xml')->prefix('/_wdt');
- $routes->import('@WebProfilerBundle/Resources/config/routing/profiler.xml')->prefix('/_profiler');
+ $routes->import('@WebProfilerBundle/Resources/config/routing/wdt.php', 'php')->prefix('/_wdt');
+ $routes->import('@WebProfilerBundle/Resources/config/routing/profiler.php', 'php')->prefix('/_profiler');
}
// load the routes defined as PHP attributes
@@ -310,6 +309,12 @@ Now it looks like this::
// to override the default locations for these directories
}
+
+.. versionadded:: 7.3
+
+ The ``wdt.php`` and ``profiler.php`` files were introduced in Symfony 7.3.
+ Previously, you had to import ``wdt.xml`` and ``profiler.xml``
+
Before continuing, run this command to add support for the new dependencies:
.. code-block:: terminal
@@ -465,8 +470,7 @@ this:
├─ composer.json
└─ composer.lock
-As before you can use the :doc:`Symfony Local Web Server
-`:
+As before you can use the :ref:`Symfony local web server `:
.. code-block:: terminal
diff --git a/configuration/secrets.rst b/configuration/secrets.rst
index f717456a22c..285b89d521e 100644
--- a/configuration/secrets.rst
+++ b/configuration/secrets.rst
@@ -311,7 +311,7 @@ The secrets system is enabled by default and some of its behavior can be configu
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/framework https://symfony.com/schema/dic/framework/framework-1.0.xsd"
>
-
+
`, it is recommended to use the built-in completion
- script that will ensure the right PHP version and configuration are used when
- running the Console Completion. Run ``symfony completion --help`` for the
- installation instructions for your shell. The Symfony CLI will provide
- completion for the ``console`` and ``composer`` commands.
+ If you are using the :doc:`Symfony CLI ` tool, follow
+ :ref:`these instructions ` to enable autocompletion.
+
+.. _console_creating-command:
Creating a Command
------------------
-Commands are defined in classes extending
-:class:`Symfony\\Component\\Console\\Command\\Command`. For example, you may
-want a command to create a user::
+Commands are defined in classes and auto-registered using the ``#[AsCommand]``
+attribute. For example, you may 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
+ class CreateUserCommand
{
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(): int
{
// ... put here the code to create the user
@@ -147,104 +142,54 @@ want a command to create a user::
}
}
-Configuring the Command
-~~~~~~~~~~~~~~~~~~~~~~~
-
-You can optionally define a description, help message and the
-:doc:`input options and arguments ` by overriding the
-``configure()`` method::
+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 `,
+this is already done for you, thanks to :ref:`autoconfiguration `.
- // src/Command/CreateUserCommand.php
+You can also use ``#[AsCommand]`` to add a description and longer help text for the command::
- // ...
- class CreateUserCommand extends Command
+ #[AsCommand(
+ name: 'app:create-user',
+ description: 'Creates a new user.', // the command description shown when running "php bin/console list"
+ help: 'This command allows you to create a user...', // the command help shown when running the command with the "--help" option
+ )]
+ class CreateUserCommand
{
- // ...
- protected function configure(): void
+ public function __invoke(): int
{
- $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...')
- ;
+ // ...
}
}
-.. tip::
-
- 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 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
-first and then call to the parent constructor, to make those properties
-available in the ``configure()`` method::
+Additionally, you can extend the :class:`Symfony\\Component\\Console\\Command\\Command` class to
+leverage advanced features like lifecycle hooks (e.g. :method:`Symfony\\Component\\Console\\Command\\Command::initialize` and
+and :method:`Symfony\\Component\\Console\\Command\\Command::interact`)::
- // ...
+ use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
- use Symfony\Component\Console\Input\InputArgument;
+ use Symfony\Component\Console\Input\InputInterface;
+ use Symfony\Component\Console\Output\OutputInterface;
+ #[AsCommand(name: 'app:create-user')]
class CreateUserCommand extends Command
{
- // ...
-
- public function __construct(bool $requirePassword = false)
+ public function initialize(InputInterface $input, OutputInterface $output): void
{
- // best practices recommend to call the parent constructor first and
- // then set your own properties. That wouldn't work in this case
- // because configure() needs the properties set in this constructor
- $this->requirePassword = $requirePassword;
-
- parent::__construct();
+ // ...
}
- protected function configure(): void
+ public function interact(InputInterface $input, OutputInterface $output): void
{
- $this
- // ...
- ->addArgument('password', $this->requirePassword ? InputArgument::REQUIRED : InputArgument::OPTIONAL, 'User password')
- ;
+ // ...
}
- }
-
-.. _console_registering-the-command:
-
-Registering the Command
-~~~~~~~~~~~~~~~~~~~~~~~
-You can register the command by adding the ``AsCommand`` attribute to it::
-
- // src/Command/CreateUserCommand.php
- namespace App\Command;
-
- use Symfony\Component\Console\Attribute\AsCommand;
- use Symfony\Component\Console\Command\Command;
-
- #[AsCommand(
- name: 'app:create-user',
- description: 'Creates a new user.',
- hidden: false,
- aliases: ['app:add-user']
- )]
- class CreateUserCommand extends Command
- {
- // ...
+ public function __invoke(): int
+ {
+ // ...
+ }
}
-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 `,
-this is already done for you, thanks to :ref:`autoconfiguration `.
-
Running the Command
~~~~~~~~~~~~~~~~~~~
@@ -255,16 +200,16 @@ After configuring and registering the command, you can run it in the terminal:
$ php bin/console app:create-user
As you might expect, this command will do nothing as you didn't write any logic
-yet. Add your own logic inside the ``execute()`` method.
+yet. Add your own logic inside the ``__invoke()`` method.
Console Output
--------------
-The ``execute()`` method has access to the output stream to write messages to
+The ``__invoke()`` method has access to the output stream to write messages to
the console::
// ...
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(OutputInterface $output): int
{
// outputs multiple lines to the console (adding "\n" at the end of each line)
$output->writeln([
@@ -315,9 +260,10 @@ method, which returns an instance of
// ...
use Symfony\Component\Console\Output\ConsoleOutputInterface;
- class MyCommand extends Command
+ #[AsCommand(name: 'app:my-command')]
+ class MyCommand
{
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(OutputInterface $output): int
{
if (!$output instanceof ConsoleOutputInterface) {
throw new \LogicException('This command accepts only an instance of "ConsoleOutputInterface".');
@@ -376,20 +322,12 @@ Console Input
Use input options or arguments to pass information to the command::
- use Symfony\Component\Console\Input\InputArgument;
+ use Symfony\Component\Console\Attribute\Argument;
- // ...
- protected function configure(): void
- {
- $this
- // configure an argument
- ->addArgument('username', InputArgument::REQUIRED, 'The username of the user.')
- // ...
- ;
- }
-
- // ...
- public function execute(InputInterface $input, OutputInterface $output): int
+ // The #[Argument] attribute configures $username as a
+ // required input argument and its value is automatically
+ // passed to this parameter
+ public function __invoke(#[Argument('The username of the user.')] string $username, OutputInterface $output): int
{
$output->writeln([
'User Creator',
@@ -397,8 +335,7 @@ Use input options or arguments to pass information to the command::
'',
]);
- // retrieve the argument value using getArgument()
- $output->writeln('Username: '.$input->getArgument('username'));
+ $output->writeln('Username: '.$username);
return Command::SUCCESS;
}
@@ -428,23 +365,22 @@ as a service, you can use normal dependency injection. Imagine you have a
// ...
use App\Service\UserManager;
- use Symfony\Component\Console\Command\Command;
+ use Symfony\Component\Console\Attribute\Argument;
+ use Symfony\Component\Console\Attribute\AsCommand;
- class CreateUserCommand extends Command
+ #[AsCommand(name: 'app:create-user')]
+ class CreateUserCommand
{
public function __construct(
- private UserManager $userManager,
- ){
- parent::__construct();
+ private UserManager $userManager
+ ) {
}
- // ...
-
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(#[Argument] string $username, OutputInterface $output): int
{
// ...
- $this->userManager->create($input->getArgument('username'));
+ $this->userManager->create($username);
$output->writeln('User successfully generated!');
@@ -472,7 +408,7 @@ command:
Note that it will not be called when the command is run without interaction
(e.g. when passing the ``--no-interaction`` global option flag).
-:method:`Symfony\\Component\\Console\\Command\\Command::execute` *(required)*
+``__invoke()`` (or :method:`Symfony\\Component\\Console\\Command\\Command::execute`) *(required)*
This method is executed after ``interact()`` and ``initialize()``.
It contains the logic you want the command to execute and it must
return an integer which will be used as the command `exit status`_.
diff --git a/console/calling_commands.rst b/console/calling_commands.rst
index dd1f0b12ff9..875ead15d2d 100644
--- a/console/calling_commands.rst
+++ b/console/calling_commands.rst
@@ -14,20 +14,18 @@ arguments and options you want to pass to the command. The command name must be
the first argument.
Eventually, calling the ``doRun()`` method actually runs the command and returns
-the returned code from the command (return value from command ``execute()``
+the returned code from the command (return value from command ``__invoke()``
method)::
// ...
- use Symfony\Component\Console\Command;
+ use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\ArrayInput;
- use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
- class CreateUserCommand extends Command
+ #[AsCommand(name: 'app:create-user')]
+ class CreateUserCommand
{
- // ...
-
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(OutputInterface $output): int
{
$greetInput = new ArrayInput([
// the command name is passed as first argument
diff --git a/console/commands_as_services.rst b/console/commands_as_services.rst
index 1393879a1df..ed5b99f9cb4 100644
--- a/console/commands_as_services.rst
+++ b/console/commands_as_services.rst
@@ -16,27 +16,16 @@ For example, suppose you want to log something from within your 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
+ #[AsCommand(name: 'app:sunshine', description: 'Good morning!')]
+ class SunshineCommand
{
public function __construct(
private LoggerInterface $logger,
) {
- // you *must* call the parent constructor
- parent::__construct();
- }
-
- protected function configure(): void
- {
- $this
- ->setDescription('Good morning!');
}
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(): int
{
$this->logger->info('Waking up the sun');
// ...
@@ -70,7 +59,7 @@ To make your command lazily loaded, either define its name using the PHP
// ...
#[AsCommand(name: 'app:sunshine')]
- class SunshineCommand extends Command
+ class SunshineCommand
{
// ...
}
diff --git a/console/hide_commands.rst b/console/hide_commands.rst
index 44a69d09289..4ab9d3a6dad 100644
--- a/console/hide_commands.rst
+++ b/console/hide_commands.rst
@@ -15,10 +15,9 @@ the ``hidden`` property of the ``AsCommand`` attribute::
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
+ class LegacyCommand
{
// ...
}
diff --git a/console/input.rst b/console/input.rst
index 7a978687066..d5b6e4881bb 100644
--- a/console/input.rst
+++ b/console/input.rst
@@ -447,11 +447,12 @@ The Console component adds some predefined options to all commands:
* ``--verbose``: sets the verbosity level (e.g. ``1`` the default, ``2`` and
``3``, or you can use respective shortcuts ``-v``, ``-vv`` and ``-vvv``)
* ``--silent``: disables all output and interaction, including errors
-* ``--quiet``: disables output and interaction, but errors are still displayed
-* ``--no-interaction``: disables interaction
-* ``--version``: outputs the version number of the console application
-* ``--help``: displays the command help
+* ``--quiet|-q``: disables output and interaction, but errors are still displayed
+* ``--no-interaction|-n``: disables interaction
+* ``--version|-V``: outputs the version number of the console application
+* ``--help|-h``: displays the command help
* ``--ansi|--no-ansi``: whether to force of disable coloring the output
+* ``--profile``: enables the Symfony profiler
.. versionadded:: 7.2
@@ -459,7 +460,7 @@ The Console component adds some predefined options to all commands:
When using the ``FrameworkBundle``, two more options are predefined:
-* ``--env``: sets the Kernel configuration environment (defaults to ``APP_ENV``)
+* ``--env|-e``: sets the Kernel configuration environment (defaults to ``APP_ENV``)
* ``--no-debug``: disables Kernel debug (defaults to ``APP_DEBUG``)
So your custom commands can use them too out-of-the-box.
diff --git a/console/lockable_trait.rst b/console/lockable_trait.rst
index 0f4a4900e17..2a4fd64ffaf 100644
--- a/console/lockable_trait.rst
+++ b/console/lockable_trait.rst
@@ -13,19 +13,17 @@ that adds two convenient methods to lock and release commands::
// ...
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\LockableTrait;
- use Symfony\Component\Console\Input\InputInterface;
- use Symfony\Component\Console\Output\OutputInterface;
+ use Symfony\Component\Console\Style\SymfonyStyle;
- class UpdateContentsCommand extends Command
+ #[AsCommand(name: 'contents:update')]
+ class UpdateContentsCommand
{
use LockableTrait;
- // ...
-
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(SymfonyStyle $io): int
{
if (!$this->lock()) {
- $output->writeln('The command is already running in another process.');
+ $io->writeln('The command is already running in another process.');
return Command::SUCCESS;
}
@@ -52,7 +50,8 @@ a ``$lockFactory`` property with your own lock factory::
use Symfony\Component\Console\Command\LockableTrait;
use Symfony\Component\Lock\LockFactory;
- class UpdateContentsCommand extends Command
+ #[AsCommand(name: 'contents:update')]
+ class UpdateContentsCommand
{
use LockableTrait;
diff --git a/console/style.rst b/console/style.rst
index e1e5df38ffe..5357b9e6172 100644
--- a/console/style.rst
+++ b/console/style.rst
@@ -7,18 +7,18 @@ questions to the user involves a lot of repetitive code.
Consider for example the code used to display the title of the following command::
- // src/Command/GreetCommand.php
+ // src/Command/MyCommand.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;
- class GreetCommand extends Command
+ #[AsCommand(name: 'app:my-command')]
+ class MyCommand
{
- // ...
-
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(InputInterface $input, OutputInterface $output): int
{
$output->writeln([
'Lorem Ipsum Dolor Sit Amet>',
@@ -42,26 +42,22 @@ which allow to create *semantic* commands and forget about their styling.
Basic Usage
-----------
-In your command, instantiate the :class:`Symfony\\Component\\Console\\Style\\SymfonyStyle`
-class and pass the ``$input`` and ``$output`` variables as its arguments. Then,
-you can start using any of its helpers, such as ``title()``, which displays the
-title of the command::
+In your ``__invoke()`` method, add an argument of type :class:`Symfony\\Component\\Console\\Style\\SymfonyStyle`.
+Then, you can start using any of its helpers, such as ``title()``, which
+displays the title of the command::
- // src/Command/GreetCommand.php
+ // src/Command/MyCommand.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;
use Symfony\Component\Console\Style\SymfonyStyle;
- class GreetCommand extends Command
+ #[AsCommand(name: 'app:my-command')]
+ class MyCommand
{
- // ...
-
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(SymfonyStyle $io): int
{
- $io = new SymfonyStyle($input, $output);
$io->title('Lorem Ipsum Dolor Sit Amet');
// ...
@@ -353,6 +349,12 @@ User Input Methods
$io->choice('Select the queue to analyze', ['queue1', 'queue2', 'queue3'], 'queue1');
+ Choice questions display both the choice value and a numeric index, which
+ starts from ``0`` by default. To use custom indices, pass an array with
+ custom numeric keys as the choice values::
+
+ $io->choice('Select the queue to analyze', [5 => 'queue1', 6 => 'queue2', 7 => 'queue3']);
+
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)::
@@ -448,19 +450,17 @@ long they are. This is done to enable clickable URLs in terminals that support t
If you prefer to wrap all contents, including URLs, use this method::
- // src/Command/GreetCommand.php
+ // src/Command/MyCommand.php
namespace App\Command;
// ...
use Symfony\Component\Console\Style\SymfonyStyle;
- class GreetCommand extends Command
+ #[AsCommand(name: 'app:my-command')]
+ class MyCommand
{
- // ...
-
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(SymfonyStyle $io): int
{
- $io = new SymfonyStyle($input, $output);
$io->getOutputWrapper()->setAllowCutUrls(true);
// ...
@@ -487,7 +487,7 @@ Then, instantiate this custom class instead of the default ``SymfonyStyle`` in
your commands. Thanks to the ``StyleInterface`` you won't need to change the code
of your commands to change their appearance::
- // src/Command/GreetCommand.php
+ // src/Command/MyCommand.php
namespace App\Console;
use App\Console\CustomStyle;
@@ -495,16 +495,11 @@ of your commands to change their appearance::
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
- class GreetCommand extends Command
+ #[AsCommand(name: 'app:my-command')]
+ class MyCommand
{
- // ...
-
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(InputInterface $input, OutputInterface $output): int
{
- // Before
- $io = new SymfonyStyle($input, $output);
-
- // After
$io = new CustomStyle($input, $output);
// ...
diff --git a/console/verbosity.rst b/console/verbosity.rst
index 9910dca0c3d..3afd085d773 100644
--- a/console/verbosity.rst
+++ b/console/verbosity.rst
@@ -49,26 +49,27 @@ It is possible to print a message in a command for only a specific verbosity
level. For example::
// ...
+ use Symfony\Component\Console\Attribute\Argument;
+ use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
- class CreateUserCommand extends Command
+ #[AsCommand(name: 'app:create-user')]
+ class CreateUserCommand
{
- // ...
-
- public function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(OutputInterface $output, #[Argument] string $username, #[Argument] string $password): int
{
$user = new User(...);
$output->writeln([
- 'Username: '.$input->getArgument('username'),
- 'Password: '.$input->getArgument('password'),
+ 'Username: '.$username,
+ 'Password: '.$password,
]);
// available methods: ->isSilent(), ->isQuiet(), ->isVerbose(), ->isVeryVerbose(), ->isDebug()
if ($output->isVerbose()) {
- $output->writeln('User class: '.get_class($user));
+ $output->writeln('User class: '.$user::class);
}
// alternatively you can pass the verbosity level PHP constant to writeln()
diff --git a/contributing/code/bc.rst b/contributing/code/bc.rst
index 497c70fb01d..ee3f72a0333 100644
--- a/contributing/code/bc.rst
+++ b/contributing/code/bc.rst
@@ -176,6 +176,13 @@ covered by our backward compatibility promise:
| Use a public, protected or private method | Yes |
+-----------------------------------------------+-----------------------------+
+Using our Translations
+~~~~~~~~~~~~~~~~~~~~~~
+
+All translations provided by Symfony for security and validation errors are
+intended for internal use only. They may be changed or removed at any time.
+Symfony's Backward Compatibility Promise does not apply to internal translations.
+
Working on Symfony Code
-----------------------
@@ -278,6 +285,7 @@ Make final No
Move to parent class Yes
:ref:`Add argument without a default value ` No
:ref:`Add argument with a default value ` No :ref:`[7] ` :ref:`[8] `
+Rename argument Yes :ref:`[10] `
Remove argument No :ref:`[3] `
Add default value to an argument No :ref:`[7] ` :ref:`[8] `
Remove default value of an argument No
@@ -297,6 +305,7 @@ Make public No
Move to parent class Yes
:ref:`Add argument without a default value ` No
:ref:`Add argument with a default value ` No :ref:`[7] ` :ref:`[8] `
+Rename argument Yes :ref:`[10] `
Remove argument No :ref:`[3] `
Add default value to an argument No :ref:`[7] ` :ref:`[8] `
Remove default value of an argument No :ref:`[7] `
@@ -313,6 +322,7 @@ Change name Yes
Make public or protected Yes
Add argument without a default value Yes
Add argument with a default value Yes
+Rename argument Yes
Remove argument Yes
Add default value to an argument Yes
Remove default value of an argument Yes
diff --git a/contributing/code/reproducer.rst b/contributing/code/reproducer.rst
index 3392ca87035..c2208b70b09 100644
--- a/contributing/code/reproducer.rst
+++ b/contributing/code/reproducer.rst
@@ -65,8 +65,8 @@ to a route definition. Then, after creating your project:
of controllers, actions, etc. as in your original application.
#. Create a small controller and add your routing definition that shows the bug.
#. Don't create or modify any other file.
-#. Install the :doc:`local web server ` provided by Symfony
- and use the ``symfony server:start`` command to browse to the new route and
+#. Install the :doc:`Symfony CLI ` tool and use the
+ ``symfony server:start`` command to browse to the new route and
see if the bug appears or not.
#. If you can see the bug, you're done and you can already share the code with us.
#. If you can't see the bug, you must keep making small changes. For example, if
diff --git a/contributing/code_of_conduct/code_of_conduct.rst b/contributing/code_of_conduct/code_of_conduct.rst
index 6202fdad424..ce14dd5ad0e 100644
--- a/contributing/code_of_conduct/code_of_conduct.rst
+++ b/contributing/code_of_conduct/code_of_conduct.rst
@@ -34,7 +34,7 @@ Examples of unacceptable behavior include:
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
-* Publishing others’ private information, such as a physical or email address,
+* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
@@ -128,7 +128,7 @@ Attribution
This Code of Conduct is adapted from the `Contributor Covenant`_, version 2.1,
available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
-Community Impact Guidelines were inspired by `Mozilla’s code of conduct enforcement ladder`_.
+Community Impact Guidelines were inspired by `Mozilla's code of conduct enforcement ladder`_.
Related Documents
-----------------
@@ -141,4 +141,4 @@ Related Documents
concrete_example_document
.. _Contributor Covenant: https://www.contributor-covenant.org
-.. _Mozilla’s code of conduct enforcement ladder: https://github.com/mozilla/diversity
+.. _Mozilla's code of conduct enforcement ladder: https://github.com/mozilla/diversity
diff --git a/contributing/code_of_conduct/concrete_example_document.rst b/contributing/code_of_conduct/concrete_example_document.rst
index 60ffe2527db..227a41df4a8 100644
--- a/contributing/code_of_conduct/concrete_example_document.rst
+++ b/contributing/code_of_conduct/concrete_example_document.rst
@@ -9,7 +9,7 @@ according to the Symfony code of conduct.
Concrete Examples
-----------------
-* Unwelcome comments regarding a person’s lifestyle choices and practices,
+* Unwelcome comments regarding a person's lifestyle choices and practices,
including those related to food, health, parenting, drugs, and employment;
* Deliberate misgendering or use of `dead names`_ (The birth name
of a person who has since changed their name, often a transgender person);
diff --git a/contributing/community/review-comments.rst b/contributing/community/review-comments.rst
index 5b9bc932205..331352bb5fd 100644
--- a/contributing/community/review-comments.rst
+++ b/contributing/community/review-comments.rst
@@ -28,7 +28,7 @@ constructive, respectful and helpful reviews and replies.
welcoming place for everyone. **You are free to disagree with
someone's opinions, but don't be disrespectful.**
-It’s important to accept that many programming decisions are opinions.
+It's important to accept that many programming decisions are opinions.
Discuss trade-offs, which you prefer, and reach a resolution quickly.
It's not about being right or wrong, but using what works.
diff --git a/contributing/core_team.rst b/contributing/core_team.rst
index f895dcd00d8..932cc390d60 100644
--- a/contributing/core_team.rst
+++ b/contributing/core_team.rst
@@ -101,7 +101,8 @@ Active Core Members
* **Berislav Balogović** (`hypemc`_);
* **Mathias Arlaud** (`mtarld`_);
* **Florent Morselli** (`spomky`_);
- * **Alexandre Daubois** (`alexandre-daubois`_).
+ * **Alexandre Daubois** (`alexandre-daubois`_);
+ * **Christopher Hertel** (`chr-hertel`_).
* **Security Team** (``@symfony/security`` on GitHub):
@@ -197,7 +198,7 @@ Pull Request Merging Policy
A pull request **can be merged** if:
-* It is a :ref:`unsubstantial change `;
+* It is an :ref:`unsubstantial change `;
* Enough time was given for peer reviews;
* It is a bug fix and at least two **Mergers Team** members voted ``+1``
(only one if the submitter is part of the Mergers team) and no Core
@@ -254,7 +255,7 @@ generate the CHANGELOG files when releasing new versions.
#. A clone for their own contributions, which they use to push to their
fork on GitHub. Clear out the push URL for the Symfony repository using
``git remote set-url --push origin dev://null`` (change ``origin``
- to the Git remote poiting to the Symfony repository);
+ to the Git remote pointing to the Symfony repository);
#. A clone for merging, which they use in combination with ``gh`` and
allows them to push to the main repository.
@@ -378,4 +379,5 @@ discretion of the **Project Leader**.
.. _`spomky`: https://github.com/spomky/
.. _`alexandre-daubois`: https://github.com/alexandre-daubois/
.. _`tucksaun`: https://github.com/tucksaun/
+.. _`chr-hertel`: https://github.com/chr-hertel/
.. _`the releases page`: https://symfony.com/releases
diff --git a/contributing/diversity/further_reading.rst b/contributing/diversity/further_reading.rst
index 8bb07c39c97..b5f44047159 100644
--- a/contributing/diversity/further_reading.rst
+++ b/contributing/diversity/further_reading.rst
@@ -9,7 +9,7 @@ Diversity in Open Source
`Sage Sharp - What makes a good community? `_
`Ashe Dryden - The Ethics of Unpaid Labor and the OSS Community `_
`Model View Culture - The Dehumanizing Myth of the Meritocracy `_
-`Annalee - How “Good Intent” Undermines Diversity and Inclusion `_
+`Annalee - How "Good Intent" Undermines Diversity and Inclusion `_
`Karolina Szczur - Building Inclusive Communities `_
Code of Conduct
@@ -22,7 +22,7 @@ Code of Conduct
Inclusive language
------------------
-`Jenée Desmond-Harris - Why I’m finally convinced it's time to stop saying "you guys" `_
+`Jenée Desmond-Harris - Why I'm finally convinced it's time to stop saying "you guys" `_
`inclusive language presentations `_
Other talks and Blog Posts
@@ -30,7 +30,7 @@ Other talks and Blog Posts
`Lena Reinhard – A Talk About Nothing `_
`Lena Reinhard - A Talk about Everything `_
-`Sage Sharp - SCALE: Improving Diversity with Maslow’s hierarchy `_
+`Sage Sharp - SCALE: Improving Diversity with Maslow's hierarchy `_
`UCSF - Unconscious Bias `_
`Responding to harassment reports `_
`Unconscious bias at work `_
diff --git a/controller.rst b/controller.rst
index 05abdaee4ea..5b0b77b35b9 100644
--- a/controller.rst
+++ b/controller.rst
@@ -41,7 +41,7 @@ class::
The controller is the ``number()`` method, which lives inside the
controller class ``LuckyController``.
-This controller is pretty straightforward:
+This controller is quite simple:
* *line 2*: Symfony takes advantage of PHP's namespace functionality to
namespace the entire controller class.
diff --git a/controller/error_pages.rst b/controller/error_pages.rst
index 96856764ece..06087837437 100644
--- a/controller/error_pages.rst
+++ b/controller/error_pages.rst
@@ -154,7 +154,8 @@ automatically when installing ``symfony/framework-bundle``):
# config/routes/framework.yaml
when@dev:
_errors:
- resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
+ resource: '@FrameworkBundle/Resources/config/routing/errors.php'
+ type: php
prefix: /_error
.. code-block:: xml
@@ -167,7 +168,7 @@ automatically when installing ``symfony/framework-bundle``):
https://symfony.com/schema/routing/routing-1.0.xsd">
-
+
@@ -178,7 +179,7 @@ automatically when installing ``symfony/framework-bundle``):
return function (RoutingConfigurator $routes): void {
if ('dev' === $routes->env()) {
- $routes->import('@FrameworkBundle/Resources/config/routing/errors.xml')
+ $routes->import('@FrameworkBundle/Resources/config/routing/errors.php', 'php')
->prefix('/_error')
;
}
@@ -191,6 +192,11 @@ need to replace ``http://localhost/`` by the host used in your local setup):
* ``http://localhost/_error/{statusCode}`` for HTML
* ``http://localhost/_error/{statusCode}.{format}`` for any other format
+.. versionadded:: 7.3
+
+ The ``errors.php`` file was introduced in Symfony 7.3.
+ Previously, you had to import ``errors.xml``
+
.. _overriding-non-html-error-output:
Overriding Error output for non-HTML formats
diff --git a/controller/service.rst b/controller/service.rst
index 88af093ff29..cf83e066a19 100644
--- a/controller/service.rst
+++ b/controller/service.rst
@@ -7,11 +7,65 @@ and your controllers extend the `AbstractController`_ class, they *are* automati
registered as services. This means you can use dependency injection like any
other normal service.
-If your controllers don't extend the `AbstractController`_ class, you must
-explicitly mark your controller services as ``public``. Alternatively, you can
-apply the ``controller.service_arguments`` tag to your controller services. This
-will make the tagged services ``public`` and will allow you to inject services
-in method parameters:
+If you prefer to not extend the ``AbstractController`` class, you can register
+your controllers as services in several ways:
+
+#. Using the ``#[Route]`` attribute;
+#. Using the ``#[AsController]`` attribute;
+#. Using the ``controller.service_arguments`` service tag.
+
+Using the ``#[Route]`` Attribute
+--------------------------------
+
+When using :ref:`the #[Route] attribute ` to define
+routes on any PHP class, Symfony treats that class as a controller. It registers
+it as a public, non-lazy service and enables service argument injection in all
+its methods.
+
+This is the simplest and recommended way to register controllers as services
+when not extending the base controller class.
+
+.. versionadded:: 7.3
+
+ The feature to register controllers as services when using the ``#[Route]``
+ attribute was introduced in Symfony 7.3.
+
+Using the ``#[AsController]`` Attribute
+---------------------------------------
+
+If you prefer, you can use the ``#[AsController]`` PHP attribute to automatically
+apply the ``controller.service_arguments`` tag to your controller services::
+
+ // src/Controller/HelloController.php
+ namespace App\Controller;
+
+ use Symfony\Component\HttpFoundation\Response;
+ use Symfony\Component\HttpKernel\Attribute\AsController;
+ use Symfony\Component\Routing\Attribute\Route;
+
+ #[AsController]
+ class HelloController
+ {
+ #[Route('/hello', name: 'hello', methods: ['GET'])]
+ public function index(): Response
+ {
+ // ...
+ }
+ }
+
+.. tip::
+
+ When using the ``#[Route]`` attribute, Symfony already registers the controller
+ class as a service, so using the ``#[AsController]`` attribute is redundant.
+
+Using the ``controller.service_arguments`` Service Tag
+------------------------------------------------------
+
+If your controllers don't extend the `AbstractController`_ class and you don't
+use the ``#[AsController]`` or ``#[Route]`` attributes, you must register the
+controllers as public services manually and apply the ``controller.service_arguments``
+:doc:`service tag ` to enable service injection in
+controller actions:
.. configuration-block::
@@ -58,26 +112,6 @@ in method parameters:
calls:
- [setContainer, ['@abstract_controller.locator']]
-If you prefer, you can use the ``#[AsController]`` PHP attribute to automatically
-apply the ``controller.service_arguments`` tag to your controller services::
-
- // src/Controller/HelloController.php
- namespace App\Controller;
-
- use Symfony\Component\HttpFoundation\Response;
- use Symfony\Component\HttpKernel\Attribute\AsController;
- use Symfony\Component\Routing\Attribute\Route;
-
- #[AsController]
- class HelloController
- {
- #[Route('/hello', name: 'hello', methods: ['GET'])]
- public function index(): Response
- {
- // ...
- }
- }
-
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.
diff --git a/controller/upload_file.rst b/controller/upload_file.rst
index cff326a8e2b..793cd26dd65 100644
--- a/controller/upload_file.rst
+++ b/controller/upload_file.rst
@@ -77,11 +77,8 @@ so Symfony doesn't try to get/set its value from the related entity::
'constraints' => [
new File(
maxSize: '1024k',
- mimeTypes: [
- 'application/pdf',
- 'application/x-pdf',
- ],
- mimeTypesMessage: 'Please upload a valid PDF document',
+ extensions: ['pdf'],
+ extensionsMessage: 'Please upload a valid PDF document',
)
],
])
diff --git a/controller/value_resolver.rst b/controller/value_resolver.rst
index 1844ff0c9be..835edcfbff9 100644
--- a/controller/value_resolver.rst
+++ b/controller/value_resolver.rst
@@ -281,7 +281,7 @@ 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
+ // src/ValueResolver/BookingIdValueResolver.php
namespace App\ValueResolver;
use App\IdentifierInterface;
@@ -333,6 +333,20 @@ but you can set it yourself to change its ``priority`` or ``name`` attributes.
.. configuration-block::
+ .. code-block:: php-attributes
+
+ // src/ValueResolver/BookingIdValueResolver.php
+ namespace App\ValueResolver;
+
+ use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
+ use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
+
+ #[AsTaggedItem(index: 'booking_id', priority: 150)]
+ class BookingIdValueResolver implements ValueResolverInterface
+ {
+ // ...
+ }
+
.. code-block:: yaml
# config/services.yaml
@@ -414,7 +428,7 @@ 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
+ // src/ValueResolver/BookingIdValueResolver.php
namespace App\ValueResolver;
use Symfony\Component\HttpKernel\Attribute\AsTargetedValueResolver;
diff --git a/create_framework/dependency_injection.rst b/create_framework/dependency_injection.rst
index de3c4e11e4e..aa377a77b5a 100644
--- a/create_framework/dependency_injection.rst
+++ b/create_framework/dependency_injection.rst
@@ -10,7 +10,6 @@ to it::
namespace Simplex;
use Symfony\Component\EventDispatcher\EventDispatcher;
- use Symfony\Component\HttpFoundation;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel;
use Symfony\Component\Routing;
@@ -199,6 +198,7 @@ Now, here is how you can register a custom listener in the front controller::
// ...
use Simplex\StringResponseListener;
+ use Symfony\Component\DependencyInjection\Reference;
$container->register('listener.string_response', StringResponseListener::class);
$container->getDefinition('dispatcher')
@@ -227,16 +227,16 @@ object::
$container->setParameter('charset', 'UTF-8');
Instead of relying on the convention that the routes are defined by the
-``$routes`` variables, let's use a parameter again::
+``$routes`` variables, let's use a reference::
// ...
$container->register('matcher', Routing\Matcher\UrlMatcher::class)
- ->setArguments(['%routes%', new Reference('context')])
+ ->setArguments([new Reference('routes'), new Reference('context')])
;
And the related change in the front controller::
- $container->setParameter('routes', include __DIR__.'/../src/app.php');
+ $container->set('routes', $routes);
We have barely scratched the surface of what you can do with the
container: from class names as parameters, to overriding existing object
diff --git a/create_framework/event_dispatcher.rst b/create_framework/event_dispatcher.rst
index 650e4c7554e..9a3a48942ac 100644
--- a/create_framework/event_dispatcher.rst
+++ b/create_framework/event_dispatcher.rst
@@ -121,7 +121,7 @@ the registration of a listener for the ``response`` event::
$response = $event->getResponse();
if ($response->isRedirection()
- || ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html'))
+ || ($response->headers->has('Content-Type') && !str_contains($response->headers->get('Content-Type'), 'html'))
|| 'html' !== $event->getRequest()->getRequestFormat()
) {
return;
@@ -200,7 +200,7 @@ Let's refactor the code a bit by moving the Google listener to its own class::
$response = $event->getResponse();
if ($response->isRedirection()
- || ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html'))
+ || ($response->headers->has('Content-Type') && !str_contains($response->headers->get('Content-Type'), 'html'))
|| 'html' !== $event->getRequest()->getRequestFormat()
) {
return;
diff --git a/create_framework/front_controller.rst b/create_framework/front_controller.rst
index fded71a7b1c..cc440dd8910 100644
--- a/create_framework/front_controller.rst
+++ b/create_framework/front_controller.rst
@@ -154,7 +154,7 @@ Now, configure your web server root directory to point to ``web/`` and all
other files will no longer be accessible from the client.
To test your changes in a browser (``http://localhost:4321/hello?name=Fabien``),
-run the :doc:`Symfony Local Web Server `:
+run the :ref:`Symfony local web server `:
.. code-block:: terminal
diff --git a/create_framework/http_foundation.rst b/create_framework/http_foundation.rst
index 219119164b4..71146b1785c 100644
--- a/create_framework/http_foundation.rst
+++ b/create_framework/http_foundation.rst
@@ -189,7 +189,7 @@ fingertips thanks to a nice and simple API::
// retrieves a COOKIE value
$request->cookies->get('PHPSESSID');
- // retrieves a HTTP request header, with normalized, lowercase keys
+ // retrieves an 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 1c2857c9ed9..6c7e469da27 100644
--- a/create_framework/http_kernel_controller_resolver.rst
+++ b/create_framework/http_kernel_controller_resolver.rst
@@ -165,15 +165,6 @@ Let's conclude with the new version of our framework::
use Symfony\Component\HttpKernel;
use Symfony\Component\Routing;
- function render_template(Request $request): Response
- {
- extract($request->attributes->all(), EXTR_SKIP);
- ob_start();
- include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
-
- return new Response(ob_get_clean());
- }
-
$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';
diff --git a/create_framework/http_kernel_httpkernel_class.rst b/create_framework/http_kernel_httpkernel_class.rst
index ecf9d4c7879..158de638f8a 100644
--- a/create_framework/http_kernel_httpkernel_class.rst
+++ b/create_framework/http_kernel_httpkernel_class.rst
@@ -39,7 +39,6 @@ And the new front controller::
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
- use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel;
use Symfony\Component\Routing;
diff --git a/create_framework/introduction.rst b/create_framework/introduction.rst
index 7a1e6b2ad50..420537a8088 100644
--- a/create_framework/introduction.rst
+++ b/create_framework/introduction.rst
@@ -101,7 +101,7 @@ start with the simplest web application we can think of in PHP::
printf('Hello %s', $name);
-You can use the :doc:`Symfony Local Web Server ` to test
+You can use the :ref:`Symfony local web server ` to test
this great application in a browser
(``http://localhost:8000/index.php?name=Fabien``):
diff --git a/create_framework/unit_testing.rst b/create_framework/unit_testing.rst
index 32c97a03846..55220dad31f 100644
--- a/create_framework/unit_testing.rst
+++ b/create_framework/unit_testing.rst
@@ -12,25 +12,25 @@ using `PHPUnit`_. At first, install PHPUnit as a development dependency:
.. code-block:: terminal
- $ composer require --dev phpunit/phpunit:^9.6
+ $ composer require --dev phpunit/phpunit:^11.0
-Then, create a PHPUnit configuration file in ``example.com/phpunit.xml.dist``:
+Then, create a PHPUnit configuration file in ``example.com/phpunit.dist.xml``:
.. code-block:: xml
-
+
./src
-
+
@@ -103,12 +103,12 @@ We are now ready to write our first test::
$matcher
->expects($this->once())
->method('match')
- ->will($this->throwException($exception))
+ ->willThrowException($exception)
;
$matcher
->expects($this->once())
->method('getContext')
- ->will($this->returnValue($this->createMock(Routing\RequestContext::class)))
+ ->willReturn($this->createMock(Routing\RequestContext::class))
;
$controllerResolver = $this->createMock(ControllerResolverInterface::class);
$argumentResolver = $this->createMock(ArgumentResolverInterface::class);
@@ -161,16 +161,16 @@ Response::
$matcher
->expects($this->once())
->method('match')
- ->will($this->returnValue([
+ ->willReturn([
'_route' => 'is_leap_year/{year}',
'year' => '2000',
'_controller' => [new LeapYearController(), 'index'],
- ]))
+ ])
;
$matcher
->expects($this->once())
->method('getContext')
- ->will($this->returnValue($this->createMock(Routing\RequestContext::class)))
+ ->willReturn($this->createMock(Routing\RequestContext::class))
;
$controllerResolver = new ControllerResolver();
$argumentResolver = new ArgumentResolver();
@@ -194,7 +194,7 @@ coverage feature (you need to enable `XDebug`_ first):
$ ./vendor/bin/phpunit --coverage-html=cov/
-Open ``example.com/cov/src/Simplex/Framework.php.html`` in a browser and check
+Open ``example.com/cov/Simplex/Framework.php.html`` in a browser and check
that all the lines for the Framework class are green (it means that they have
been visited when the tests were executed).
@@ -212,6 +212,6 @@ Symfony code.
Now that we are confident (again) about the code we have written, we can
safely think about the next batch of features we want to add to our framework.
-.. _`PHPUnit`: https://docs.phpunit.de/en/9.6/
-.. _`test doubles`: https://docs.phpunit.de/en/9.6/test-doubles.html
+.. _`PHPUnit`: https://docs.phpunit.de/en/11.0/
+.. _`test doubles`: https://docs.phpunit.de/en/11.0/test-doubles.html
.. _`XDebug`: https://xdebug.org/
diff --git a/deployment.rst b/deployment.rst
index 07187f53cba..b9d985920b5 100644
--- a/deployment.rst
+++ b/deployment.rst
@@ -5,7 +5,7 @@ How to Deploy a Symfony Application
Deploying a Symfony application can be a complex and varied task depending on
the setup and the requirements of your application. This article is not a
-step-by-step guide, but is a general list of the most common requirements and
+step-by-step guide, but rather a general list of the most common requirements and
ideas for deployment.
.. _symfony2-deployment-basics:
diff --git a/doctrine.rst b/doctrine.rst
index 171f8a3348a..6a1438322fa 100644
--- a/doctrine.rst
+++ b/doctrine.rst
@@ -646,8 +646,22 @@ automatically! You can simplify the controller to::
}
}
-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.
+That's it! The attribute uses the ``{id}`` from the route to query for the ``Product``
+by the ``id`` column. If it's not found, a 404 error is thrown.
+
+You can change this behavior by making the controller argument optional. In that
+case, no 404 is thrown automatically and you're free to handle the missing entity
+yourself::
+
+ #[Route('/product/{id}')]
+ public function show(?Product $product): Response
+ {
+ if (null === $product) {
+ // run your own logic to return a custom response
+ }
+
+ // ...
+ }
.. tip::
@@ -680,7 +694,7 @@ will automatically fetch them::
/**
* Perform a findOneBy() where the slug property matches {slug}.
*/
- #[Route('/product/{slug}')]
+ #[Route('/product/{slug:product}')]
public function showBySlug(Product $product): Response
{
}
@@ -694,14 +708,17 @@ Automatic fetching works in these situations:
*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``.
+The ``{slug:product}`` syntax maps the route parameter named ``slug`` to the
+controller argument named ``$product``. It also hints the resolver to look up
+the corresponding ``Product`` object from the database using the slug.
-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`_ ::
+.. versionadded:: 7.1
+
+ Route parameter mapping was introduced in Symfony 7.1.
+
+You can also configure the mapping explicitly for any controller argument
+using the ``MapEntity`` attribute. You can even control the behavior of the
+``EntityValueResolver`` by using the `MapEntity options`_ ::
// src/Controller/ProductController.php
namespace App\Controller;
@@ -781,6 +798,32 @@ variable. Let's say you want the first or the last comment of a product dependin
): Response {
}
+.. _doctrine-entity-value-resolver-resolve-target-entities:
+
+Fetch via Interfaces
+~~~~~~~~~~~~~~~~~~~~
+
+Suppose your ``Product`` class implements an interface called ``ProductInterface``.
+If you want to decouple your controllers from the concrete entity implementation,
+you can reference the entity by its interface instead.
+
+To enable this, first configure the
+:doc:`resolve_target_entities option `.
+Then, your controller can type-hint the interface, and the entity will be
+resolved automatically::
+
+ public function show(
+ #[MapEntity]
+ ProductInterface $product
+ ): Response {
+ // ...
+ }
+
+.. versionadded:: 7.3
+
+ Support for target entity resolution in the ``EntityValueResolver`` was
+ introduced Symfony 7.3
+
MapEntity Options
~~~~~~~~~~~~~~~~~
@@ -812,18 +855,6 @@ control behavior:
): 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.
diff --git a/doctrine/associations.rst b/doctrine/associations.rst
index 8dd9aa7f36b..bb670eeee52 100644
--- a/doctrine/associations.rst
+++ b/doctrine/associations.rst
@@ -447,7 +447,7 @@ by adding JOINs.
$category = $product->getCategory();
// prints "Proxies\AppEntityCategoryProxy"
- dump(get_class($category));
+ dump($category::class);
die();
This proxy object extends the true ``Category`` object, and looks and
@@ -501,7 +501,7 @@ following method to the ``ProductRepository`` class::
}
}
-This will *still* return an array of ``Product`` objects. But now, when you call
+This will *still* return a ``Product`` object. But now, when you call
``$product->getCategory()`` and use that data, no second query is made.
Now, you can use this method in your controller to query for a ``Product``
diff --git a/doctrine/events.rst b/doctrine/events.rst
index 929f44b915e..accf424083a 100644
--- a/doctrine/events.rst
+++ b/doctrine/events.rst
@@ -304,23 +304,6 @@ listener in the Symfony application by creating a new service for it and
.. 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
diff --git a/doctrine/resolve_target_entity.rst b/doctrine/resolve_target_entity.rst
index 5ae6475a957..1495f475628 100644
--- a/doctrine/resolve_target_entity.rst
+++ b/doctrine/resolve_target_entity.rst
@@ -1,39 +1,45 @@
-How to Define Relationships with Abstract Classes and Interfaces
-================================================================
+Referencing Entities with Abstract Classes and Interfaces
+=========================================================
-One of the goals of bundles is to create discrete bundles of functionality
-that do not have many (if any) dependencies, allowing you to use that
-functionality in other applications without including unnecessary items.
+In applications where functionality is organized in layers or modules with
+minimal concrete dependencies, such as monoliths split into multiple modules,
+it can be challenging to avoid tight coupling between entities.
-Doctrine 2.2 includes a new utility called the ``ResolveTargetEntityListener``,
-that functions by intercepting certain calls inside Doctrine and rewriting
-``targetEntity`` parameters in your metadata mapping at runtime. It means that
-in your bundle you are able to use an interface or abstract class in your
-mappings and expect correct mapping to a concrete entity at runtime.
+Doctrine provides a utility called the ``ResolveTargetEntityListener`` to solve
+this issue. It works by intercepting certain calls within Doctrine and rewriting
+``targetEntity`` parameters in your metadata mapping at runtime. This allows you
+to reference an interface or abstract class in your mappings and have it resolved
+to a concrete entity at runtime.
-This functionality allows you to define relationships between different entities
-without making them hard dependencies.
+This makes it possible to define relationships between entities without
+creating hard dependencies. This feature also works with the ``EntityValueResolver``
+:ref:`as explained in the main Doctrine article `.
+
+.. versionadded:: 7.3
+
+ Support for target entity resolution in the ``EntityValueResolver`` was
+ introduced Symfony 7.3
Background
----------
-Suppose you have an InvoiceBundle which provides invoicing functionality
-and a CustomerBundle that contains customer management tools. You want
-to keep these separated, because they can be used in other systems without
-each other, but for your application you want to use them together.
+Suppose you have an application with two modules: an Invoice module that
+provides invoicing functionality, and a Customer module that handles customer
+management. You want to keep these modules decoupled, so that neither is aware
+of the other's implementation details.
-In this case, you have an ``Invoice`` entity with a relationship to a
-non-existent object, an ``InvoiceSubjectInterface``. The goal is to get
-the ``ResolveTargetEntityListener`` to replace any mention of the interface
-with a real object that implements that interface.
+In this case, your ``Invoice`` entity has a relationship to the interface
+``InvoiceSubjectInterface``. Since interfaces are not valid Doctrine entities,
+the goal is to use the ``ResolveTargetEntityListener`` to replace all
+references to this interface with a concrete class that implements it.
Set up
------
-This article uses the following two basic entities (which are incomplete for
-brevity) to explain how to set up and use the ``ResolveTargetEntityListener``.
+This article uses two basic (incomplete) entities to demonstrate how to set up
+and use the ``ResolveTargetEntityListener``.
-A Customer entity::
+A ``Customer`` entity::
// src/Entity/Customer.php
namespace App\Entity;
@@ -50,7 +56,7 @@ A Customer entity::
// are already implemented in the BaseCustomer
}
-An Invoice entity::
+An ``Invoice`` entity::
// src/Entity/Invoice.php
namespace App\Entity;
@@ -58,9 +64,6 @@ An Invoice entity::
use App\Model\InvoiceSubjectInterface;
use Doctrine\ORM\Mapping as ORM;
- /**
- * Represents an Invoice.
- */
#[ORM\Entity]
#[ORM\Table(name: 'invoice')]
class Invoice
@@ -69,7 +72,7 @@ An Invoice entity::
protected InvoiceSubjectInterface $subject;
}
-An InvoiceSubjectInterface::
+The interface representing the subject used in the invoice::
// src/Model/InvoiceSubjectInterface.php
namespace App\Model;
@@ -89,8 +92,8 @@ An InvoiceSubjectInterface::
public function getName(): string;
}
-Next, you need to configure the listener, which tells the DoctrineBundle
-about the replacement:
+Now configure the ``resolve_target_entities`` option to tell Doctrine
+how to replace the interface with the concrete class:
.. configuration-block::
@@ -140,7 +143,6 @@ about the replacement:
Final Thoughts
--------------
-With the ``ResolveTargetEntityListener``, you are able to decouple your
-bundles, keeping them usable by themselves, but still being able to
-define relationships between different objects. By using this method,
-your bundles will end up being easier to maintain independently.
+Using ``ResolveTargetEntityListener`` allows you to decouple your modules
+while still defining relationships between their entities. This makes your
+codebase more modular and easier to maintain over time.
diff --git a/event_dispatcher.rst b/event_dispatcher.rst
index d9b913ed49f..ffa9e67aa0d 100644
--- a/event_dispatcher.rst
+++ b/event_dispatcher.rst
@@ -149,7 +149,7 @@ Defining Event Listeners with PHP Attributes
An alternative way to define an event listener is to use the
:class:`Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener`
-PHP attribute. This allows to configure the listener inside its class, without
+PHP attribute. This allows you to configure the listener inside its class, without
having to add any configuration in external files::
namespace App\EventListener;
@@ -509,7 +509,7 @@ A ``kernel.controller`` (aka ``KernelEvents::CONTROLLER``) listener gets notifie
on *every* request, right before the controller is executed. So, first, you need
some way to identify if the controller that matches the request needs token validation.
-A clean and easy way is to create an empty interface and make the controllers
+A clean and simple way is to create an empty interface and make the controllers
implement it::
namespace App\Controller;
diff --git a/form/button_based_validation.rst b/form/button_based_validation.rst
deleted file mode 100644
index 47f2673b079..00000000000
--- a/form/button_based_validation.rst
+++ /dev/null
@@ -1,36 +0,0 @@
-How to Choose Validation Groups Based on the Clicked Button
-===========================================================
-
-When your form contains multiple submit buttons, you can change the validation
-group depending on which button is used to submit the form. For example,
-consider a form in a wizard that lets you advance to the next step or go back
-to the previous step. Also assume that when returning to the previous step,
-the data of the form should be saved, but not validated.
-
-First, we need to add the two buttons to the form::
-
- $form = $this->createFormBuilder($task)
- // ...
- ->add('nextStep', SubmitType::class)
- ->add('previousStep', SubmitType::class)
- ->getForm();
-
-Then, we configure the button for returning to the previous step to run
-specific validation groups. In this example, we want it to suppress validation,
-so we set its ``validation_groups`` option to false::
-
- $form = $this->createFormBuilder($task)
- // ...
- ->add('previousStep', SubmitType::class, [
- 'validation_groups' => false,
- ])
- ->getForm();
-
-Now the form will skip your validation constraints. It will still validate
-basic integrity constraints, such as checking whether an uploaded file was too
-large or whether you tried to submit text in a number field.
-
-.. seealso::
-
- To see how to use a service to resolve ``validation_groups`` dynamically
- read the :doc:`/form/validation_group_service_resolver` article.
diff --git a/form/data_based_validation.rst b/form/data_based_validation.rst
deleted file mode 100644
index b01bea10b16..00000000000
--- a/form/data_based_validation.rst
+++ /dev/null
@@ -1,72 +0,0 @@
-How to Choose Validation Groups Based on the Submitted Data
-===========================================================
-
-If you need some advanced logic to determine the validation groups (e.g.
-based on submitted data), you can set the ``validation_groups`` option
-to an array callback::
-
- use App\Entity\Client;
- use Symfony\Component\OptionsResolver\OptionsResolver;
-
- // ...
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'validation_groups' => [
- Client::class,
- 'determineValidationGroups',
- ],
- ]);
- }
-
-This will call the static method ``determineValidationGroups()`` on the
-``Client`` class after the form is submitted, but before validation is
-invoked. The Form object is passed as an argument to that method (see next
-example). You can also define whole logic inline by using a ``Closure``::
-
- use App\Entity\Client;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\OptionsResolver\OptionsResolver;
-
- // ...
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'validation_groups' => function (FormInterface $form): array {
- $data = $form->getData();
-
- if (Client::TYPE_PERSON == $data->getType()) {
- return ['person'];
- }
-
- return ['company'];
- },
- ]);
- }
-
-Using the ``validation_groups`` option overrides the default validation
-group which is being used. If you want to validate the default constraints
-of the entity as well you have to adjust the option as follows::
-
- use App\Entity\Client;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\OptionsResolver\OptionsResolver;
-
- // ...
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'validation_groups' => function (FormInterface $form): array {
- $data = $form->getData();
-
- if (Client::TYPE_PERSON == $data->getType()) {
- return ['Default', 'person'];
- }
-
- return ['Default', 'company'];
- },
- ]);
- }
-
-You can find more information about how the validation groups and the default constraints
-work in the article about :doc:`validation groups `.
diff --git a/form/form_collections.rst b/form/form_collections.rst
index 2a0ba99657f..3c8a2050690 100644
--- a/form/form_collections.rst
+++ b/form/form_collections.rst
@@ -699,13 +699,12 @@ the relationship between the removed ``Tag`` and ``Task`` object.
The Symfony community has created some JavaScript packages that provide the
functionality needed to add, edit and delete elements of the collection.
- Check out the `@a2lix/symfony-collection`_ package for modern browsers and
- the `symfony-collection`_ package based on jQuery for the rest of browsers.
+ Check out the `@a2lix/symfony-collection`_ or search on GitHub for other
+ recent packages.
.. _`Owning Side and Inverse Side`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/unitofwork-associations.html
.. _`JSFiddle`: https://jsfiddle.net/ey8ozh6n/
.. _`@a2lix/symfony-collection`: https://github.com/a2lix/symfony-collection
-.. _`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/inherit_data_option.rst b/form/inherit_data_option.rst
index 2caa0afcdbe..8067e932c5a 100644
--- a/form/inherit_data_option.rst
+++ b/form/inherit_data_option.rst
@@ -29,7 +29,7 @@ entities, a ``Company`` and a ``Customer``::
private string $firstName;
private string $lastName;
- private string $address;
+ private string $address;
private string $zipcode;
private string $city;
private string $country;
diff --git a/form/validation_group_service_resolver.rst b/form/validation_group_service_resolver.rst
deleted file mode 100644
index 82a6f65d6ec..00000000000
--- a/form/validation_group_service_resolver.rst
+++ /dev/null
@@ -1,58 +0,0 @@
-How to Dynamically Configure Form Validation Groups
-===================================================
-
-Sometimes you need advanced logic to determine the validation groups. If they
-can't be determined by a callback, you can use a service. Create a service
-that implements ``__invoke()`` which accepts a ``FormInterface`` as a
-parameter::
-
- // src/Validation/ValidationGroupResolver.php
- namespace App\Validation;
-
- use Symfony\Component\Form\FormInterface;
-
- class ValidationGroupResolver
- {
- public function __construct(
- private object $service1,
- private object $service2,
- ) {
- }
-
- public function __invoke(FormInterface $form): array
- {
- $groups = [];
-
- // ... determine which groups to apply and return an array
-
- return $groups;
- }
- }
-
-Then in your form, inject the resolver and set it as the ``validation_groups``::
-
- // src/Form/MyClassType.php;
- namespace App\Form;
-
- use App\Validation\ValidationGroupResolver;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\OptionsResolver\OptionsResolver;
-
- class MyClassType extends AbstractType
- {
- public function __construct(
- private ValidationGroupResolver $groupResolver,
- ) {
- }
-
- // ...
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'validation_groups' => $this->groupResolver,
- ]);
- }
- }
-
-This will result in the form validator invoking your group resolver to set the
-validation groups returned when validating.
diff --git a/form/validation_groups.rst b/form/validation_groups.rst
index 4addc1ba1a7..3157ef7bc5b 100644
--- a/form/validation_groups.rst
+++ b/form/validation_groups.rst
@@ -1,39 +1,163 @@
-How to Define the Validation Groups to Use
-==========================================
+Configuring Validation Groups in Forms
+======================================
-Validation Groups
------------------
+If the object handled in your form uses :doc:`validation groups `,
+you need to specify which validation group(s) the form should apply.
-If your object takes advantage of :doc:`validation groups `,
-you'll need to specify which validation group(s) your form should use. Pass
-this as an option when :ref:`creating forms in controllers `::
+To define them when :ref:`creating forms in classes `,
+use the ``configureOptions()`` method::
+
+ use Symfony\Component\OptionsResolver\OptionsResolver;
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ // ...
+ 'validation_groups' => ['registration'],
+ ]);
+ }
+
+When :ref:`creating forms in controllers `, pass
+it as a form option::
$form = $this->createFormBuilder($user, [
'validation_groups' => ['registration'],
])->add(/* ... */);
-When :ref:`creating forms in classes `, add the
-following to the ``configureOptions()`` method::
+In both cases, *only* the ``registration`` group will be used to validate the
+object. To apply the ``registration`` group *and* all constraints not in any
+other group, add the special ``Default`` group::
+
+ [
+ // ...
+ 'validation_groups' => ['Default', 'registration'],
+ ]
+.. note::
+
+ You can use any name for your validation groups. Symfony recommends using
+ "lower snake case" (e.g. ``foo_bar``), while automatically generated
+ groups use "UpperCamelCase" (e.g. ``Default``, ``SomeClassName``).
+
+Choosing Validation Groups Based on the Clicked Button
+------------------------------------------------------
+
+When your form has :doc:`multiple submit buttons `, you
+can change the validation group based on the clicked button. For example, in a
+multi-step form like the following, you might want to skip validation when
+returning to a previous step::
+
+ $form = $this->createFormBuilder($task)
+ // ...
+ ->add('nextStep', SubmitType::class)
+ ->add('previousStep', SubmitType::class)
+ ->getForm();
+
+To do so, configure the validation groups of the ``previousStep`` button to
+``false``, which is a special value that skips validation::
+
+ $form = $this->createFormBuilder($task)
+ // ...
+ ->add('previousStep', SubmitType::class, [
+ 'validation_groups' => false,
+ ])
+ ->getForm();
+
+Now the form will skip your validation constraints when that button is clicked.
+It will still validate basic integrity constraints, such as checking whether an
+uploaded file was too large or whether you tried to submit text in a number field.
+
+Choosing Validation Groups Based on Submitted Data
+--------------------------------------------------
+
+To determine validation groups dynamically based on submitted data, use a
+callback. This is called after the form is submitted, but before validation is
+invoked. The callback receives the form object as its first argument::
+
+ use App\Entity\Client;
+ use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
- // ...
- 'validation_groups' => ['registration'],
+ 'validation_groups' => function (FormInterface $form): array {
+ $data = $form->getData();
+
+ if (Client::TYPE_PERSON === $data->getType()) {
+ return ['Default', 'person'];
+ }
+
+ return ['Default', 'company'];
+ },
]);
}
-In both of these cases, *only* the ``registration`` validation group will
-be used to validate the underlying object. To apply the ``registration``
-group *and* all constraints that are not in a group, use::
+.. note::
+
+ Adding ``Default`` to the list of validation groups is common but not mandatory.
+ See the main :doc:`article about validation groups ` to
+ learn more about validation groups and the default constraints.
- 'validation_groups' => ['Default', 'registration']
+You can also pass a static class method callback::
-.. note::
+ 'validation_groups' => [Client::class, 'determineValidationGroups']
+
+Choosing Validation Groups via a Service
+----------------------------------------
+
+If validation group logic requires services or can't fit in a closure, use a
+dedicated validation group resolver service. The class of this service must
+be invokable and receives the form object as its first argument::
+
+ // src/Validation/ValidationGroupResolver.php
+ namespace App\Validation;
+
+ use Symfony\Component\Form\FormInterface;
+
+ class ValidationGroupResolver
+ {
+ public function __construct(
+ private object $service1,
+ private object $service2,
+ ) {
+ }
+
+ public function __invoke(FormInterface $form): array
+ {
+ $groups = [];
+
+ // ... determine which groups to return
+
+ return $groups;
+ }
+ }
+
+Then use the service in your form type::
+
+ namespace App\Form;
+
+ use App\Validation\ValidationGroupResolver;
+ use Symfony\Component\Form\AbstractType;
+ use Symfony\Component\OptionsResolver\OptionsResolver;
+
+ class MyClassType extends AbstractType
+ {
+ public function __construct(
+ private ValidationGroupResolver $groupResolver,
+ ) {
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'validation_groups' => $this->groupResolver,
+ ]);
+ }
+ }
+
+Learn More
+----------
- You can choose any name for your validation groups, but Symfony recommends
- using "lower snake case" names (e.g. ``foo_bar``) in contrast with the
- automatic validation groups created by Symfony, which use "upper camel case"
- (e.g. ``Default``, ``SomeClassName``).
+For more information about how validation groups work, see
+:doc:`/validation/groups`.
diff --git a/form/without_class.rst b/form/without_class.rst
index 5fec7f3a663..c31ff346170 100644
--- a/form/without_class.rst
+++ b/form/without_class.rst
@@ -177,3 +177,41 @@ in your controller::
->add('firstName', TextType::class)
->add('lastName', TextType::class)
->getForm();
+
+Conditional Constraints
+~~~~~~~~~~~~~~~~~~~~~~~
+
+It's possible to define field constraints that depend on the value of other
+fields (e.g. a field must not be blank when another field has a certain value).
+To achieve this, use the ``expression`` option of the
+:doc:`When constraint ` to reference the other field::
+
+ $builder
+ ->add('how_did_you_hear', ChoiceType::class, [
+ 'required' => true,
+ 'label' => 'How did you hear about us?',
+ 'choices' => [
+ 'Search engine' => 'search_engine',
+ 'Friends' => 'friends',
+ 'Other' => 'other',
+ ],
+ 'expanded' => true,
+ 'constraints' => [
+ new Assert\NotBlank(),
+ ]
+ ])
+
+ // this field is only required if the value of the 'how_did_you_hear' field is 'other'
+ ->add('other_text', TextType::class, [
+ 'required' => false,
+ 'label' => 'Please specify',
+ 'constraints' => [
+ new Assert\When(
+ expression: 'this.getParent().get("how_did_you_hear").getData() == "other"',
+ constraints: [
+ new Assert\NotBlank(),
+ ],
+ )
+ ],
+ ])
+ ;
diff --git a/forms.rst b/forms.rst
index 008c60a66c6..83065d7524b 100644
--- a/forms.rst
+++ b/forms.rst
@@ -995,8 +995,6 @@ Validation:
:maxdepth: 1
/form/validation_groups
- /form/validation_group_service_resolver
- /form/button_based_validation
/form/disabling_validation
Misc.:
diff --git a/frontend/asset_mapper.rst b/frontend/asset_mapper.rst
index 8b27cd8ba16..912f645bf6a 100644
--- a/frontend/asset_mapper.rst
+++ b/frontend/asset_mapper.rst
@@ -252,6 +252,26 @@ This adds the ``bootstrap`` package to your ``importmap.php`` file::
`package.json configuration file`_. Try to contact the package maintainer to
ask them to fix those issues.
+.. tip::
+
+ If you see a network error like *Connection was reset for "https://cdn.jsdelivr.net/npm/..."*,
+ it may be caused by a proxy or firewall restriction. In that case, you can
+ temporarily configure a proxy to connect to the ``jsDelivr`` CDN:
+
+ .. code-block:: yaml
+
+ # config/packages/framework.yaml
+ framework:
+ # ...
+ http_client:
+ default_options:
+ proxy: '185.250.180.238:8080'
+ # if you use CURL, add extra options:
+ extra:
+ curl:
+ # 61 is value of constant CURLOPT_HTTPPROXYTUNNEL
+ '61': true
+
Now you can import the ``bootstrap`` package like usual:
.. code-block:: javascript
@@ -281,6 +301,33 @@ You can update your third-party packages to their current versions by running:
$ php bin/console importmap:update bootstrap lodash
$ php bin/console importmap:outdated bootstrap lodash
+Removing JavaScript Packages
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you need to remove a JavaScript package that was previously added to your
+``importmap.php`` file, use the ``importmap:remove`` command. For example, to
+remove the ``lodash`` package:
+
+.. code-block:: terminal
+
+ $ php bin/console importmap:remove lodash
+
+This updates your ``importmap.php`` file and removes the specified package
+(along with any dependencies that were added with it).
+
+After running this command, it's recommended to also run the following to ensure
+that your ``assets/vendor/`` directory is in sync with the updated import map:
+
+.. code-block:: terminal
+
+ $ php bin/console importmap:install
+
+.. tip::
+
+ Removing a package from the import map does not automatically remove any
+ references to it in your JavaScript files. Make sure to update your code and
+ remove any ``import`` statements that reference the removed package.
+
How does the importmap Work?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -720,7 +767,11 @@ Symfony will add a ``Link`` header in the response to preload the CSS files.
Pre-Compressing Assets
----------------------
-Although most servers (Caddy, Nginx, Apache, FrankenPHP) and services like Cloudflare
+.. versionadded:: 7.3
+
+ Support for pre-compressing assets was introduced in Symfony 7.3.
+
+Although most web servers (Caddy, Nginx, Apache, FrankenPHP) and services like Cloudflare
provide asset compression features, AssetMapper also allows you to compress all
your assets before serving them.
@@ -730,7 +781,7 @@ server, which then returns them to the client without wasting CPU resources on
compression.
AssetMapper supports `Brotli`_, `Zstandard`_ and `gzip`_ compression formats.
-Before using any of them, the server that pre-compresses assets must have
+Before using any of them, the machine that pre-compresses assets must have
installed the following PHP extensions or CLI commands:
* Brotli: ``brotli`` CLI command; `brotli PHP extension`_;
@@ -748,7 +799,12 @@ and which file extensions should be compressed:
# ...
precompress:
+ # possible values: 'brotli', 'zstandard', 'gzip'
format: 'zstandard'
+
+ # you can also pass multiple values to generate files in several formats
+ # format: ['brotli', 'zstandard']
+
# if you don't define the following option, AssetMapper will compress all
# the extensions considered safe (css, js, json, svg, xml, ttf, otf, wasm, etc.)
extensions: ['css', 'js', 'json', 'svg', 'xml']
@@ -1146,14 +1202,19 @@ both ``app`` and ``checkout``:
{{ importmap(['app', 'checkout']) }}
{% endblock %}
-By passing both ``app`` and ``checkout``, the ``importmap()`` function will
-output the ``importmap`` and also add a ``
-The ``asset()`` function's main purpose is to make your application more portable.
-If your application lives at the root of your host (e.g. ``https://example.com``),
-then the rendered path should be ``/images/logo.png``. But if your application
-lives in a subdirectory (e.g. ``https://example.com/my_app``), each asset path
-should render with the subdirectory (e.g. ``/my_app/images/logo.png``). The
-``asset()`` function takes care of this by determining how your application is
-being used and generating the correct paths accordingly.
+Using the ``asset()`` function is recommended for these reasons:
-.. tip::
+* **Asset versioning**: ``asset()`` appends a version hash to asset URLs for
+ cache busting. This works both via :doc:`AssetMapper ` and the
+ :doc:`Asset component ` (see also the
+ :ref:`assets configuration options `, such as ``version``
+ and ``version_format``).
- The ``asset()`` function supports various cache busting techniques via the
- :ref:`version `,
- :ref:`version_format `, and
- :ref:`json_manifest_path ` configuration options.
+* **Application portability**: whether your app is hosted at the root
+ (e.g. ``https://example.com``) or in a subdirectory (e.g. ``https://example.com/my_app``),
+ ``asset()`` generates the correct path (e.g. ``/images/logo.png`` vs ``/my_app/images/logo.png``)
+ automatically based on your app's base URL.
If you need absolute URLs for assets, use the ``absolute_url()`` Twig function
as follows:
.. code-block:: html+twig
-
+
@@ -497,8 +495,8 @@ in container parameters `:
.. code-block:: php
// config/packages/twig.php
- use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
use Symfony\Config\TwigConfig;
+ use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
return static function (TwigConfig $twig): void {
// ...
@@ -973,7 +971,7 @@ following code to display the user information is repeated in several places:
{# ... #}
-
+
{{ user.fullName }} - {{ user.email }}
@@ -1258,7 +1256,7 @@ In practice, the ``base.html.twig`` template would look like this:
{% block title %}My Application{% endblock %}
{% block stylesheets %}
-
+
{% endblock %}
@@ -1513,8 +1511,8 @@ Bundle Templates
~~~~~~~~~~~~~~~~
If you :ref:`install packages/bundles ` in your application, they
-may include their own Twig templates (in the ``Resources/views/`` directory of
-each bundle). To avoid messing with your own templates, Symfony adds bundle
+may include their own Twig templates (in the ``templates/`` directory of each
+bundle). To avoid messing with your own templates, Symfony adds bundle
templates under an automatic namespace created after the bundle name.
For example, the templates of a bundle called ``AcmeBlogBundle`` are available
@@ -1553,23 +1551,20 @@ as currency:
{# pass in the 3 optional arguments #}
{{ product.price|price(2, ',', '.') }}
-Create a class that extends ``AbstractExtension`` and fill in the logic::
+.. _templates-twig-filter-attribute:
+
+Create a regular PHP class with a method that contains the filter logic. Then,
+add the ``#[AsTwigFilter]`` attribute to define the name and options of
+the Twig filter::
// src/Twig/AppExtension.php
namespace App\Twig;
- use Twig\Extension\AbstractExtension;
- use Twig\TwigFilter;
+ use Twig\Attribute\AsTwigFilter;
- class AppExtension extends AbstractExtension
+ class AppExtension
{
- public function getFilters(): array
- {
- return [
- new TwigFilter('price', [$this, 'formatPrice']),
- ];
- }
-
+ #[AsTwigFilter('price')]
public function formatPrice(float $number, int $decimals = 0, string $decPoint = '.', string $thousandsSep = ','): string
{
$price = number_format($number, $decimals, $decPoint, $thousandsSep);
@@ -1579,24 +1574,19 @@ Create a class that extends ``AbstractExtension`` and fill in the logic::
}
}
-If you want to create a function instead of a filter, define the
-``getFunctions()`` method::
+.. _templates-twig-function-attribute:
+
+If you want to create a function instead of a filter, use the
+``#[AsTwigFunction]`` attribute::
// src/Twig/AppExtension.php
namespace App\Twig;
- use Twig\Extension\AbstractExtension;
- use Twig\TwigFunction;
+ use Twig\Attribute\AsTwigFunction;
- class AppExtension extends AbstractExtension
+ class AppExtension
{
- public function getFunctions(): array
- {
- return [
- new TwigFunction('area', [$this, 'calculateArea']),
- ];
- }
-
+ #[AsTwigFunction('area')]
public function calculateArea(int $width, int $length): int
{
return $width * $length;
@@ -1608,6 +1598,18 @@ If you want to create a function instead of a filter, define the
Along with custom filters and functions, you can also register
`global variables`_.
+.. versionadded:: 7.3
+
+ Support for the ``#[AsTwigFilter]``, ``#[AsTwigFunction]`` and ``#[AsTwigTest]``
+ attributes was introduced in Symfony 7.3. Previously, you had to extend the
+ ``AbstractExtension`` class, and override the ``getFilters()`` and ``getFunctions()``
+ methods.
+
+If you're using the :ref:`default services.yaml configuration `,
+the :ref:`service autoconfiguration ` feature will enable
+this class as a Twig extension. Otherwise, you need to define a service manually
+and :doc:`tag it ` with the ``twig.attribute_extension`` tag.
+
Register an Extension as a Service
..................................
@@ -1631,10 +1633,11 @@ this command to confirm that your new filter was successfully registered:
Creating Lazy-Loaded Twig Extensions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Including the code of the custom filters/functions in the Twig extension class
-is the simplest way to create extensions. However, Twig must initialize all
-extensions before rendering any template, even if the template doesn't use an
-extension.
+When :ref:`using attributes to extend Twig `,
+the **Twig extensions are already lazy-loaded** and you don't have to do anything
+else. However, if your Twig extensions follow the **legacy approach** of extending
+the ``AbstractExtension`` class, Twig initializes all the extensions before
+rendering any template, even if they are not used.
If extensions don't define dependencies (i.e. if you don't inject services in
them) performance is not affected. However, if extensions define lots of complex
diff --git a/testing.rst b/testing.rst
index 7b73e8f32fc..64ee65ccdf1 100644
--- a/testing.rst
+++ b/testing.rst
@@ -16,7 +16,7 @@ Types of Tests
There are many types of automated tests and precise definitions often
differ from project to project. In Symfony, the following definitions are
used. If you have learned something different, that is not necessarily
-wrong, just different from what the Symfony documentation is using.
+wrong, merely different from what the Symfony documentation is using.
`Unit Tests`_
These tests ensure that *individual* units of source code (e.g. a single
@@ -55,16 +55,16 @@ This command automatically runs your application tests. Each test is a
PHP class ending with "Test" (e.g. ``BlogControllerTest``) that lives in
the ``tests/`` directory of your application.
-PHPUnit is configured by the ``phpunit.xml.dist`` file in the root of your
-application. The default configuration provided by Symfony Flex will be
-enough in most cases. Read the `PHPUnit documentation`_ to discover all
-possible configuration options (e.g. to enable code coverage or to split
-your test into multiple "test suites").
+PHPUnit is configured by the ``phpunit.dist.xml`` file in the root of your
+application (in PHPUnit versions older than 10, the file is named ``phpunit.xml.dist``).
+The default configuration provided by Symfony Flex will be enough in most cases.
+Read the `PHPUnit documentation`_ to discover all possible configuration options
+(e.g. to enable code coverage or to split your test into multiple "test suites").
.. note::
:ref:`Symfony Flex ` automatically creates
- ``phpunit.xml.dist`` and ``tests/bootstrap.php``. If these files are
+ ``phpunit.dist.xml`` and ``tests/bootstrap.php``. If these files are
missing, you can try running the recipe again using
``composer recipes:install phpunit/phpunit --force -v``.
@@ -81,7 +81,7 @@ By convention, the ``tests/`` directory should replicate the directory
of your application for unit tests. So, if you're testing a class in the
``src/Form/`` directory, put the test in the ``tests/Form/`` directory.
Autoloading is automatically enabled via the ``vendor/autoload.php`` file
-(as configured by default in the ``phpunit.xml.dist`` file).
+(as configured by default in the ``phpunit.dist.xml`` file).
You can run tests using the ``bin/phpunit`` command:
@@ -113,7 +113,7 @@ to use the Symfony Kernel to fetch a service from the dependency injection
container.
Symfony provides a :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase`
-class to help you creating and booting the kernel in your tests using
+class to help you create and boot the kernel in your tests using
``bootKernel()``::
// tests/Service/NewsletterGeneratorTest.php
@@ -386,11 +386,14 @@ Now, enable it as a PHPUnit extension:
.. code-block:: xml
-
+
+
+
+
@@ -402,11 +405,11 @@ test finishes to undo all changes. Read more in the documentation of the
.. _doctrine-fixtures:
-Load Dummy Data Fixtures
-........................
+Load Test Data Fixtures
+.......................
Instead of using the real data from the production database, it's common to
-use fake or dummy data in the test database. This is usually called
+use fake or test data in the test database. This is usually called
*"fixtures data"* and Doctrine provides a library to create and load them.
Install it with:
@@ -714,6 +717,29 @@ stores in the session of the test client. If you need to define custom
attributes in this token, you can use the ``tokenAttributes`` argument of the
:method:`Symfony\\Bundle\\FrameworkBundle\\KernelBrowser::loginUser` method.
+You can also use an :ref:`in-memory user ` in your tests
+by instantiating :class:`Symfony\\Component\\Security\\Core\\User\\InMemoryUser` directly::
+
+ // tests/Controller/ProfileControllerTest.php
+ use Symfony\Component\Security\Core\User\InMemoryUser;
+
+ $client = static::createClient();
+ $testUser = new InMemoryUser('admin', 'password', ['ROLE_ADMIN']);
+ $client->loginUser($testUser);
+
+Before doing this, you must define the in-memory user in your test environment
+configuration to ensure it exists and can be authenticated::
+
+.. code-block:: yaml
+
+ # config/packages/security.yaml
+ when@test:
+ security:
+ users_in_memory:
+ memory:
+ users:
+ admin: { password: password, roles: ROLE_ADMIN }
+
To set a specific firewall (``main`` is set by default)::
$client->loginUser($testUser, 'my_firewall');
@@ -734,8 +760,10 @@ a shortcut to make AJAX requests::
// the required HTTP_X_REQUESTED_WITH header is added automatically
$client->xmlHttpRequest('POST', '/submit', ['name' => 'Fabien']);
-Sending Custom Headers
-......................
+.. _sending-custom-headers:
+
+Sending Custom HTTP Headers
+...........................
If your application behaves according to some HTTP headers, pass them as the
second argument of ``createClient()``::
@@ -864,8 +892,8 @@ Use the ``submitForm()`` method to submit the form that contains the given butto
'comment_form[content]' => '...',
]);
-The first argument of ``submitForm()`` is the text content, ``id``, ``value`` or
-``name`` of any ```` or `` `` included in the form.
+The first argument of ``submitForm()`` is the text content, ``id`` or ``name``
+of any ```` or `` `` included in the form.
The second optional argument is used to override the default form field values.
.. note::
diff --git a/testing/bootstrap.rst b/testing/bootstrap.rst
index 59fc289f0be..83e8e55149b 100644
--- a/testing/bootstrap.rst
+++ b/testing/bootstrap.rst
@@ -35,11 +35,11 @@ You can modify this file to add custom logic:
.. note::
If you don't use Symfony Flex, make sure this file is configured as
- bootstrap file in your ``phpunit.xml.dist`` file:
+ bootstrap file in your ``phpunit.dist.xml`` file:
.. code-block:: xml
-
+
filter('input[type=submit]')
->last()
- ->parents()
+ ->ancestors()
->first()
;
Many other methods are also available:
``filter('h1.title')``
- Nodes that match the CSS selector.
+ Finds nodes that match the given CSS selector (which must be supported by
+ Symfony's :doc:`CSS Selector component `).
``filterXpath('h1')``
- Nodes that match the XPath expression.
+ Finds nodes matching the given `XPath expression`_.
``eq(1)``
- Node for the specified index.
+ Returns the node at the given index (``0`` is the first node).
``first()``
- First node.
+ Returns the first node (equivalent to ``eq(0)``).
``last()``
- Last node.
+ Returns the last node.
``siblings()``
- Siblings.
+ Returns all sibling nodes (nodes with the same parent, excluding the current node).
``nextAll()``
- All following siblings.
+ Returns all following siblings (same parent, after the current node).
``previousAll()``
- All preceding siblings.
-``parents()``
- Returns the parent nodes.
+ Returns all preceding siblings (same parent, before the current node).
+``ancestors()``
+ Returns all ancestor nodes (parents, grandparents, etc., up to the ````
+ element).
``children()``
- Returns children nodes.
+ Returns all direct child nodes of the current node.
``reduce($lambda)``
- Nodes for which the callable does not return false.
+ Filters the nodes using a callback; keeps only those for which it returns ``true``.
Since each of these methods returns a new ``Crawler`` instance, you can
narrow down your node selection by chaining the method calls::
@@ -91,3 +93,5 @@ The Crawler can extract information from the nodes::
$data = $crawler->each(function ($node, int $i): string {
return $node->attr('href');
});
+
+.. _`XPath expression`: https://developer.mozilla.org/en-US/docs/Web/XML/XPath
diff --git a/testing/end_to_end.rst b/testing/end_to_end.rst
index 83d61b1e60f..74d6d495637 100644
--- a/testing/end_to_end.rst
+++ b/testing/end_to_end.rst
@@ -76,12 +76,16 @@ When using the extension in conjunction with the ``PANTHER_ERROR_SCREENSHOT_DIR`
environment variable, tests using the Panther client that fail or error (after the
client is created) will automatically get a screenshot taken to help debugging.
-To register the Panther extension, add the following lines to ``phpunit.xml.dist``:
+To register the Panther extension, add the following lines to ``phpunit.dist.xml``
+(in legacy PHPUnit versions older than 10, the file is named ``phpunit.xml.dist``):
.. code-block:: xml
-
+
+
+
+
@@ -868,17 +872,17 @@ Another option is to create a file called ``tests/router.php`` and add the follo
require $script;
-Then declare it as a router for Panther server in ``phpunit.xml.dist`` using the
+Then declare it as a router for Panther server in ``phpunit.dist.xml`` using the
``PANTHER_WEB_SERVER_ROUTER`` environment variable:
.. code-block:: xml
-
+
-
+
diff --git a/translation.rst b/translation.rst
index 86282090801..47f9124a5f2 100644
--- a/translation.rst
+++ b/translation.rst
@@ -52,6 +52,12 @@ First, run this command to install the translator before using it:
$ composer require symfony/translation
+Symfony includes several internationalization polyfills (``symfony/polyfill-intl-icu``,
+``symfony/polyfill-intl-messageformatter``, etc.) that allow you to use translation
+features even without the `PHP intl extension`_. However, these polyfills only
+support English translations, so you must install the PHP ``intl`` extension
+when translating into other languages.
+
.. _translation-configuration:
Configuration
@@ -487,11 +493,11 @@ your application:
.. code-block:: twig
- {{ 'Application version: {version}'|trans }}
+ {{ 'Application version: {app_version}'|trans }}
{# output: "Application version: 1.2.3" #}
{# parameters passed to the message override global parameters #}
- {{ 'Package version: {version}'|trans({'{version}': '2.3.4'}) }}
+ {{ 'Package version: {app_version}'|trans({'{app_version}': '2.3.4'}) }}
# Displays "Package version: 2.3.4"
Forcing the Translator Locale
@@ -677,8 +683,7 @@ Translations of Doctrine Entities
Unlike the contents of templates, it's not practical to translate the contents
stored in Doctrine Entities using translation catalogs. Instead, use the
-Doctrine `Translatable Extension`_ or the `Translatable Behavior`_. For more
-information, read the documentation of those libraries.
+Doctrine `Translatable Extension`_.
Custom Translation Resources
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -734,7 +739,7 @@ You'll now have a new line in your ``.env`` file that you can uncomment:
The ``LOCO_DSN`` isn't a *real* address: it's a convenient format that offloads
most of the configuration work to Symfony. The ``loco`` scheme activates the
-Loco provider that you just installed, which knows all about how to push and
+Loco provider that you installed, which knows all about how to push and
pull translations via Loco. The *only* part you need to change is the
``API_KEY`` placeholder.
@@ -1177,18 +1182,11 @@ checks translation resources for several locales:
Switch Locale Programmatically
------------------------------
-Sometimes you need to change the locale of the application dynamically
-just to run some code. Imagine a console command that renders Twig templates
-of emails in different languages. You need to change the locale only to
-render those templates.
-
-The ``LocaleSwitcher`` class allows you to change at once the locale
-of:
+Sometimes you need to change the application's locale dynamically while running
+some code. For example, a console command that renders email templates in
+different languages. In such cases, you only need to switch the locale temporarily.
-* All the services that are tagged with ``kernel.locale_aware``;
-* ``\Locale::setDefault()``;
-* If the ``RequestContext`` service is available, the ``_locale``
- parameter (so urls are generated with the new locale)::
+The ``LocaleSwitcher`` class allows you to do that::
use Symfony\Component\Translation\LocaleSwitcher;
@@ -1201,28 +1199,23 @@ of:
public function someMethod(): void
{
- // you can get the current application locale like this:
$currentLocale = $this->localeSwitcher->getLocale();
- // you can set the locale for the entire application like this:
- // (from now on, the application will use 'fr' (French) as the
- // locale; including the default locale used to translate Twig templates)
+ // set the application locale programmatically to 'fr' (French):
+ // this affects translation, URL generation, etc.
$this->localeSwitcher->setLocale('fr');
- // reset the current locale of your application to the configured default locale
- // in config/packages/translation.yaml, by option 'default_locale'
+ // reset the locale to the default one configured via the
+ // 'default_locale' option in config/packages/translation.yaml
$this->localeSwitcher->reset();
- // you can also run some code with a certain locale, without
+ // run some code with a specific locale, temporarily, without
// changing the locale for the rest of the application
$this->localeSwitcher->runWithLocale('es', function() {
-
- // e.g. render here some Twig templates using 'es' (Spanish) locale
-
+ // e.g. render templates, send emails, etc. using the 'es' (Spanish) locale
});
- // you can optionally declare an argument in your callback to receive the
- // injected locale
+ // optionally, receive the current locale as an argument:
$this->localeSwitcher->runWithLocale('es', function(string $locale) {
// here, the $locale argument will be set to 'es'
@@ -1233,6 +1226,20 @@ of:
}
}
+The ``LocaleSwitcher`` class changes the locale of:
+
+* All services tagged with ``kernel.locale_aware``;
+* The default locale set via ``\Locale::setDefault()``;
+* The ``_locale`` parameter of the ``RequestContext`` service (if available),
+ so generated URLs reflect the new locale.
+
+.. note::
+
+ The LocaleSwitcher applies the new locale only for the current request,
+ and its effect is lost on subsequent requests, such as after a redirect.
+
+ See :ref:`how to make the locale persist across requests `.
+
When using :ref:`autowiring `, type-hint any controller or
service argument with the :class:`Symfony\\Component\\Translation\\LocaleSwitcher`
class to inject the locale switcher service. Otherwise, configure your services
@@ -1674,8 +1681,8 @@ Learn more
.. _`ICU MessageFormat`: https://unicode-org.github.io/icu/userguide/format_parse/messages/
.. _`ISO 3166-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes
.. _`ISO 639-1`: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
+.. _`PHP intl extension`: https://php.net/book.intl
.. _`Translatable Extension`: https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/translatable.md
-.. _`Translatable Behavior`: https://github.com/KnpLabs/DoctrineBehaviors
.. _`Custom Language Name setting`: https://docs.lokalise.com/en/articles/1400492-uploading-files#custom-language-codes
.. _`ICU resource bundle`: https://github.com/unicode-org/icu-docs/blob/main/design/bnf_rb.txt
.. _`Portable object format`: https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html
diff --git a/validation/custom_constraint.rst b/validation/custom_constraint.rst
index bb34775a71c..10584a36383 100644
--- a/validation/custom_constraint.rst
+++ b/validation/custom_constraint.rst
@@ -176,6 +176,13 @@ message as its argument and returns an instance of
:class:`Symfony\\Component\\Validator\\Violation\\ConstraintViolationBuilderInterface`.
The ``addViolation()`` method call finally adds the violation to the context.
+.. tip::
+
+ Validation error messages are automatically translated to the current application
+ locale. If your application doesn't use translations, you can disable this behavior
+ by calling the ``disableTranslation()`` method of ``ConstraintViolationBuilderInterface``.
+ See also the :ref:`framework.validation.disable_translation option `.
+
Using the new Validator
-----------------------
@@ -316,7 +323,7 @@ define those options as public properties on the constraint class::
}
Then, inside the validator class you can access these options directly via the
-constraint class passes to the ``validate()`` method::
+constraint class passed to the ``validate()`` method::
class FooValidator extends ConstraintValidator
{
diff --git a/web_link.rst b/web_link.rst
index 8602445313f..2f2f96d106b 100644
--- a/web_link.rst
+++ b/web_link.rst
@@ -146,7 +146,8 @@ The WebLink component provides the following Twig functions to send those hints:
* ``prefetch()``: "identifies a resource that might be required by the next
navigation, and that the user agent *should* fetch, such that the user agent
can deliver a faster response once the resource is requested in the future".
-* ``prerender()``: "identifies a resource that might be required by the next
+* ``prerender()``: " **deprecated** and superseded by the `Speculation Rules API`_,
+ identifies a resource that might be required by the next
navigation, and that the user agent *should* fetch and execute, such that the
user agent can deliver a faster response once the resource is requested later".
@@ -194,6 +195,12 @@ You can also add links to the HTTP response directly from controllers and servic
}
}
+.. tip::
+
+ The possible values of link relations (``'preload'``, ``'preconnect'``, etc.)
+ are also defined as constants in the :class:`Symfony\\Component\\WebLink\\Link`
+ class (e.g. ``Link::REL_PRELOAD``, ``Link::REL_PRECONNECT``, etc.).
+
.. _`WebLink`: https://github.com/symfony/web-link
.. _`HTTP/2 Server Push`: https://tools.ietf.org/html/rfc7540#section-8.2
.. _`Resource Hints`: https://www.w3.org/TR/resource-hints/
@@ -206,3 +213,4 @@ You can also add links to the HTTP response directly from controllers and servic
.. _`Akamai`: https://http2.akamai.com/
.. _`link defined in the HTML specification`: https://html.spec.whatwg.org/dev/links.html#linkTypes
.. _`PSR-13`: https://www.php-fig.org/psr/psr-13/
+.. _`Speculation Rules API`: https://developer.mozilla.org/docs/Web/API/Speculation_Rules_API
diff --git a/webhook.rst b/webhook.rst
index 6e9408c12eb..d27a6e6d906 100644
--- a/webhook.rst
+++ b/webhook.rst
@@ -117,7 +117,7 @@ webhook consumer code.
The webhook routing name is part of the URL you need to configure at the
third-party mailer provider. The URL is the concatenation of your domain name
and the routing name you chose in the configuration (like
-``https://example.com/webhook/mailer_mailgun``.
+``https://example.com/webhook/mailer_mailgun``).
For Mailgun, you will get a secret for the webhook. Store this secret as
MAILER_MAILGUN_SECRET (in the :doc:`secrets management system
diff --git a/workflow.rst b/workflow.rst
index 11b4005bb10..54a1b06313e 100644
--- a/workflow.rst
+++ b/workflow.rst
@@ -1,7 +1,7 @@
Workflow
========
-Using the Workflow component inside a Symfony application requires knowing first
+Using the Workflow component inside a Symfony application requires first knowing
some basic theory and concepts about workflows and state machines.
:doc:`Read this article ` for a quick overview.
@@ -826,7 +826,7 @@ transition. The value of this option is any valid expression created with the
from: draft
to: reviewed
publish:
- # or "is_anonymous", "is_remember_me", "is_fully_authenticated", "is_granted", "is_valid"
+ # or "is_remember_me", "is_fully_authenticated", "is_granted", "is_valid"
guard: "is_authenticated"
from: reviewed
to: published
@@ -861,7 +861,7 @@ transition. The value of this option is any valid expression created with the
-
+
is_authenticated
reviewed
published
@@ -897,7 +897,7 @@ transition. The value of this option is any valid expression created with the
$blogPublishing->transition()
->name('publish')
- // or "is_anonymous", "is_remember_me", "is_fully_authenticated", "is_granted"
+ // or "is_remember_me", "is_fully_authenticated", "is_granted"
->guard('is_authenticated')
->from(['reviewed'])
->to(['published']);
@@ -1306,6 +1306,87 @@ In Twig templates, metadata is available via the ``workflow_metadata()`` functio
+Validating Workflow Definitions
+-------------------------------
+
+Symfony allows you to validate workflow definitions using your own custom logic.
+To do so, create a class that implements the
+:class:`Symfony\\Component\\Workflow\\Validator\\DefinitionValidatorInterface`::
+
+ namespace App\Workflow\Validator;
+
+ use Symfony\Component\Workflow\Definition;
+ use Symfony\Component\Workflow\Exception\InvalidDefinitionException;
+ use Symfony\Component\Workflow\Validator\DefinitionValidatorInterface;
+
+ final class BlogPublishingValidator implements DefinitionValidatorInterface
+ {
+ public function validate(Definition $definition, string $name): void
+ {
+ if (!$definition->getMetadataStore()->getMetadata('title')) {
+ throw new InvalidDefinitionException(sprintf('The workflow metadata title is missing in Workflow "%s".', $name));
+ }
+
+ // ...
+ }
+ }
+
+After implementing your validator, configure your workflow to use it:
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/workflow.yaml
+ framework:
+ workflows:
+ blog_publishing:
+ # ...
+
+ definition_validators:
+ - App\Workflow\Validator\BlogPublishingValidator
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+ App\Workflow\Validator\BlogPublishingValidator
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/workflow.php
+ use Symfony\Config\FrameworkConfig;
+
+ return static function (FrameworkConfig $framework): void {
+ $blogPublishing = $framework->workflows()->workflows('blog_publishing');
+ // ...
+
+ $blogPublishing->definitionValidators([
+ App\Workflow\Validator\BlogPublishingValidator::class
+ ]);
+
+ // ...
+ };
+
+The ``BlogPublishingValidator`` will be executed during container compilation
+to validate the workflow definition.
+
+.. versionadded:: 7.3
+
+ Support for workflow definition validators was introduced in Symfony 7.3.
+
Learn more
----------