diff --git a/cache.rst b/cache.rst
index 075f4002e74..5687d3544b6 100644
--- a/cache.rst
+++ b/cache.rst
@@ -539,6 +539,8 @@ Symfony stores the item automatically in all the missing pools.
;
};
+.. _cache-using-cache-tags:
+
Using Cache Tags
----------------
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 23dd8d948a1..c0295b487a0 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`_
+of `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
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/clock.rst b/components/clock.rst
index 5b20e6000b9..c4ac88e9092 100644
--- a/components/clock.rst
+++ b/components/clock.rst
@@ -267,6 +267,36 @@ timestamps::
:method:`Symfony\\Component\\Clock\\DatePoint::getMicrosecond` methods were
introduced in Symfony 7.1.
+Storing DatePoints in the Database
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you :doc:`use Doctrine ` to work with databases, consider using the
+``date_point`` Doctrine type, which converts to/from ``DatePoint`` objects automatically::
+
+ // src/Entity/Product.php
+ namespace App\Entity;
+
+ use Doctrine\ORM\Mapping as ORM;
+ use Symfony\Component\Clock\DatePoint;
+
+ #[ORM\Entity]
+ class Product
+ {
+ // if you don't define the Doctrine type explicitly, Symfony will autodetect it:
+ #[ORM\Column]
+ private DatePoint $createdAt;
+
+ // if you prefer to define the Doctrine type explicitly:
+ #[ORM\Column(type: 'date_point')]
+ private DatePoint $updatedAt;
+
+ // ...
+ }
+
+.. versionadded:: 7.3
+
+ The ``DatePointType`` was introduced in Symfony 7.3.
+
.. _clock_writing-tests:
Writing Time-Sensitive Tests
diff --git a/components/config/definition.rst b/components/config/definition.rst
index 0e626931568..4848af33ffe 100644
--- a/components/config/definition.rst
+++ b/components/config/definition.rst
@@ -186,6 +186,25 @@ The configuration can now be written like this::
->end()
;
+You can also use the ``enumClass()`` 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)
+ ->end()
+ ->end()
+ ;
+
+When using a backed enum, the values provided to the node will be cast
+to one of the enum cases if possible.
+
+.. versionadded:: 7.3
+
+ The ``enumClass()`` method was introduced in Symfony 7.3.
+
Array Nodes
~~~~~~~~~~~
@@ -527,6 +546,30 @@ and in XML:
+You can also provide a URL to a full documentation page::
+
+ $rootNode
+ ->docUrl('Full documentation is available at https://example.com/docs/{version:major}.{version:minor}/reference.html')
+ ->children()
+ ->integerNode('entries_per_page')
+ ->defaultValue(25)
+ ->end()
+ ->end()
+ ;
+
+A few placeholders are available to customize the URL:
+
+* ``{version:major}``: The major version of the package currently installed
+* ``{version:minor}``: The minor version of the package currently installed
+* ``{package}``: The name of the package
+
+The placeholders will be replaced when printing the configuration tree with the
+``config:dump-reference`` command.
+
+.. versionadded:: 7.3
+
+ The ``docUrl()`` method was introduced in Symfony 7.3.
+
Optional Sections
-----------------
@@ -815,6 +858,7 @@ A validation rule always has an "if" part. You can specify this part in
the following ways:
- ``ifTrue()``
+- ``ifFalse()``
- ``ifString()``
- ``ifNull()``
- ``ifEmpty()``
@@ -833,6 +877,10 @@ A validation rule also requires a "then" part:
Usually, "then" is a closure. Its return value will be used as a new value
for the node, instead of the node's original value.
+.. versionadded:: 7.3
+
+ The ``ifFalse()`` method was introduced in Symfony 7.3.
+
Configuring the Node Path Separator
-----------------------------------
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 3cb87c4c307..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
@@ -129,10 +125,16 @@ Sometimes you want to format seconds to time. This is possible with the
The first argument is the seconds to format and the second argument is the
precision (default ``1``) of the result::
- Helper::formatTime(42); // 42 secs
- Helper::formatTime(125); // 2 mins
- Helper::formatTime(125, 2); // 2 mins, 5 secs
- Helper::formatTime(172799, 4); // 1 day, 23 hrs, 59 mins, 59 secs
+ Helper::formatTime(0.001); // 1 ms
+ Helper::formatTime(42); // 42 s
+ Helper::formatTime(125); // 2 min
+ Helper::formatTime(125, 2); // 2 min, 5 s
+ Helper::formatTime(172799, 4); // 1 d, 23 h, 59 min, 59 s
+ Helper::formatTime(172799.056, 5); // 1 d, 23 h, 59 min, 59 s, 56 ms
+
+.. versionadded:: 7.3
+
+ Support for formatting up to milliseconds was introduced in Symfony 7.3.
Formatting Memory
-----------------
diff --git a/components/console/helpers/map.rst.inc b/components/console/helpers/map.rst.inc
index 41f0667c40d..73d5d4da7a0 100644
--- a/components/console/helpers/map.rst.inc
+++ b/components/console/helpers/map.rst.inc
@@ -4,5 +4,6 @@
* :doc:`/components/console/helpers/progressindicator`
* :doc:`/components/console/helpers/questionhelper`
* :doc:`/components/console/helpers/table`
+* :doc:`/components/console/helpers/tree`
* :doc:`/components/console/helpers/debug_formatter`
* :doc:`/components/console/helpers/cursor`
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 3dc97d5c0d3..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 {
@@ -480,10 +478,10 @@ invalid answer and will only be able to proceed if their input is valid.
use Symfony\Component\Validator\Validation;
$question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle');
- $validation = Validation::createCallable(new Regex([
- 'pattern' => '/^[a-zA-Z]+Bundle$/',
- 'message' => 'The name of the bundle should be suffixed with \'Bundle\'',
- ]));
+ $validation = Validation::createCallable(new Regex(
+ pattern: '/^[a-zA-Z]+Bundle$/',
+ message: 'The name of the bundle should be suffixed with \'Bundle\'',
+ ));
$question->setValidator($validation);
Validating a Hidden Response
@@ -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 d8d1a852592..e36b1570b70 100644
--- a/components/console/helpers/table.rst
+++ b/components/console/helpers/table.rst
@@ -10,15 +10,16 @@ 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
@@ -273,6 +274,26 @@ This outputs:
║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║
╚═══════════════╧══════════════════════════╧══════════════════╝
+**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
~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -452,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);
diff --git a/components/console/helpers/tree.rst b/components/console/helpers/tree.rst
new file mode 100644
index 00000000000..5e08e684e51
--- /dev/null
+++ b/components/console/helpers/tree.rst
@@ -0,0 +1,336 @@
+Tree Helper
+===========
+
+The Tree Helper allows you to build and display tree structures in the console.
+It's commonly used to render directory hierarchies, but you can also use it to render
+any tree-like content, such us organizational charts, product category trees, taxonomies, etc.
+
+.. versionadded:: 7.3
+
+ The ``TreeHelper`` class was introduced in Symfony 7.3.
+
+Rendering a Tree
+----------------
+
+The :method:`Symfony\\Component\\Console\\Helper\\TreeHelper::createTree` method
+creates a tree structure from an array and returns a :class:`Symfony\\Component\\Console\\Helper\\Tree`
+object that can be rendered in the console.
+
+Rendering a Tree from an Array
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can build a tree from an array by passing the array to the
+:method:`Symfony\\Component\\Console\\Helper\\TreeHelper::createTree` method
+inside your console command::
+
+ namespace App\Command;
+
+ use Symfony\Component\Console\Attribute\AsCommand;
+ use Symfony\Component\Console\Helper\TreeHelper;
+ use Symfony\Component\Console\Helper\TreeNode;
+ use Symfony\Component\Console\Style\SymfonyStyle;
+
+ #[AsCommand(name: 'app:my-command', description: '...')]
+ class MyCommand
+ {
+ // ...
+
+ public function __invoke(SymfonyStyle $io): int
+ {
+ $node = TreeNode::fromValues([
+ 'config/',
+ 'public/',
+ 'src/',
+ 'templates/',
+ 'tests/',
+ ]);
+
+ $tree = TreeHelper::createTree($io, $node);
+ $tree->render();
+
+ // ...
+ }
+ }
+
+This exampe would output the following:
+
+.. code-block:: terminal
+
+ ├── config/
+ ├── public/
+ ├── src/
+ ├── templates/
+ └── tests/
+
+The given contents can be defined in a multi-dimensional array::
+
+ $tree = TreeHelper::createTree($io, null, [
+ 'src' => [
+ 'Command',
+ 'Controller' => [
+ 'DefaultController.php',
+ ],
+ 'Kernel.php',
+ ],
+ 'templates' => [
+ 'base.html.twig',
+ ],
+ ]);
+
+ $tree->render();
+
+The above code will output the following tree:
+
+.. code-block:: terminal
+
+ ├── src
+ │ ├── Command
+ │ ├── Controller
+ │ │ └── DefaultController.php
+ │ └── Kernel.php
+ └── templates
+ └── base.html.twig
+
+Building a Tree Programmatically
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+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;
+
+ $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();
+
+Customizing the Tree Style
+--------------------------
+
+Built-in Tree Styles
+~~~~~~~~~~~~~~~~~~~~
+
+The tree helper provides a few built-in styles that you can use to customize the
+output of the tree.
+
+**Default**::
+
+ TreeHelper::createTree($io, $node, [], TreeStyle::default());
+
+This outputs:
+
+.. code-block:: terminal
+
+ ├── config
+ │ ├── packages
+ │ └── routes
+ │ ├── framework.yaml
+ │ └── web_profiler.yaml
+ ├── src
+ │ ├── Command
+ │ ├── Controller
+ │ │ └── DefaultController.php
+ │ └── Kernel.php
+ └── templates
+ └── base.html.twig
+
+**Box**::
+
+ TreeHelper::createTree($io, $node, [], TreeStyle::box());
+
+This outputs:
+
+.. code-block:: terminal
+
+ ┃╸ config
+ ┃ ┃╸ packages
+ ┃ ┗╸ routes
+ ┃ ┃╸ framework.yaml
+ ┃ ┗╸ web_profiler.yaml
+ ┃╸ src
+ ┃ ┃╸ Command
+ ┃ ┃╸ Controller
+ ┃ ┃ ┗╸ DefaultController.php
+ ┃ ┗╸ Kernel.php
+ ┗╸ templates
+ ┗╸ base.html.twig
+
+**Double box**::
+
+ TreeHelper::createTree($io, $node, [], TreeStyle::doubleBox());
+
+This outputs:
+
+.. code-block:: terminal
+
+ ╠═ config
+ ║ ╠═ packages
+ ║ ╚═ routes
+ ║ ╠═ framework.yaml
+ ║ ╚═ web_profiler.yaml
+ ╠═ src
+ ║ ╠═ Command
+ ║ ╠═ Controller
+ ║ ║ ╚═ DefaultController.php
+ ║ ╚═ Kernel.php
+ ╚═ templates
+ ╚═ base.html.twig
+
+**Compact**::
+
+ TreeHelper::createTree($io, $node, [], TreeStyle::compact());
+
+This outputs:
+
+.. code-block:: terminal
+
+ ├ config
+ │ ├ packages
+ │ └ routes
+ │ ├ framework.yaml
+ │ └ web_profiler.yaml
+ ├ src
+ │ ├ Command
+ │ ├ Controller
+ │ │ └ DefaultController.php
+ │ └ Kernel.php
+ └ templates
+ └ base.html.twig
+
+**Light**::
+
+ TreeHelper::createTree($io, $node, [], TreeStyle::light());
+
+This outputs:
+
+.. code-block:: terminal
+
+ |-- config
+ | |-- packages
+ | `-- routes
+ | |-- framework.yaml
+ | `-- web_profiler.yaml
+ |-- src
+ | |-- Command
+ | |-- Controller
+ | | `-- DefaultController.php
+ | `-- Kernel.php
+ `-- templates
+ `-- base.html.twig
+
+**Minimal**::
+
+ TreeHelper::createTree($io, $node, [], TreeStyle::minimal());
+
+This outputs:
+
+.. code-block:: terminal
+
+ . config
+ . . packages
+ . . routes
+ . . framework.yaml
+ . . web_profiler.yaml
+ . src
+ . . Command
+ . . Controller
+ . . . DefaultController.php
+ . . Kernel.php
+ . templates
+ . base.html.twig
+
+**Rounded**::
+
+ TreeHelper::createTree($io, $node, [], TreeStyle::rounded());
+
+This outputs:
+
+.. code-block:: terminal
+
+ ├─ config
+ │ ├─ packages
+ │ ╰─ routes
+ │ ├─ framework.yaml
+ │ ╰─ web_profiler.yaml
+ ├─ src
+ │ ├─ Command
+ │ ├─ Controller
+ │ │ ╰─ DefaultController.php
+ │ ╰─ Kernel.php
+ ╰─ templates
+ ╰─ base.html.twig
+
+Making a Custom Tree Style
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can create your own tree style by passing the characters to the constructor
+of the :class:`Symfony\\Component\\Console\\Helper\\TreeStyle` class::
+
+ use Symfony\Component\Console\Helper\TreeHelper;
+ use Symfony\Component\Console\Helper\TreeStyle;
+
+ $customStyle = new TreeStyle('🟣 ', '🟠 ', '🔵 ', '🟢 ', '🔴 ', '🟡 ');
+
+ // Pass the custom style to the createTree method
+
+ $tree = TreeHelper::createTree($io, null, [
+ 'src' => [
+ 'Command',
+ 'Controller' => [
+ 'DefaultController.php',
+ ],
+ 'Kernel.php',
+ ],
+ 'templates' => [
+ 'base.html.twig',
+ ],
+ ], $customStyle);
+
+ $tree->render();
+
+The above code will output the following tree:
+
+.. code-block:: terminal
+
+ 🔵 🟣 🟡 src
+ 🔵 🟢 🟣 🟡 Command
+ 🔵 🟢 🟣 🟡 Controller
+ 🔵 🟢 🟢 🟠 🟡 DefaultController.php
+ 🔵 🟢 🟠 🟡 Kernel.php
+ 🔵 🟠 🟡 templates
+ 🔵 🔴 🟠 🟡 base.html.twig
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/http_foundation.rst b/components/http_foundation.rst
index f35f2020535..1cb87aafb24 100644
--- a/components/http_foundation.rst
+++ b/components/http_foundation.rst
@@ -681,8 +681,19 @@ Streaming a Response
~~~~~~~~~~~~~~~~~~~~
The :class:`Symfony\\Component\\HttpFoundation\\StreamedResponse` class allows
-you to stream the Response back to the client. The response content is
-represented by a PHP callable instead of a string::
+you to stream the Response back to the client. The response content can be
+represented by a string iterable::
+
+ use Symfony\Component\HttpFoundation\StreamedResponse;
+
+ $chunks = ['Hello', ' World'];
+
+ $response = new StreamedResponse();
+ $response->setChunks($chunks);
+ $response->send();
+
+For most complex use cases, the response content can be instead represented by
+a PHP callable::
use Symfony\Component\HttpFoundation\StreamedResponse;
@@ -710,6 +721,10 @@ represented by a PHP callable instead of a string::
// disables FastCGI buffering in nginx only for this response
$response->headers->set('X-Accel-Buffering', 'no');
+.. versionadded:: 7.3
+
+ Support for using string iterables was introduced in Symfony 7.3.
+
Streaming a JSON Response
~~~~~~~~~~~~~~~~~~~~~~~~~
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..2403763bd4a 100644
--- a/components/lock.rst
+++ b/components/lock.rst
@@ -612,9 +612,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 +623,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/options_resolver.rst b/components/options_resolver.rst
index da265680d23..17ec46c2fc9 100644
--- a/components/options_resolver.rst
+++ b/components/options_resolver.rst
@@ -305,13 +305,21 @@ correctly. To validate the types of the options, call
// specify multiple allowed types
$resolver->setAllowedTypes('port', ['null', 'int']);
+ // if you prefer, you can also use the following equivalent syntax
+ $resolver->setAllowedTypes('port', 'int|null');
// check all items in an array recursively for a type
$resolver->setAllowedTypes('dates', 'DateTime[]');
$resolver->setAllowedTypes('ports', 'int[]');
+ // the following syntax means "an array of integers or an array of strings"
+ $resolver->setAllowedTypes('endpoints', '(int|string)[]');
}
}
+.. versionadded:: 7.3
+
+ Defining type unions with the ``|`` syntax was introduced in Symfony 7.3.
+
You can pass any type for which an ``is_()`` function is defined in PHP.
You may also pass fully qualified class or interface names (which is checked
using ``instanceof``). Additionally, you can validate all items in an array
@@ -386,7 +394,7 @@ returns ``true`` for acceptable values and ``false`` for invalid values::
// ...
$resolver->setAllowedValues('transport', Validation::createIsValidCallable(
- new Length(['min' => 10 ])
+ new Length(min: 10)
));
In sub-classes, you can use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addAllowedValues`
@@ -654,7 +662,7 @@ default value::
public function configureOptions(OptionsResolver $resolver): void
{
- $resolver->setDefault('spool', function (OptionsResolver $spoolResolver): void {
+ $resolver->setOptions('spool', function (OptionsResolver $spoolResolver): void {
$spoolResolver->setDefaults([
'type' => 'file',
'path' => '/path/to/spool',
@@ -678,6 +686,16 @@ default value::
],
]);
+.. deprecated:: 7.3
+
+ Defining nested options via :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setDefault`
+ is deprecated since Symfony 7.3. Use the :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setOptions`
+ method instead, which also allows defining default values for prototyped options.
+
+.. versionadded:: 7.3
+
+ The ``setOptions()`` method was introduced in Symfony 7.3.
+
Nested options also support required options, validation (type, value) and
normalization of their values. If the default value of a nested option depends
on another option defined in the parent level, add a second ``Options`` argument
@@ -690,7 +708,7 @@ to the closure to access to them::
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefault('sandbox', false);
- $resolver->setDefault('spool', function (OptionsResolver $spoolResolver, Options $parent): void {
+ $resolver->setOptions('spool', function (OptionsResolver $spoolResolver, Options $parent): void {
$spoolResolver->setDefaults([
'type' => $parent['sandbox'] ? 'memory' : 'file',
// ...
@@ -713,13 +731,13 @@ In same way, parent options can access to the nested options as normal arrays::
public function configureOptions(OptionsResolver $resolver): void
{
- $resolver->setDefault('spool', function (OptionsResolver $spoolResolver): void {
+ $resolver->setOptions('spool', function (OptionsResolver $spoolResolver): void {
$spoolResolver->setDefaults([
'type' => 'file',
// ...
]);
});
- $resolver->setDefault('profiling', function (Options $options): void {
+ $resolver->setOptions('profiling', function (Options $options): void {
return 'file' === $options['spool']['type'];
});
}
@@ -740,7 +758,7 @@ with ``host``, ``database``, ``user`` and ``password`` each.
The best way to implement this is to define the ``connections`` option as prototype::
- $resolver->setDefault('connections', function (OptionsResolver $connResolver): void {
+ $resolver->setOptions('connections', function (OptionsResolver $connResolver): void {
$connResolver
->setPrototype(true)
->setRequired(['host', 'database'])
diff --git a/components/process.rst b/components/process.rst
index f6c8837d2c3..9c25c931510 100644
--- a/components/process.rst
+++ b/components/process.rst
@@ -114,6 +114,8 @@ You can configure the options passed to the ``other_options`` argument of
and ``suppress_errors``) are only supported on Windows operating systems.
Check out the `PHP documentation for proc_open()`_ before using them.
+.. _process-using-features-from-the-os-shell:
+
Using Features From the OS Shell
--------------------------------
@@ -428,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)
@@ -442,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 60b481c09b4..865a36c5941 100644
--- a/components/property_info.rst
+++ b/components/property_info.rst
@@ -469,7 +469,18 @@ information from annotations of properties and methods, such as ``@var``,
use App\Domain\Foo;
$phpStanExtractor = new PhpStanExtractor();
+
+ // Type information.
$phpStanExtractor->getTypesFromConstructor(Foo::class, 'bar');
+ // Description information.
+ $phpStanExtractor->getShortDescription($class, 'bar');
+ $phpStanExtractor->getLongDescription($class, 'bar');
+
+.. versionadded:: 7.3
+
+ The :method:`Symfony\\Component\\PropertyInfo\\Extractor\\PhpStanExtractor::getShortDescription`
+ and :method:`Symfony\\Component\\PropertyInfo\\Extractor\\PhpStanExtractor::getLongDescription`
+ methods were introduced in Symfony 7.3.
SerializerExtractor
~~~~~~~~~~~~~~~~~~~
@@ -527,6 +538,8 @@ with the ``property_info`` service in the Symfony Framework::
// Type information.
$doctrineExtractor->getTypes($class, $property);
+.. _components-property-information-constructor-extractor:
+
ConstructorExtractor
~~~~~~~~~~~~~~~~~~~~
@@ -559,6 +572,7 @@ Creating Your Own Extractors
You can create your own property information extractors by creating a
class that implements one or more of the following interfaces:
+:class:`Symfony\\Component\\PropertyInfo\\Extractor\\ConstructorArgumentTypeExtractorInterface`,
:class:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface`,
:class:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface`,
:class:`Symfony\\Component\\PropertyInfo\\PropertyListExtractorInterface`,
@@ -576,6 +590,11 @@ service by defining it as a service with one or more of the following
* ``property_info.access_extractor`` if it provides access information.
* ``property_info.initializable_extractor`` if it provides initializable information
(it checks if a property can be initialized through the constructor).
+* ``property_info.constructor_extractor`` if it provides type information from the constructor argument.
+
+ .. versionadded:: 7.3
+
+ The ``property_info.constructor_extractor`` tag was introduced in Symfony 7.3.
.. _`PSR-1`: https://www.php-fig.org/psr/psr-1/
.. _`phpDocumentor Reflection`: https://github.com/phpDocumentor/ReflectionDocBlock
diff --git a/components/type_info.rst b/components/type_info.rst
index 3d1aa569fec..817c7f1d61a 100644
--- a/components/type_info.rst
+++ b/components/type_info.rst
@@ -40,6 +40,16 @@ to the :class:`Symfony\\Component\\TypeInfo\\Type` static methods as following::
Many others methods are available and can be found
in :class:`Symfony\\Component\\TypeInfo\\TypeFactoryTrait`.
+You can also use a generic method that detects the type automatically::
+
+ Type::fromValue(1.1); // same as Type::float()
+ Type::fromValue('...'); // same as Type::string()
+ Type::fromValue(false); // same as Type::false()
+
+.. versionadded:: 7.3
+
+ The ``fromValue()`` method was introduced in Symfony 7.3.
+
Resolvers
~~~~~~~~~
@@ -114,7 +124,7 @@ Advanced Usages
The TypeInfo component provides various methods to manipulate and check types,
depending on your needs.
-Checking a **simple type**::
+**Identify** a type::
// define a simple integer type
$type = Type::int();
@@ -141,6 +151,23 @@ Checking a **simple type**::
$type->isIdentifiedBy(DummyParent::class); // true
$type->isIdentifiedBy(DummyInterface::class); // true
+Checking if a type **accepts a value**::
+
+ $type = Type::int();
+ // check if the type accepts a given value
+ $type->accepts(123); // true
+ $type->accepts('z'); // false
+
+ $type = Type::union(Type::string(), Type::int());
+ // now the second check is true because the union type accepts either an int or a string value
+ $type->accepts(123); // true
+ $type->accepts('z'); // true
+
+.. versionadded:: 7.3
+
+ The :method:`Symfony\\Component\\TypeInfo\\Type::accepts`
+ method was introduced in Symfony 7.3.
+
Using callables for **complex checks**::
class Foo
diff --git a/components/validator.rst b/components/validator.rst
index 085c77a7946..12c61507257 100644
--- a/components/validator.rst
+++ b/components/validator.rst
@@ -36,7 +36,7 @@ characters long::
$validator = Validation::createValidator();
$violations = $validator->validate('Bernhard', [
- new Length(['min' => 10]),
+ new Length(min: 10),
new NotBlank(),
]);
diff --git a/components/validator/metadata.rst b/components/validator/metadata.rst
index e7df42413bc..782e1ee216f 100755
--- a/components/validator/metadata.rst
+++ b/components/validator/metadata.rst
@@ -24,7 +24,7 @@ the ``Author`` class has at least 3 characters::
$metadata->addPropertyConstraint('firstName', new Assert\NotBlank());
$metadata->addPropertyConstraint(
'firstName',
- new Assert\Length(["min" => 3])
+ new Assert\Length(min: 3)
);
}
}
@@ -55,9 +55,9 @@ Then, add the Validator component configuration to the class::
{
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue([
- 'message' => 'The password cannot match your first name',
- ]));
+ $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue(
+ message: 'The password cannot match your first name',
+ ));
}
}
diff --git a/components/validator/resources.rst b/components/validator/resources.rst
index 4fc7aa2cbb0..7d6cd0e8e5d 100644
--- a/components/validator/resources.rst
+++ b/components/validator/resources.rst
@@ -42,10 +42,10 @@ In this example, the validation metadata is retrieved executing the
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$metadata->addPropertyConstraint('name', new Assert\NotBlank());
- $metadata->addPropertyConstraint('name', new Assert\Length([
- 'min' => 5,
- 'max' => 20,
- ]));
+ $metadata->addPropertyConstraint('name', new Assert\Length(
+ min: 5,
+ max: 20,
+ ));
}
}
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 b78accb6652..efaf84f04e6 100644
--- a/components/yaml.rst
+++ b/components/yaml.rst
@@ -428,6 +428,16 @@ you can dump them as ``~`` with the ``DUMP_NULL_AS_TILDE`` flag::
$dumped = Yaml::dump(['foo' => null], 2, 4, Yaml::DUMP_NULL_AS_TILDE);
// foo: ~
+Another valid representation of the ``null`` value is an empty string. You can
+use the ``DUMP_NULL_AS_EMPTY`` flag to dump null values as empty strings::
+
+ $dumped = Yaml::dump(['foo' => null], 2, 4, Yaml::DUMP_NULL_AS_EMPTY);
+ // foo:
+
+.. versionadded:: 7.3
+
+ The ``DUMP_NULL_AS_EMPTY`` flag was introduced in Symfony 7.3.
+
Dumping Numeric Keys as Strings
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -440,6 +450,58 @@ By default, digit-only array keys are dumped as integers. You can use the
$dumped = Yaml::dump([200 => 'foo'], 2, 4, Yaml::DUMP_NUMERIC_KEY_AS_STRING);
// '200': foo
+Dumping Double Quotes on Values
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+By default, only unsafe string values are enclosed in double quotes (for example,
+if they are reserved words or contain newlines and spaces). Use the
+``DUMP_FORCE_DOUBLE_QUOTES_ON_VALUES`` flag to add double quotes to all string values::
+
+ $dumped = Yaml::dump([
+ 'foo' => 'bar', 'some foo' => 'some bar', 'x' => 3.14, 'y' => true, 'z' => null,
+ ]);
+ // foo: bar, 'some foo': 'some bar', x: 3.14, 'y': true, z: null
+
+ $dumped = Yaml::dump([
+ 'foo' => 'bar', 'some foo' => 'some bar', 'x' => 3.14, 'y' => true, 'z' => null,
+ ], 2, 4, Yaml::DUMP_FORCE_DOUBLE_QUOTES_ON_VALUES);
+ // "foo": "bar", "some foo": "some bar", "x": 3.14, "y": true, "z": null
+
+.. versionadded:: 7.3
+
+ The ``Yaml::DUMP_FORCE_DOUBLE_QUOTES_ON_VALUES`` flag was introduced in Symfony 7.3.
+
+Dumping Collection of Maps
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When the YAML component dumps collections of maps, it uses a hyphen on a separate
+line as a delimiter:
+
+.. code-block:: yaml
+
+ planets:
+ -
+ name: Mercury
+ distance: 57910000
+ -
+ name: Jupiter
+ distance: 778500000
+
+To produce a more compact output where the delimiter is included within the map,
+use the ``Yaml::DUMP_COMPACT_NESTED_MAPPING`` flag:
+
+.. code-block:: yaml
+
+ planets:
+ - name: Mercury
+ distance: 57910000
+ - name: Jupiter
+ distance: 778500000
+
+.. versionadded:: 7.3
+
+ The ``Yaml::DUMP_COMPACT_NESTED_MAPPING`` flag was introduced in Symfony 7.3.
+
Syntax Validation
~~~~~~~~~~~~~~~~~
diff --git a/configuration/micro_kernel_trait.rst b/configuration/micro_kernel_trait.rst
index ec09eba4466..542532ee1af 100644
--- a/configuration/micro_kernel_trait.rst
+++ b/configuration/micro_kernel_trait.rst
@@ -296,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
@@ -309,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
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"
>
-
+ ` 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
@@ -143,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
~~~~~~~~~~~~~~~~~~~
@@ -251,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([
@@ -311,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".');
@@ -372,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',
@@ -393,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;
}
@@ -424,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!');
@@ -468,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/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 c6011be5634..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');
// ...
@@ -169,6 +165,32 @@ Content Methods
styled according to the Symfony Style Guide, which allows you to use
features such as dynamically appending rows.
+:method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::tree`
+ It displays the given nested array as a formatted directory/file tree
+ structure in the console output::
+
+ $io->tree([
+ 'src' => [
+ 'Controller' => [
+ 'DefaultController.php',
+ ],
+ 'Kernel.php',
+ ],
+ 'templates' => [
+ 'base.html.twig',
+ ],
+ ]);
+
+.. versionadded:: 7.3
+
+ The ``SymfonyStyle::tree()`` and the ``SymfonyStyle::createTree()`` methods
+ were introduced in Symfony 7.3.
+
+:method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::createTree`
+ Creates an instance of :class:`Symfony\\Component\\Console\\Helper\\TreeHelper`
+ styled according to the Symfony Style Guide, which allows you to use
+ features such as dynamically nesting nodes.
+
:method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::newLine`
It displays a blank line in the command output. Although it may seem useful,
most of the times you won't need it at all. The reason is that every helper
@@ -428,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);
// ...
@@ -467,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;
@@ -475,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 ac81c92d696..3afd085d773 100644
--- a/console/verbosity.rst
+++ b/console/verbosity.rst
@@ -49,21 +49,22 @@ 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()
diff --git a/controller.rst b/controller.rst
index 3ce0a33cae9..05abdaee4ea 100644
--- a/controller.rst
+++ b/controller.rst
@@ -371,6 +371,11 @@ The ``MapQueryParameter`` attribute supports the following argument types:
* ``float``
* ``int``
* ``string``
+* Objects that extend :class:`Symfony\\Component\\Uid\\AbstractUid`
+
+.. versionadded:: 7.3
+
+ Support for ``AbstractUid`` objects was introduced in Symfony 7.3.
``#[MapQueryParameter]`` can take an optional argument called ``filter``. You can use the
`Validate Filters`_ constants defined in PHP::
@@ -452,6 +457,26 @@ HTTP status to return if the validation fails::
The default status code returned if the validation fails is 404.
+If you want to map your object to a nested array in your query using a specific key,
+set the ``key`` option in the ``#[MapQueryString]`` attribute::
+
+ use App\Model\SearchDto;
+ use Symfony\Component\HttpFoundation\Response;
+ use Symfony\Component\HttpKernel\Attribute\MapQueryString;
+
+ // ...
+
+ public function dashboard(
+ #[MapQueryString(key: 'search')] SearchDto $searchDto
+ ): Response
+ {
+ // ...
+ }
+
+.. versionadded:: 7.3
+
+ The ``key`` option of ``#[MapQueryString]`` was introduced in Symfony 7.3.
+
If you need a valid DTO even when the request query string is empty, set a
default value for your controller arguments::
diff --git a/controller/error_pages.rst b/controller/error_pages.rst
index fc36b88779a..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
@@ -336,3 +342,50 @@ time and again, you can have just one (or several) listeners deal with them.
your application (like :class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException`)
and takes measures like redirecting the user to the login page, logging them
out and other things.
+
+Dumping Error Pages as Static HTML Files
+----------------------------------------
+
+.. versionadded:: 7.3
+
+ The feature to dump error pages into static HTML files was introduced in Symfony 7.3.
+
+If an error occurs before reaching your Symfony application, web servers display
+their own default error pages instead of your custom ones. Dumping your application's
+error pages to static HTML ensures users always see your defined pages and improves
+performance by allowing the server to deliver errors instantly without calling
+your application.
+
+Symfony provides the following command to turn your error pages into static HTML files:
+
+.. code-block:: terminal
+
+ # the first argument is the path where the HTML files are stored
+ $ APP_ENV=prod php bin/console error:dump var/cache/prod/error_pages/
+
+ # by default, it generates the pages of all 4xx and 5xx errors, but you can
+ # pass a list of HTTP status codes to only generate those
+ $ APP_ENV=prod php bin/console error:dump var/cache/prod/error_pages/ 401 403 404 500
+
+You must also configure your web server to use these generated pages. For example,
+if you use Nginx:
+
+.. code-block:: nginx
+
+ # /etc/nginx/conf.d/example.com.conf
+ server {
+ # Existing server configuration
+ # ...
+
+ # Serve static error pages
+ error_page 400 /error_pages/400.html;
+ error_page 401 /error_pages/401.html;
+ # ...
+ error_page 510 /error_pages/510.html;
+ error_page 511 /error_pages/511.html;
+
+ location ^~ /error_pages/ {
+ root /path/to/your/symfony/var/cache/error_pages;
+ internal; # prevent direct URL access
+ }
+ }
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 425709be0d2..793cd26dd65 100644
--- a/controller/upload_file.rst
+++ b/controller/upload_file.rst
@@ -75,11 +75,11 @@ so Symfony doesn't try to get/set its value from the related entity::
// unmapped fields can't define their validation using attributes
// in the associated entity, so you can use the PHP constraint classes
'constraints' => [
- new File([
- 'maxSize' => '1024k',
- 'extensions' => ['pdf'],
- 'extensionsMessage' => 'Please upload a valid PDF document',
- ])
+ new File(
+ maxSize: '1024k',
+ extensions: ['pdf'],
+ extensionsMessage: 'Please upload a valid PDF document',
+ )
],
])
// ...
diff --git a/controller/value_resolver.rst b/controller/value_resolver.rst
index fd5e9db85ff..835edcfbff9 100644
--- a/controller/value_resolver.rst
+++ b/controller/value_resolver.rst
@@ -410,7 +410,7 @@ command to see which argument resolvers are present and in which order they run:
.. code-block:: terminal
- $ php bin/console debug:container debug.argument_resolver.inner --show-arguments
+ $ php bin/console debug:container debug.argument_resolver.inner
You can also configure the name passed to the ``ValueResolver`` attribute to target
your resolver. Otherwise it will default to the service's id.
diff --git a/doctrine.rst b/doctrine.rst
index 07ed985a519..6a1438322fa 100644
--- a/doctrine.rst
+++ b/doctrine.rst
@@ -798,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
~~~~~~~~~~~~~~~~~
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/form/form_customization.rst b/form/form_customization.rst
index 1c23601a883..dc09aefe77d 100644
--- a/form/form_customization.rst
+++ b/form/form_customization.rst
@@ -103,6 +103,7 @@ That's why Symfony provides other Twig form helpers that render the value of
each form field part without adding any HTML around it:
* ``field_name()``
+* ``field_id()``
* ``field_value()``
* ``field_label()``
* ``field_help()``
@@ -116,6 +117,7 @@ fields, so you no longer have to deal with form themes:
+.. versionadded:: 7.3
+
+ The ``field_id()`` helper was introduced in Symfony 7.3.
+
Form Rendering Variables
------------------------
diff --git a/form/without_class.rst b/form/without_class.rst
index c0da3c9db5a..c31ff346170 100644
--- a/form/without_class.rst
+++ b/form/without_class.rst
@@ -96,12 +96,12 @@ but here's a short example::
{
$builder
->add('firstName', TextType::class, [
- 'constraints' => new Length(['min' => 3]),
+ 'constraints' => new Length(min: 3),
])
->add('lastName', TextType::class, [
'constraints' => [
new NotBlank(),
- new Length(['min' => 3]),
+ new Length(min: 3),
],
])
;
@@ -153,10 +153,10 @@ This can be done by setting the ``constraints`` option in the
$resolver->setDefaults([
'data_class' => null,
'constraints' => new Collection([
- 'firstName' => new Length(['min' => 3]),
+ 'firstName' => new Length(min: 3),
'lastName' => [
new NotBlank(),
- new Length(['min' => 3]),
+ new Length(min: 3),
],
]),
]);
diff --git a/frontend/asset_mapper.rst b/frontend/asset_mapper.rst
index 60cf6c40e8b..454e13a1d29 100644
--- a/frontend/asset_mapper.rst
+++ b/frontend/asset_mapper.rst
@@ -215,6 +215,15 @@ to add any `npm package`_:
$ php bin/console importmap:require bootstrap
+.. tip::
+
+ Add the ``--dry-run`` option to simulate package installation without actually
+ making any changes (e.g. ``php bin/console importmap:require bootstrap --dry-run``)
+
+ .. versionadded:: 7.3
+
+ The ``--dry-run`` option was introduced in Symfony 7.3.
+
This adds the ``bootstrap`` package to your ``importmap.php`` file::
// importmap.php
@@ -676,7 +685,9 @@ which will automatically do most of these things for you:
- **Compress your assets**: Your web server should compress (e.g. using gzip)
your assets (JavaScript, CSS, images) before sending them to the browser. This
is automatically enabled in Caddy and can be activated in Nginx and Apache.
- In Cloudflare, assets are compressed by default.
+ In Cloudflare, assets are compressed by default. AssetMapper also supports
+ :ref:`precompressing your web assets ` to further
+ improve performance.
- **Set long-lived cache expiry**: Your web server should set a long-lived
``Cache-Control`` HTTP header on your assets. Because the AssetMapper component includes a version
@@ -724,6 +735,85 @@ even though it hasn't yet seen the ``import`` statement for them.
Additionally, if the :doc:`WebLink Component ` is available in your application,
Symfony will add a ``Link`` header in the response to preload the CSS files.
+.. _performance-precompressing:
+
+Pre-Compressing Assets
+----------------------
+
+.. 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.
+
+This improves performance because you can compress assets using the highest (and
+slowest) compression ratios beforehand and provide those compressed assets to the
+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 machine that pre-compresses assets must have
+installed the following PHP extensions or CLI commands:
+
+* Brotli: ``brotli`` CLI command; `brotli PHP extension`_;
+* Zstandard: ``zstd`` CLI command; `zstd PHP extension`_;
+* gzip: ``zopfli`` (better) or ``gzip`` CLI command; `zlib PHP extension`_.
+
+Then, update your AssetMapper configuration to define which compression to use
+and which file extensions should be compressed:
+
+.. code-block:: yaml
+
+ # config/packages/asset_mapper.yaml
+ framework:
+ asset_mapper:
+ # ...
+
+ 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']
+
+Now, when running the ``asset-map:compile`` command, all matching files will be
+compressed in the configured format and at the highest compression level. The
+compressed files are created with the same name as the original but with the
+``.br``, ``.zst``, or ``.gz`` extension appended.
+
+Then, you need to configure your web server to serve the precompressed assets
+instead of the original ones:
+
+.. configuration-block::
+
+ .. code-block:: caddy
+
+ file_server {
+ precompressed br zstd gzip
+ }
+
+ .. code-block:: nginx
+
+ gzip_static on;
+
+ # Requires https://github.com/google/ngx_brotli
+ brotli_static on;
+
+ # Requires https://github.com/tokers/zstd-nginx-module
+ zstd_static on;
+
+.. tip::
+
+ AssetMapper provides an ``assets:compress`` CLI command and a service called
+ ``asset_mapper.compressor`` that you can use anywhere in your application to
+ compress any kind of files (e.g. files uploaded by users to your application).
+
Frequently Asked Questions
--------------------------
@@ -1220,3 +1310,9 @@ command as part of your CI to be warned anytime a new vulnerability is found.
.. _strict-dynamic: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#strict-dynamic
.. _kocal/biome-js-bundle: https://github.com/Kocal/BiomeJsBundle
.. _`SensioLabs Minify Bundle`: https://github.com/sensiolabs/minify-bundle
+.. _`Brotli`: https://en.wikipedia.org/wiki/Brotli
+.. _`Zstandard`: https://en.wikipedia.org/wiki/Zstd
+.. _`gzip`: https://en.wikipedia.org/wiki/Gzip
+.. _`brotli PHP extension`: https://pecl.php.net/package/brotli
+.. _`zstd PHP extension`: https://pecl.php.net/package/zstd
+.. _`zlib PHP extension`: https://www.php.net/manual/en/book.zlib.php
diff --git a/logging/monolog_console.rst b/logging/monolog_console.rst
index 67bf0f5acae..4d007abe854 100644
--- a/logging/monolog_console.rst
+++ b/logging/monolog_console.rst
@@ -10,10 +10,9 @@ When a lot of logging has to happen, it's cumbersome to print information
depending on the verbosity settings (``-v``, ``-vv``, ``-vvv``) because the
calls need to be wrapped in conditions. For example::
- use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(OutputInterface $output): int
{
if ($output->isDebug()) {
$output->writeln('Some info');
@@ -34,23 +33,22 @@ the current log level and the console verbosity.
The example above could then be rewritten as::
- // src/Command/YourCommand.php
+ // src/Command/MyCommand.php
namespace App\Command;
use Psr\Log\LoggerInterface;
+ use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
- use Symfony\Component\Console\Input\InputInterface;
- use Symfony\Component\Console\Output\OutputInterface;
- class YourCommand extends Command
+ #[AsCommand(name: 'app:my-command')]
+ class MyCommand
{
public function __construct(
private LoggerInterface $logger,
) {
- parent::__construct();
}
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(): int
{
$this->logger->debug('Some info');
$this->logger->notice('Some more info');
diff --git a/mailer.rst b/mailer.rst
index 075283bfe12..46517dfd968 100644
--- a/mailer.rst
+++ b/mailer.rst
@@ -100,6 +100,7 @@ via a third-party provider:
===================== =============================================== ===============
Service Install with Webhook support
===================== =============================================== ===============
+`AhaSend`_ ``composer require symfony/aha-send-mailer`` yes
`Amazon SES`_ ``composer require symfony/amazon-mailer``
`Azure`_ ``composer require symfony/azure-mailer``
`Brevo`_ ``composer require symfony/brevo-mailer`` yes
@@ -127,6 +128,10 @@ Service Install with Webhook su
The Mailomat, Mailtrap, Postal and Sweego integrations were introduced in Symfony 7.2.
+.. versionadded:: 7.3
+
+ The AhaSend integration was introduced in Symfony 7.3.
+
.. note::
As a convenience, Symfony also provides support for Gmail (``composer
@@ -175,6 +180,10 @@ party provider:
+------------------------+---------------------------------------------------------+
| Provider | Formats |
+========================+=========================================================+
+| `AhaSend`_ | - API ``ahasend+api://KEY@default`` |
+| | - HTTP n/a |
+| | - SMTP ``ahasend+smtp://USERNAME:PASSWORD@default`` |
++------------------------+---------------------------------------------------------+
| `Amazon SES`_ | - SMTP ``ses+smtp://USERNAME:PASSWORD@default`` |
| | - HTTP ``ses+https://ACCESS_KEY:SECRET_KEY@default`` |
| | - API ``ses+api://ACCESS_KEY:SECRET_KEY@default`` |
@@ -259,12 +268,6 @@ party provider:
you need to add the ``ping_threshold`` parameter to your ``MAILER_DSN`` with
a value lower than ``10``: ``ses+smtp://USERNAME:PASSWORD@default?ping_threshold=9``
-.. warning::
-
- If you send custom headers when using the `Amazon SES`_ transport (to receive
- them later via a webhook), make sure to use the ``ses+https`` provider because
- it's the only one that supports them.
-
.. note::
When using SMTP, the default timeout for sending a message before throwing an
@@ -331,6 +334,17 @@ The failover-transport starts using the first transport and if it fails, it
will retry the same delivery with the next transports until one of them succeeds
(or until all of them fail).
+By default, delivery is retried 60 seconds after a failed attempt. You can adjust
+the retry period by setting the ``retry_period`` option in the DSN:
+
+.. code-block:: env
+
+ MAILER_DSN="failover(postmark+api://ID@default sendgrid+smtp://KEY@default)?retry_period=15"
+
+.. versionadded:: 7.3
+
+ The ``retry_period`` option was introduced in Symfony 7.3.
+
Load Balancing
~~~~~~~~~~~~~~
@@ -351,6 +365,17 @@ As with the failover transport, round-robin retries deliveries until
a transport succeeds (or all fail). In contrast to the failover transport,
it *spreads* the load across all its transports.
+By default, delivery is retried 60 seconds after a failed attempt. You can adjust
+the retry period by setting the ``retry_period`` option in the DSN:
+
+.. code-block:: env
+
+ MAILER_DSN="roundrobin(postmark+api://ID@default sendgrid+smtp://KEY@default)?retry_period=15"
+
+.. versionadded:: 7.3
+
+ The ``retry_period`` option was introduced in Symfony 7.3.
+
TLS Peer Verification
~~~~~~~~~~~~~~~~~~~~~
@@ -395,6 +420,49 @@ setting the ``auto_tls`` option to ``false`` in the DSN::
This setting only works when the ``smtp://`` protocol is used.
+Ensure TLS
+~~~~~~~~~~
+
+You may want to ensure that TLS is used (either directly or via ``STARTTLS``)
+when sending mail over SMTP, regardless of other options or SMTP server support.
+To require TLS, call ``setRequireTls(true)`` on the ``EsmtpTransport`` instance,
+or set the ``require_tls`` option to ``true`` in the DSN::
+
+ $dsn = 'smtp://user:pass@10.0.0.25?require_tls=true';
+
+When TLS is required, a :class:`Symfony\\Component\\Mailer\\Exception\\TransportException`
+is thrown if a TLS connection cannot be established during the initial communication
+with the SMTP server.
+
+.. note::
+
+ This setting only applies when using the ``smtp://`` protocol.
+
+.. versionadded:: 7.3
+
+ The ``require_tls`` option was introduced in Symfony 7.3.
+
+Binding to IPv4 or IPv6
+~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 7.3
+
+ The option to bind to IPv4, or IPv6, or a specific IP address was introduced in Symfony 7.3.
+
+By default, the underlying ``SocketStream`` will bind to IPv4 or IPv6 based on the
+available interfaces. You can enforce binding to a specific protocol or IP address
+by using the ``source_ip`` option. To bind to IPv4, use::
+
+ $dsn = 'smtp://smtp.example.com?source_ip=0.0.0.0';
+
+As per RFC2732, IPv6 addresses must be enclosed in square brackets. To bind to IPv6, use::
+
+ $dsn = 'smtp://smtp.example.com?source_ip=[::]';
+
+.. note::
+
+ This option only works when using the ``smtp://`` protocol.
+
Overriding default SMTP authenticators
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1047,6 +1115,18 @@ the email contents:
Welcome {{ email.toName }}!
{# ... #}
+By default this will create an attachment using the file path as file name:
+``Content-Disposition: inline; name="cid..."; filename="@images/logo.png"``.
+This behavior can be overridden by passing a custom file name as the third argument:
+
+.. code-block:: html+twig
+
+
+
+.. versionadded:: 7.3
+
+ The third argument of ``email.image()`` was introduced in Symfony 7.3.
+
.. _mailer-inline-css:
Inlining CSS Styles
@@ -1344,6 +1424,81 @@ key but not a certificate::
->toArray()
);
+Signing Messages Globally
+.........................
+
+Instead of creating a signer instance for each email, you can configure a global
+signer that automatically applies to all outgoing messages. This approach
+minimizes repetition and centralizes your configuration for DKIM and S/MIME signing.
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/mailer.yaml
+ framework:
+ mailer:
+ dkim_signer:
+ key: 'file://%kernel.project_dir%/var/certificates/dkim.pem'
+ domain: 'symfony.com'
+ select: 's1'
+ smime_signer:
+ key: '%kernel.project_dir%/var/certificates/smime.key'
+ certificate: '%kernel.project_dir%/var/certificates/smime.crt'
+ passphrase: ''
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+ file://%kernel.project_dir%/var/certificates/dkim.pem
+ symfony.com
+ s1
+
+
+ %kernel.project_dir%/var/certificates/smime.pem
+ %kernel.project_dir%/var/certificates/smime.crt
+
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/mailer.php
+ use Symfony\Config\FrameworkConfig;
+
+ return static function (FrameworkConfig $framework): void {
+ $mailer = $framework->mailer();
+ $mailer->dsn('%env(MAILER_DSN)%');
+ $mailer->dkimSigner()
+ ->key('file://%kernel.project_dir%/var/certificates/dkim.pem')
+ ->domain('symfony.com')
+ ->select('s1');
+
+ $mailer->smimeSigner()
+ ->key('%kernel.project_dir%/var/certificates/smime.key')
+ ->certificate('%kernel.project_dir%/var/certificates/smime.crt')
+ ->passphrase('')
+ ;
+ };
+
+.. versionadded:: 7.3
+
+ Global message signing was introduced in Symfony 7.3.
+
Encrypting Messages
~~~~~~~~~~~~~~~~~~~
@@ -1385,6 +1540,86 @@ and it will select the appropriate certificate depending on the ``To`` option::
$firstEncryptedEmail = $encrypter->encrypt($firstEmail);
$secondEncryptedEmail = $encrypter->encrypt($secondEmail);
+Encrypting Messages Globally
+............................
+
+Instead of creating a new encrypter for each email, you can configure a global S/MIME
+encrypter that automatically applies to all outgoing messages:
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/mailer.yaml
+ framework:
+ mailer:
+ smime_encrypter:
+ repository: App\Security\LocalFileCertificateRepository
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+ App\Security\LocalFileCertificateRepository
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/mailer.php
+ use App\Security\LocalFileCertificateRepository;
+ use Symfony\Config\FrameworkConfig;
+
+ return static function (FrameworkConfig $framework): void {
+ $mailer = $framework->mailer();
+ $mailer->smimeEncrypter()
+ ->repository(LocalFileCertificateRepository::class)
+ ;
+ };
+
+The ``repository`` option is the ID of a service that implements
+:class:`Symfony\\Component\\Mailer\\EventListener\\SmimeCertificateRepositoryInterface`.
+This interface requires only one method: ``findCertificatePathFor()``, which must
+return the file path to the certificate associated with the given email address::
+
+ namespace App\Security;
+
+ use Symfony\Component\DependencyInjection\Attribute\Autowire;
+ use Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface;
+
+ class LocalFileCertificateRepository implements SmimeCertificateRepositoryInterface
+ {
+ public function __construct(
+ #[Autowire(param: 'kernel.project_dir')]
+ private readonly string $projectDir
+ ){}
+
+ public function findCertificatePathFor(string $email): ?string
+ {
+ $hash = hash('sha256', strtolower(trim($email)));
+ $path = sprintf('%s/storage/%s.crt', $this->projectDir, $hash);
+
+ return file_exists($path) ? $path : null;
+ }
+ }
+
+.. versionadded:: 7.3
+
+ Global message encryption configuration was introduced in Symfony 7.3.
+
.. _multiple-email-transports:
Multiple Email Transports
@@ -2033,6 +2268,7 @@ the :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\MailerAssertionsTrait`::
following the redirection and the message will be lost from the mailer event
handler.
+.. _`AhaSend`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/AhaSend/README.md
.. _`Amazon SES`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Amazon/README.md
.. _`Azure`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Azure/README.md
.. _`App Password`: https://support.google.com/accounts/answer/185833
diff --git a/messenger.rst b/messenger.rst
index 97b95565fa7..18fc5e03cec 100644
--- a/messenger.rst
+++ b/messenger.rst
@@ -555,7 +555,7 @@ the message from being redelivered until the worker completes processing it:
.. note::
- This option is only available for the following transports: Beanstalkd and AmazonSQS.
+ This option is only available for the following transports: Beanstalkd, AmazonSQS, Doctrine and Redis.
.. versionadded:: 7.2
@@ -849,7 +849,56 @@ message before terminating.
However, you might prefer to use different POSIX signals for graceful shutdown.
You can override default ones by setting the ``framework.messenger.stop_worker_on_signals``
-configuration option.
+configuration option:
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/messenger.yaml
+ framework:
+ messenger:
+ stop_worker_on_signals:
+ - SIGTERM
+ - SIGINT
+ - SIGUSR1
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+ SIGTERM
+ SIGINT
+ SIGUSR1
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/messenger.php
+ use Symfony\Config\FrameworkConfig;
+
+ return static function (FrameworkConfig $framework): void {
+ $framework->messenger()
+ ->stopWorkerOnSignals(['SIGTERM', 'SIGINT', 'SIGUSR1']);
+ };
+
+.. versionadded:: 7.3
+
+ Support for signals plain names in configuration was introduced in Symfony 7.3.
+ Previously, you had to use the numeric values of signals as defined by the
+ ``pcntl`` extension's `predefined constants`_.
In some cases the ``SIGTERM`` signal is sent by Supervisor itself (e.g. stopping
a Docker container having Supervisor as its entrypoint). In these cases you
@@ -1263,6 +1312,9 @@ to retry them:
# remove all messages in the failure transport
$ php bin/console messenger:failed:remove --all
+ # remove only App\Message\MyMessage messages
+ $ php bin/console messenger:failed:remove --class-filter='App\Message\MyMessage'
+
If the message fails again, it will be re-sent back to the failure transport
due to the normal :ref:`retry rules `. Once the max
retry has been hit, the message will be discarded permanently.
@@ -1272,6 +1324,11 @@ retry has been hit, the message will be discarded permanently.
The option to skip a message in the ``messenger:failed:retry`` command was
introduced in Symfony 7.2
+.. versionadded:: 7.3
+
+ The option to filter by a message class in the ``messenger:failed:remove`` command was
+ introduced in Symfony 7.3
+
Multiple Failed Transports
~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1588,11 +1645,15 @@ The transport has a number of options:
Exchange flags
``exchange[name]``
- Name of the exchange
+ Name of the exchange. Use an empty string to use the default exchange.
``exchange[type]`` (default: ``fanout``)
Type of exchange
+.. versionadded:: 7.3
+
+ Empty string support for ``exchange[name]`` was introduced in Symfony 7.3.
+
You can also configure AMQP-specific settings on your message by adding
:class:`Symfony\\Component\\Messenger\\Bridge\\Amqp\\Transport\\AmqpStamp` to
your Envelope::
@@ -1696,6 +1757,13 @@ in the table.
The length of time to wait for a response when calling
``PDO::pgsqlGetNotify``, in milliseconds.
+The Doctrine transport supports the ``--keepalive`` option by periodically updating
+the ``delivered_at`` timestamp to prevent the message from being redelivered.
+
+.. versionadded:: 7.3
+
+ Keepalive support was introduced in Symfony 7.3.
+
Beanstalkd Transport
~~~~~~~~~~~~~~~~~~~~
@@ -1718,8 +1786,13 @@ The Beanstalkd transport DSN may looks like this:
The transport has a number of options:
-``tube_name`` (default: ``default``)
- Name of the queue
+``bury_on_reject`` (default: ``false``)
+ When set to ``true``, rejected messages are placed into a "buried" state
+ in Beanstalkd instead of being deleted.
+
+ .. versionadded:: 7.3
+
+ The ``bury_on_reject`` option was introduced in Symfony 7.3.
``timeout`` (default: ``0``)
Message reservation timeout - in seconds. 0 will cause the server to
@@ -1729,6 +1802,9 @@ The transport has a number of options:
The message time to run before it is put back in the ready queue - in
seconds.
+``tube_name`` (default: ``default``)
+ Name of the queue
+
The Beanstalkd transport supports the ``--keepalive`` option by using Beanstalkd's
``touch`` command to periodically reset the job's ``ttr``.
@@ -1736,6 +1812,23 @@ The Beanstalkd transport supports the ``--keepalive`` option by using Beanstalkd
Keepalive support was introduced in Symfony 7.2.
+The Beanstalkd transport lets you set the priority of the messages being dispatched.
+Use the :class:`Symfony\\Component\\Messenger\\Bridge\\Beanstalkd\\Transport\\BeanstalkdPriorityStamp`
+and pass a number to specify the priority (default = ``1024``; lower numbers mean higher priority)::
+
+ use App\Message\SomeMessage;
+ use Symfony\Component\Messenger\Stamp\BeanstalkdPriorityStamp;
+
+ $this->bus->dispatch(new SomeMessage('some data'), [
+ // 0 = highest priority
+ // 2**32 - 1 = lowest priority
+ new BeanstalkdPriorityStamp(0),
+ ]);
+
+.. versionadded:: 7.3
+
+ ``BeanstalkdPriorityStamp`` support was introduced in Symfony 7.3.
+
.. _messenger-redis-transport:
Redis Transport
@@ -1882,6 +1975,13 @@ under the transport in ``messenger.yaml``:
in your case) to avoid memory leaks. Otherwise, all messages will remain
forever in Redis.
+The Redis transport supports the ``--keepalive`` option by using Redis's ``XCLAIM``
+command to periodically reset the message's idle time to zero.
+
+.. versionadded:: 7.3
+
+ Keepalive support was introduced in Symfony 7.3.
+
In Memory Transport
~~~~~~~~~~~~~~~~~~~
@@ -2024,6 +2124,12 @@ The transport has a number of options:
``queue_name`` (default: ``messages``)
Name of the queue
+``queue_attributes``
+ Attributes of a queue as per `SQS CreateQueue API`_. Array of strings indexed by keys of ``AsyncAws\Sqs\Enum\QueueAttributeName``.
+
+``queue_tags``
+ Cost allocation tags of a queue as per `SQS CreateQueue API`_. Array of strings indexed by strings.
+
``region`` (default: ``eu-west-1``)
Name of the AWS region
@@ -2039,6 +2145,10 @@ The transport has a number of options:
``wait_time`` (default: ``20``)
`Long polling`_ duration in seconds
+.. versionadded:: 7.3
+
+ The ``queue_attributes`` and ``queue_tags`` options were introduced in Symfony 7.3.
+
.. note::
The ``wait_time`` parameter defines the maximum duration Amazon SQS should
@@ -2161,6 +2271,22 @@ on a case-by-case basis via the :class:`Symfony\\Component\\Messenger\\Stamp\\Se
provides that control. See `SymfonyCasts' message serializer tutorial`_ for
details.
+Closing Connections
+~~~~~~~~~~~~~~~~~~~
+
+When using a transport that requires a connection, you can close it by calling the
+:method:`Symfony\\Component\\Messenger\\Transport\\CloseableTransportInterface::close`
+method to free up resources in long-running processes.
+
+This interface is implemented by the following transports: AmazonSqs, Amqp, and Redis.
+If you need to close a Doctrine connection, you can do so
+:ref:`using middleware `.
+
+.. versionadded:: 7.3
+
+ The ``CloseableTransportInterface`` and its ``close()`` method were introduced
+ in Symfony 7.3.
+
Running Commands And External Processes
---------------------------------------
@@ -2216,8 +2342,9 @@ will take care of creating a new process with the parameters you passed::
class CleanUpService
{
- public function __construct(private readonly MessageBusInterface $bus)
- {
+ public function __construct(
+ private readonly MessageBusInterface $bus,
+ ) {
}
public function cleanUp(): void
@@ -2228,6 +2355,34 @@ will take care of creating a new process with the parameters you passed::
}
}
+If you want to use shell features such as redirections or pipes, use the static
+:method:`Symfony\\Component\\Process\\Messenger\\RunProcessMessage::fromShellCommandline` factory method::
+
+ use Symfony\Component\Messenger\MessageBusInterface;
+ use Symfony\Component\Process\Messenger\RunProcessMessage;
+
+ class CleanUpService
+ {
+ public function __construct(
+ private readonly MessageBusInterface $bus,
+ ) {
+ }
+
+ public function cleanUp(): void
+ {
+ $this->bus->dispatch(RunProcessMessage::fromShellCommandline('echo "Hello World" > var/log/hello.txt'));
+
+ // ...
+ }
+ }
+
+For more information, read the documentation about
+:ref:`using features from the OS shell `.
+
+.. versionadded:: 7.3
+
+ The ``RunProcessMessage::fromShellCommandline()`` method was introduced in Symfony 7.3.
+
Once handled, the handler will return a
:class:`Symfony\\Component\\Process\\Messenger\\RunProcessContext` which
contains many useful information such as the exit code or the output of the
@@ -2374,6 +2529,15 @@ wherever you need a query bus behavior instead of the ``MessageBusInterface``::
}
}
+You can also add new stamps when handling a message; they will be appended
+to the existing ones::
+
+ $this->handle(new SomeMessage($data), [new SomeStamp(), new AnotherStamp()]);
+
+.. versionadded:: 7.3
+
+ The ``$stamps`` parameter of the ``handle()`` method was introduced in Symfony 7.3.
+
Customizing Handlers
--------------------
@@ -3600,3 +3764,5 @@ Learn more
.. _`high connection churn`: https://www.rabbitmq.com/connections.html#high-connection-churn
.. _`article about CQRS`: https://martinfowler.com/bliki/CQRS.html
.. _`SSL context options`: https://php.net/context.ssl
+.. _`predefined constants`: https://www.php.net/pcntl.constants
+.. _`SQS CreateQueue API`: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_CreateQueue.html
diff --git a/notifier.rst b/notifier.rst
index d2dc225f4f0..49a1c2d533b 100644
--- a/notifier.rst
+++ b/notifier.rst
@@ -68,6 +68,7 @@ Service
`AllMySms`_ **Install**: ``composer require symfony/all-my-sms-notifier`` \
**DSN**: ``allmysms://LOGIN:APIKEY@default?from=FROM`` \
**Webhook support**: No
+ **Extra properties in SentMessage**: ``nbSms``, ``balance``, ``cost``
`AmazonSns`_ **Install**: ``composer require symfony/amazon-sns-notifier`` \
**DSN**: ``sns://ACCESS_KEY:SECRET_KEY@default?region=REGION`` \
**Webhook support**: No
@@ -76,7 +77,7 @@ Service
**Webhook support**: No
`Brevo`_ **Install**: ``composer require symfony/brevo-notifier`` \
**DSN**: ``brevo://API_KEY@default?sender=SENDER`` \
- **Webhook support**: No
+ **Webhook support**: Yes
`Clickatell`_ **Install**: ``composer require symfony/clickatell-notifier`` \
**DSN**: ``clickatell://ACCESS_TOKEN@default?from=FROM`` \
**Webhook support**: No
@@ -139,6 +140,7 @@ Service
`OvhCloud`_ **Install**: ``composer require symfony/ovh-cloud-notifier`` \
**DSN**: ``ovhcloud://APPLICATION_KEY:APPLICATION_SECRET@default?consumer_key=CONSUMER_KEY&service_name=SERVICE_NAME`` \
**Webhook support**: No
+ **Extra properties in SentMessage**:: ``totalCreditsRemoved``
`Plivo`_ **Install**: ``composer require symfony/plivo-notifier`` \
**DSN**: ``plivo://AUTH_ID:AUTH_TOKEN@default?from=FROM`` \
**Webhook support**: No
@@ -177,7 +179,7 @@ Service
**Webhook support**: No
`Smsbox`_ **Install**: ``composer require symfony/smsbox-notifier`` \
**DSN**: ``smsbox://APIKEY@default?mode=MODE&strategy=STRATEGY&sender=SENDER`` \
- **Webhook support**: No
+ **Webhook support**: Yes
`SmsBiuras`_ **Install**: ``composer require symfony/sms-biuras-notifier`` \
**DSN**: ``smsbiuras://UID:API_KEY@default?from=FROM&test_mode=0`` \
**Webhook support**: No
@@ -236,6 +238,12 @@ Service
The ``Primotexto``, ``Sipgate`` and ``Sweego`` integrations were introduced in Symfony 7.2.
+.. versionadded:: 7.3
+
+ Webhook support for the ``Brevo`` integration was introduced in Symfony 7.3.
+ The extra properties in ``SentMessage`` for ``AllMySms`` and ``OvhCloud``
+ providers were introduced in Symfony 7.3 too.
+
.. deprecated:: 7.1
The `Sms77`_ integration is deprecated since
@@ -357,6 +365,7 @@ Service
**DSN**: ``sns://ACCESS_KEY:SECRET_KEY@default?region=REGION``
`Bluesky`_ **Install**: ``composer require symfony/bluesky-notifier`` \
**DSN**: ``bluesky://USERNAME:PASSWORD@default``
+ **Extra properties in SentMessage**: ``cid``
`Chatwork`_ **Install**: ``composer require symfony/chatwork-notifier`` \
**DSN**: ``chatwork://API_TOKEN@default?room_id=ID``
`Discord`_ **Install**: ``composer require symfony/discord-notifier`` \
@@ -375,6 +384,8 @@ Service
**DSN**: ``linkedin://TOKEN:USER_ID@default``
`Mastodon`_ **Install**: ``composer require symfony/mastodon-notifier`` \
**DSN**: ``mastodon://ACCESS_TOKEN@HOST``
+`Matrix`_ **Install**: ``composer require symfony/matrix-notifier`` \
+ **DSN**: ``matrix://HOST:PORT/?accessToken=ACCESSTOKEN&ssl=SSL``
`Mattermost`_ **Install**: ``composer require symfony/mattermost-notifier`` \
**DSN**: ``mattermost://ACCESS_TOKEN@HOST/PATH?channel=CHANNEL``
`Mercure`_ **Install**: ``composer require symfony/mercure-notifier`` \
@@ -408,6 +419,10 @@ Service
The ``Gitter`` integration was removed in Symfony 7.2 because that service
no longer provides an API.
+.. versionadded:: 7.3
+
+ The ``Matrix`` integration was introduced in Symfony 7.3.
+
.. warning::
By default, if you have the :doc:`Messenger component ` installed,
@@ -1283,6 +1298,7 @@ is dispatched. Listeners receive a
.. _`LOX24`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Lox24/README.md
.. _`Mailjet`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Mailjet/README.md
.. _`Mastodon`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Mastodon/README.md
+.. _`Matrix`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Matrix/README.md
.. _`Mattermost`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Mattermost/README.md
.. _`Mercure`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Mercure/README.md
.. _`MessageBird`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/MessageBird/README.md
diff --git a/object_mapper.rst b/object_mapper.rst
new file mode 100644
index 00000000000..625466ffefc
--- /dev/null
+++ b/object_mapper.rst
@@ -0,0 +1,738 @@
+Object Mapper
+=============
+
+.. versionadded:: 7.3
+
+ The ObjectMapper component was introduced in Symfony 7.3 as an
+ :doc:`experimental feature `.
+
+This component transforms one object into another, simplifying tasks such as
+converting DTOs (Data Transfer Objects) into entities or vice versa. It can also
+be helpful when decoupling API input/output from internal models, particularly
+when working with legacy code or implementing hexagonal architectures.
+
+Installation
+------------
+
+Run this command to install the component before using it:
+
+.. code-block:: terminal
+
+ $ composer require symfony/object-mapper
+
+Usage
+-----
+
+The object mapper service will be :doc:`autowired `
+automatically in controllers or services when type-hinting for
+:class:`Symfony\\Component\\ObjectMapper\\ObjectMapperInterface`::
+
+ // src/Controller/UserController.php
+ namespace App\Controller;
+
+ use App\Dto\UserInput;
+ use App\Entity\User;
+ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+ use Symfony\Component\HttpFoundation\Response;
+ use Symfony\Component\ObjectMapper\ObjectMapperInterface;
+
+ class UserController extends AbstractController
+ {
+ public function updateUser(UserInput $userInput, ObjectMapperInterface $objectMapper): Response
+ {
+ $user = new User();
+ // Map properties from UserInput to User
+ $objectMapper->map($userInput, $user);
+
+ // ... persist $user and return response
+ return new Response('User updated!');
+ }
+ }
+
+Basic Mapping
+-------------
+
+The core functionality is provided by the ``map()`` method. It accepts a
+source object and maps its properties to a target. The target can either be
+a class name (to create a new instance) or an existing object (to update it).
+
+Mapping to a New Object
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Provide the target class name as the second argument::
+
+ use App\Dto\ProductInput;
+ use App\Entity\Product;
+ use Symfony\Component\ObjectMapper\ObjectMapper;
+
+ $productInput = new ProductInput();
+ $productInput->name = 'Wireless Mouse';
+ $productInput->sku = 'WM-1024';
+
+ $mapper = new ObjectMapper();
+ // creates a new Product instance and maps properties from $productInput
+ $product = $mapper->map($productInput, Product::class);
+
+ // $product is now an instance of Product
+ // with $product->name = 'Wireless Mouse' and $product->sku = 'WM-1024'
+
+Mapping to an Existing Object
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Provide an existing object instance as the second argument to update it::
+
+ use App\Dto\ProductUpdateInput;
+ use App\Entity\Product;
+ use Symfony\Component\ObjectMapper\ObjectMapper;
+
+ $product = $productRepository->find(1);
+
+ $updateInput = new ProductUpdateInput();
+ $updateInput->price = 99.99;
+
+ $mapper = new ObjectMapper();
+ // updates the existing $product instance
+ $mapper->map($updateInput, $product);
+
+ // $product->price is now 99.99
+
+Mapping from ``stdClass``
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The source object can also be an instance of ``stdClass``. This can be
+useful when working with decoded JSON data or loosely typed input::
+
+ use App\Entity\Product;
+ use Symfony\Component\ObjectMapper\ObjectMapper;
+
+ $productData = new \stdClass();
+ $productData->name = 'Keyboard';
+ $productData->sku = 'KB-001';
+
+ $mapper = new ObjectMapper();
+ $product = $mapper->map($productData, Product::class);
+
+ // $product is an instance of Product with properties mapped from $productData
+
+Configuring Mapping with Attributes
+-----------------------------------
+
+ObjectMapper uses PHP attributes to configure how properties are mapped.
+The primary attribute is :class:`Symfony\\Component\\ObjectMapper\\Attribute\\Map`.
+
+Defining the Default Target Class
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Apply ``#[Map]`` to the source class to define its default mapping target::
+
+ // src/Dto/ProductInput.php
+ namespace App\Dto;
+
+ use App\Entity\Product;
+ use Symfony\Component\ObjectMapper\Attribute\Map;
+
+ #[Map(target: Product::class)]
+ class ProductInput
+ {
+ public string $name = '';
+ public string $sku = '';
+ }
+
+ // now you can call map() without the second argument if ProductInput is the source:
+ $mapper = new ObjectMapper();
+ $product = $mapper->map($productInput); // Maps to Product automatically
+
+Configuring Property Mapping
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can apply the ``#[Map]`` attribute to properties to customize their mapping behavior:
+
+* ``target``: Specifies the name of the property in the target object;
+* ``source``: Specifies the name of the property in the source object (useful
+ when mapping is defined on the target, see below);
+* ``if``: Defines a condition for mapping the property;
+* ``transform``: Applies a transformation to the value before mapping.
+
+This is how it looks in practice::
+
+ // src/Dto/OrderInput.php
+ namespace App\Dto;
+
+ use App\Entity\Order;
+ use Symfony\Component\ObjectMapper\Attribute\Map;
+
+ #[Map(target: Order::class)]
+ class OrderInput
+ {
+ // map 'customerEmail' from source to 'email' in target
+ #[Map(target: 'email')]
+ public string $customerEmail = '';
+
+ // do not map this property at all
+ #[Map(if: false)]
+ public string $internalNotes = '';
+
+ // only map 'discountCode' if it's a non-empty string
+ // (uses PHP's strlen() function as a condition)
+ #[Map(if: 'strlen')]
+ public ?string $discountCode = null;
+ }
+
+By default, if a property exists in the source but not in the target, it is
+ignored. If a property exists in both and no ``#[Map]`` is defined, the mapper
+assumes a direct mapping when names match.
+
+Conditional Mapping with Services
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For complex conditions, you can use a dedicated service implementing
+:class:`Symfony\\Component\\ObjectMapper\\ConditionCallableInterface`::
+
+ // src/ObjectMapper/IsShippableCondition.php
+ namespace App\ObjectMapper;
+
+ use App\Dto\OrderInput;
+ use App\Entity\Order; // Target type hint
+ use Symfony\Component\ObjectMapper\ConditionCallableInterface;
+
+ /**
+ * @implements ConditionCallableInterface
+ */
+ final class IsShippableCondition implements ConditionCallableInterface
+ {
+ public function __invoke(mixed $value, object $source, ?object $target): bool
+ {
+ // example: Only map shipping address if order total is above 50
+ return $source->total > 50;
+ }
+ }
+
+Then, pass the service name (its class name by default) to the ``if`` parameter::
+
+ // src/Dto/OrderInput.php
+ namespace App\Dto;
+
+ use App\Entity\Order;
+ use App\ObjectMapper\IsShippableCondition;
+ use Symfony\Component\ObjectMapper\Attribute\Map;
+
+ #[Map(target: Order::class)]
+ class OrderInput
+ {
+ public float $total = 0.0;
+
+ #[Map(if: IsShippableCondition::class)]
+ public ?string $shippingAddress = null;
+ }
+
+For this to work, ``IsShippableCondition`` must be registered as a service.
+
+.. _object_mapper-conditional-property-target:
+
+Conditional Property Mapping based on Target
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When a source class maps to multiple targets, you may want to include or exclude
+certain properties depending on which target is being used. Use the
+:class:`Symfony\\Component\\ObjectMapper\\Condition\\TargetClass` condition within
+the ``if`` parameter of a property's ``#[Map]`` attribute to achieve this.
+
+This pattern is useful for building multiple representations (e.g., public vs. admin)
+from a given source object, and can be used as an alternative to
+:ref:`serialization groups `::
+
+ // src/Entity/User.php
+ namespace App\Entity;
+
+ use App\Dto\AdminUserProfile;
+ use App\Dto\PublicUserProfile;
+ use Symfony\Component\ObjectMapper\Attribute\Map;
+ use Symfony\Component\ObjectMapper\Condition\TargetClass;
+
+ // this User entity can be mapped to two different DTOs
+ #[Map(target: PublicUserProfile::class)]
+ #[Map(target: AdminUserProfile::class)]
+ class User
+ {
+ // map 'lastLoginIp' to 'ipAddress' ONLY when the target is AdminUserProfile
+ #[Map(target: 'ipAddress', if: new TargetClass(AdminUserProfile::class))]
+ public ?string $lastLoginIp = '192.168.1.100';
+
+ // map 'registrationDate' to 'memberSince' for both targets
+ #[Map(target: 'memberSince')]
+ public \DateTimeImmutable $registrationDate;
+
+ public function __construct() {
+ $this->registrationDate = new \DateTimeImmutable();
+ }
+ }
+
+ // src/Dto/PublicUserProfile.php
+ namespace App\Dto;
+ class PublicUserProfile
+ {
+ public \DateTimeImmutable $memberSince;
+ // no $ipAddress property here
+ }
+
+ // src/Dto/AdminUserProfile.php
+ namespace App\Dto;
+ class AdminUserProfile
+ {
+ public \DateTimeImmutable $memberSince;
+ public ?string $ipAddress = null; // mapped from lastLoginIp
+ }
+
+ // usage:
+ $user = new User();
+ $mapper = new ObjectMapper();
+
+ $publicProfile = $mapper->map($user, PublicUserProfile::class);
+ // no IP address available
+
+ $adminProfile = $mapper->map($user, AdminUserProfile::class);
+ // $adminProfile->ipAddress = '192.168.1.100'
+
+Transforming Values
+-------------------
+
+Use the ``transform`` option within ``#[Map]`` to change a value before it is
+assigned to the target. This can be a callable (e.g., a built-in PHP function,
+static method, or anonymous function) or a service implementing
+:class:`Symfony\\Component\\ObjectMapper\\TransformCallableInterface`.
+
+Using Callables
+~~~~~~~~~~~~~~~
+
+Consider the following static utility method::
+
+ // src/Util/PriceFormatter.php
+ namespace App\Util;
+
+ class PriceFormatter
+ {
+ public static function format(float $value, object $source): string
+ {
+ return number_format($value, 2, '.', '');
+ }
+ }
+
+You can use that method to format a property when mapping it::
+
+ // src/Dto/ProductInput.php
+ namespace App\Dto;
+
+ use App\Entity\Product;
+ use App\Util\PriceFormatter;
+ use Symfony\Component\ObjectMapper\Attribute\Map;
+
+ #[Map(target: Product::class)]
+ class ProductInput
+ {
+ // use a static method from another class for formatting
+ #[Map(target: 'displayPrice', transform: [PriceFormatter::class, 'format'])]
+ public float $price = 0.0;
+
+ // can also use built-in PHP functions
+ #[Map(transform: 'intval')]
+ public string $stockLevel = '100';
+ }
+
+Using Transformer Services
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Similar to conditions, complex transformations can be encapsulated in services
+implementing :class:`Symfony\\Component\\ObjectMapper\\TransformCallableInterface`::
+
+ // src/ObjectMapper/FullNameTransformer.php
+ namespace App\ObjectMapper;
+
+ use App\Dto\UserInput;
+ use App\Entity\User;
+ use Symfony\Component\ObjectMapper\TransformCallableInterface;
+
+ /**
+ * @implements TransformCallableInterface
+ */
+ final class FullNameTransformer implements TransformCallableInterface
+ {
+ public function __invoke(mixed $value, object $source, ?object $target): mixed
+ {
+ return trim($source->firstName . ' ' . $source->lastName);
+ }
+ }
+
+Then, use this service to format the mapped property::
+
+ // src/Dto/UserInput.php
+ namespace App\Dto;
+
+ use App\Entity\User;
+ use App\ObjectMapper\FullNameTransformer;
+ use Symfony\Component\ObjectMapper\Attribute\Map;
+
+ #[Map(target: User::class)]
+ class UserInput
+ {
+ // this property's value will be generated by the transformer
+ #[Map(target: 'fullName', transform: FullNameTransformer::class)]
+ public string $firstName = '';
+
+ public string $lastName = '';
+ }
+
+Class-Level Transformation
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can define a transformation at the class level using the ``transform``
+parameter on the ``#[Map]`` attribute. This callable runs *after* the target
+object is created (if the target is a class name, ``newInstanceWithoutConstructor``
+is used), but *before* any properties are mapped. It must return a correctly
+initialized instance of the target class (replacing the one created by the mapper
+if needed)::
+
+ // src/Dto/LegacyUserData.php
+ namespace App\Dto;
+
+ use App\Entity\User;
+ use Symfony\Component\ObjectMapper\Attribute\Map;
+
+ // use a static factory method on the target User class for instantiation
+ #[Map(target: User::class, transform: [User::class, 'createFromLegacy'])]
+ class LegacyUserData
+ {
+ public int $userId = 0;
+ public string $name = '';
+ }
+
+And the related target object must define the ``createFromLegacy()`` method::
+
+ // src/Entity/User.php
+ namespace App\Entity;
+ class User
+ {
+ public string $name = '';
+ private int $legacyId = 0;
+
+ // uses a private constructor to avoid direct instantiation
+ private function __construct() {}
+
+ public static function createFromLegacy(mixed $value, object $source): self
+ {
+ // $value is the initially created (empty) User object
+ // $source is the LegacyUserData object
+ $user = new self();
+ $user->legacyId = $source->userId;
+
+ // property mapping will happen *after* this method returns $user
+ return $user;
+ }
+ }
+
+Mapping Multiple Targets
+------------------------
+
+A source class can be configured to map to multiple different target classes.
+Apply the ``#[Map]`` attribute multiple times at the class level, typically
+using the ``if`` condition to determine which target is appropriate based on the
+source object's state or other logic::
+
+ // src/Dto/EventInput.php
+ namespace App\Dto;
+
+ use App\Entity\OnlineEvent;
+ use App\Entity\PhysicalEvent;
+ use Symfony\Component\ObjectMapper\Attribute\Map;
+
+ #[Map(target: OnlineEvent::class, if: [self::class, 'isOnline'])]
+ #[Map(target: PhysicalEvent::class, if: [self::class, 'isPhysical'])]
+ class EventInput
+ {
+ public string $type = 'online'; // e.g., 'online' or 'physical'
+ public string $title = '';
+
+ /**
+ * In class-level conditions, $value is null.
+ */
+ public static function isOnline(?mixed $value, object $source): bool
+ {
+ return 'online' === $source->type;
+ }
+
+ public static function isPhysical(?mixed $value, object $source): bool
+ {
+ return 'physical' === $source->type;
+ }
+ }
+
+ // consider that the src/Entity/OnlineEvent.php and PhysicalEvent.php
+ // files exist and define the needed classes
+
+ // usage:
+ $eventInput = new EventInput();
+ $eventInput->type = 'physical';
+ $mapper = new ObjectMapper();
+ $event = $mapper->map($eventInput); // automatically maps to PhysicalEvent
+
+Mapping Based on Target Properties (Source Mapping)
+---------------------------------------------------
+
+Sometimes, it's more convenient to define how a target object should retrieve
+its values from a source, especially when working with external data formats.
+This is done using the ``source`` parameter in the ``#[Map]`` attribute on the
+target class's properties.
+
+Note that if both the ``source`` and the ``target`` classes define the ``#[Map]``
+attribute, the ``source`` takes precedence.
+
+Consider the following class that stores the data obtained from an external API
+that uses snake_case property names::
+
+ // src/Api/Payload.php
+ namespace App\Api;
+
+ class Payload
+ {
+ public string $product_name = '';
+ public float $price_amount = 0.0;
+ }
+
+In your application, classes use camelCase for property names, so you can map
+them as follows::
+
+ // src/Entity/Product.php
+ namespace App\Entity;
+
+ use App\Api\Payload;
+ use Symfony\Component\ObjectMapper\Attribute\Map;
+
+ // define that Product can be mapped from Payload
+ #[Map(source: Payload::class)]
+ class Product
+ {
+ // define where 'name' should get its value from in the Payload source
+ #[Map(source: 'product_name')]
+ public string $name = '';
+
+ // define where 'price' should get its value from
+ #[Map(source: 'price_amount')]
+ public float $price = 0.0;
+ }
+
+Using it in practice::
+
+ $payload = new Payload();
+ $payload->product_name = 'Super Widget';
+ $payload->price_amount = 123.45;
+
+ $mapper = new ObjectMapper();
+ // map from the payload to the Product class
+ $product = $mapper->map($payload, Product::class);
+
+ // $product->name = 'Super Widget'
+ // $product->price = 123.45
+
+When using source-based mapping, the ``ObjectMapper`` will automatically use the
+target's ``#[Map(source: ...)]`` attributes if no mapping is defined on the
+source class.
+
+Handling Recursion
+------------------
+
+The ObjectMapper automatically detects and handles recursive relationships between
+objects (e.g., a ``User`` has a ``manager`` which is another ``User``, who might
+manage the first user). When it encounters previously mapped objects in the graph,
+it reuses the corresponding target instances to prevent infinite loops::
+
+ // src/Entity/User.php
+ namespace App\Entity;
+
+ use App\Dto\UserDto;
+ use Symfony\Component\ObjectMapper\Attribute\Map;
+
+ #[Map(target: UserDto::class)]
+ class User
+ {
+ public string $name = '';
+ public ?User $manager = null;
+ }
+
+The target DTO object defines the ``User`` class as its source and the
+ObjectMapper component detects the cyclic reference::
+
+ // src/Dto/UserDto.php
+ namespace App\Dto;
+
+ use Symfony\Component\ObjectMapper\Attribute\Map;
+
+ #[Map(source: \App\Entity\User::class)] // can also define mapping here
+ class UserDto
+ {
+ public string $name = '';
+ public ?UserDto $manager = null;
+ }
+
+Using it in practice::
+
+ $manager = new User();
+ $manager->name = 'Alice';
+ $employee = new User();
+ $employee->name = 'Bob';
+ $employee->manager = $manager;
+ // manager's manager is the employee:
+ $manager->manager = $employee;
+
+ $mapper = new ObjectMapper();
+ $employeeDto = $mapper->map($employee, UserDto::class);
+
+ // recursion is handled correctly:
+ // $employeeDto->name === 'Bob'
+ // $employeeDto->manager->name === 'Alice'
+ // $employeeDto->manager->manager === $employeeDto
+
+.. _objectmapper-custom-mapping-logic:
+
+Custom Mapping Logic
+--------------------
+
+For very complex mapping scenarios or if you prefer separating mapping rules from
+your DTOs/Entities, you can implement a custom mapping strategy using the
+:class:`Symfony\\Component\\ObjectMapper\\Metadata\\ObjectMapperMetadataFactoryInterface`.
+This allows defining mapping rules within dedicated mapper services, similar
+to the approach used by libraries like MapStruct in the Java ecosystem.
+
+First, create your custom metadata factory. The following example reads mapping
+rules defined via ``#[Map]`` attributes on a dedicated mapper service class,
+specifically on its ``map`` method for property mappings and on the class itself
+for the source-to-target relationship::
+
+ namespace App\ObjectMapper\Metadata;
+
+ use Symfony\Component\ObjectMapper\Attribute\Map;
+ use Symfony\Component\ObjectMapper\Metadata\Mapping;
+ use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface;
+ use Symfony\Component\ObjectMapper\ObjectMapperInterface;
+
+ /**
+ * A Metadata factory that implements basics similar to MapStruct.
+ * Reads mapping configuration from attributes on a dedicated mapper service.
+ */
+ final class MapStructMapperMetadataFactory implements ObjectMapperMetadataFactoryInterface
+ {
+ /**
+ * @param class-string $mapperClass The FQCN of the mapper service class
+ */
+ public function __construct(private readonly string $mapperClass)
+ {
+ if (!is_a($this->mapperClass, ObjectMapperInterface::class, true)) {
+ throw new \RuntimeException(sprintf('Mapper class "%s" must implement "%s".', $this->mapperClass, ObjectMapperInterface::class));
+ }
+ }
+
+ public function create(object $object, ?string $property = null, array $context = []): array
+ {
+ try {
+ $refl = new \ReflectionClass($this->mapperClass);
+ } catch (\ReflectionException $e) {
+ throw new \RuntimeException("Failed to reflect mapper class: " . $e->getMessage(), 0, $e);
+ }
+
+ $mapConfigs = [];
+ $sourceIdentifier = $property ?? $object::class;
+
+ // read attributes from the map method (for property mapping) or the class (for class mapping)
+ $attributesSource = $property ? $refl->getMethod('map') : $refl;
+ foreach ($attributesSource->getAttributes(Map::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
+ $map = $attribute->newInstance();
+
+ // check if the attribute's source matches the current property or source class
+ if ($map->source === $sourceIdentifier) {
+ $mapConfigs[] = new Mapping($map->target, $map->source, $map->if, $map->transform);
+ }
+ }
+
+ // if it's a property lookup and no specific mapping was found, map to the same property
+ if ($property && empty($mapConfigs)) {
+ $mapConfigs[] = new Mapping(target: $property, source: $property);
+ }
+
+ return $mapConfigs;
+ }
+ }
+
+Next, define your mapper service class. This class implements ``ObjectMapperInterface``
+but typically delegates the actual mapping back to a standard ``ObjectMapper``
+instance configured with the custom metadata factory. Mapping rules are defined
+using ``#[Map]`` attributes on this class and its ``map`` method::
+
+ namespace App\ObjectMapper;
+
+ use App\Dto\LegacyUser;
+ use App\Dto\UserDto;
+ use App\ObjectMapper\Metadata\MapStructMapperMetadataFactory;
+ use Symfony\Component\ObjectMapper\Attribute\Map;
+ use Symfony\Component\ObjectMapper\ObjectMapper;
+ use Symfony\Component\ObjectMapper\ObjectMapperInterface;
+
+ // define the source-to-target mapping at the class level
+ #[Map(source: LegacyUser::class, target: UserDto::class)]
+ class LegacyUserMapper implements ObjectMapperInterface
+ {
+ private readonly ObjectMapperInterface $objectMapper;
+
+ // inject the standard ObjectMapper or necessary dependencies
+ public function __construct(?ObjectMapperInterface $objectMapper = null)
+ {
+ // create an ObjectMapper instance configured with *this* mapper's rules
+ $metadataFactory = new MapStructMapperMetadataFactory(self::class);
+ $this->objectMapper = $objectMapper ?? new ObjectMapper($metadataFactory);
+ }
+
+ // define property-specific mapping rules on the map method
+ #[Map(source: 'fullName', target: 'name')] // Map LegacyUser::fullName to UserDto::name
+ #[Map(source: 'creationTimestamp', target: 'registeredAt', transform: [\DateTimeImmutable::class, 'createFromFormat'])]
+ #[Map(source: 'status', if: false)] // Ignore the 'status' property from LegacyUser
+ public function map(object $source, object|string|null $target = null): object
+ {
+ // delegate the actual mapping to the configured ObjectMapper
+ return $this->objectMapper->map($source, $target);
+ }
+ }
+
+Finally, use your custom mapper service::
+
+ use App\Dto\LegacyUser;
+ use App\ObjectMapper\LegacyUserMapper;
+
+ $legacyUser = new LegacyUser();
+ $legacyUser->fullName = 'Jane Doe';
+ $legacyUser->status = 'active'; // this will be ignored
+
+ // instantiate your custom mapper service
+ $mapperService = new LegacyUserMapper();
+
+ // use the map method of your service
+ $userDto = $mapperService->map($legacyUser); // Target (UserDto) is inferred from #[Map] on LegacyUserMapper
+
+This approach keeps mapping logic centralized within dedicated services, which can
+be beneficial for complex applications or when adhering to specific architectural patterns.
+
+Advanced Configuration
+----------------------
+
+The ``ObjectMapper`` constructor accepts optional arguments for advanced usage:
+
+* ``ObjectMapperMetadataFactoryInterface $metadataFactory``: Allows custom metadata
+ factories, such as the one shown in :ref:`the MapStruct-like example `.
+ The default is :class:`Symfony\\Component\\ObjectMapper\\Metadata\\ReflectionObjectMapperMetadataFactory`,
+ which uses ``#[Map]`` attributes from source and target classes.
+* ``?PropertyAccessorInterface $propertyAccessor``: Lets you customize how
+ properties are read and written to the target object, useful for accessing
+ private properties or using getters/setters.
+* ``?ContainerInterface $transformCallableLocator``: A PSR-11 container (service locator)
+ that resolves service IDs referenced by the ``transform`` option in ``#[Map]``.
+* ``?ContainerInterface $conditionCallableLocator``: A PSR-11 container for resolving
+ service IDs used in ``if`` conditions within ``#[Map]``.
+
+These dependencies are automatically configured when you use the
+``ObjectMapperInterface`` service provided by Symfony.
diff --git a/profiler.rst b/profiler.rst
index 57d412472ba..7fc97c8ee33 100644
--- a/profiler.rst
+++ b/profiler.rst
@@ -217,9 +217,48 @@ user by dynamically rewriting the current page rather than loading entire new
pages from a server.
By default, the debug toolbar displays the information of the initial page load
-and doesn't refresh after each AJAX request. However, you can set the
-``Symfony-Debug-Toolbar-Replace`` header to a value of ``'1'`` in the response to
-the AJAX request to force the refresh of the toolbar::
+and doesn't refresh after each AJAX request. However, you can configure the
+toolbar to be refreshed after each AJAX request by enabling ``ajax_replace`` in the
+``web_profiler`` configuration:
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/web_profiler.yaml
+ web_profiler:
+ toolbar:
+ ajax_replace: true
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/web_profiler.php
+ use Symfony\Config\WebProfilerConfig;
+
+ return static function (WebProfilerConfig $profiler): void {
+ $profiler->toolbar()
+ ->ajaxReplace(true);
+ };
+
+If you need a more sophisticated solution, you can set the
+``Symfony-Debug-Toolbar-Replace`` header to a value of ``'1'`` in the response
+yourself::
$response->headers->set('Symfony-Debug-Toolbar-Replace', '1');
@@ -228,31 +267,21 @@ production. To do that, create an :doc:`event subscriber `
and listen to the :ref:`kernel.response `
event::
+ use Symfony\Component\DependencyInjection\Attribute\When;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelInterface;
// ...
+ #[When(env: 'dev')]
class MySubscriber implements EventSubscriberInterface
{
- public function __construct(
- private KernelInterface $kernel,
- ) {
- }
-
// ...
public function onKernelResponse(ResponseEvent $event): void
{
- if (!$this->kernel->isDebug()) {
- return;
- }
-
- $request = $event->getRequest();
- if (!$request->isXmlHttpRequest()) {
- return;
- }
+ // Your custom logic here
$response = $event->getResponse();
$response->headers->set('Symfony-Debug-Toolbar-Replace', '1');
diff --git a/quick_tour/the_architecture.rst b/quick_tour/the_architecture.rst
index a323461885d..3b66570b3d3 100644
--- a/quick_tour/the_architecture.rst
+++ b/quick_tour/the_architecture.rst
@@ -159,29 +159,22 @@ Twig Extension & Autoconfiguration
Thanks to Symfony's service handling, you can *extend* Symfony in many ways, like
by creating an event subscriber or a security voter for complex authorization
rules. Let's add a new filter to Twig called ``greet``. How? Create a class
-that extends ``AbstractExtension``::
+with your logic::
// src/Twig/GreetExtension.php
namespace App\Twig;
use App\GreetingGenerator;
- use Twig\Extension\AbstractExtension;
- use Twig\TwigFilter;
+ use Twig\Attribute\AsTwigFilter;
- class GreetExtension extends AbstractExtension
+ class GreetExtension
{
public function __construct(
private GreetingGenerator $greetingGenerator,
) {
}
- public function getFilters(): array
- {
- return [
- new TwigFilter('greet', [$this, 'greetUser']),
- ];
- }
-
+ #[AsTwigFilter('greet')]
public function greetUser(string $name): string
{
$greeting = $this->greetingGenerator->getRandomGreeting();
@@ -198,7 +191,7 @@ After creating just *one* file, you can use this immediately:
{# Will print something like "Hey Symfony!" #}
{{ name|greet }}
-How does this work? Symfony notices that your class extends ``AbstractExtension``
+How does this work? Symfony notices that your class uses the ``#[AsTwigFilter]`` attribute
and so *automatically* registers it as a Twig extension. This is called autoconfiguration,
and it works for *many* many things. Create a class and then extend a base class
(or implement an interface). Symfony takes care of the rest.
diff --git a/rate_limiter.rst b/rate_limiter.rst
index 6c158ee52d0..3a517c37bd4 100644
--- a/rate_limiter.rst
+++ b/rate_limiter.rst
@@ -230,6 +230,12 @@ prevents that number from being higher than 5,000).
Rate Limiting in Action
-----------------------
+.. versionadded:: 7.3
+
+ :class:`Symfony\\Component\\RateLimiter\\RateLimiterFactoryInterface` was
+ added and should now be used for autowiring instead of
+ :class:`Symfony\\Component\\RateLimiter\\RateLimiterFactory`.
+
After having installed and configured the rate limiter, inject it in any service
or controller and call the ``consume()`` method to try to consume a given number
of tokens. For example, this controller uses the previous rate limiter to control
@@ -242,13 +248,13 @@ the number of requests to the API::
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
- use Symfony\Component\RateLimiter\RateLimiterFactory;
+ use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
class ApiController extends AbstractController
{
// if you're using service autowiring, the variable name must be:
// "rate limiter name" (in camelCase) + "Limiter" suffix
- public function index(Request $request, RateLimiterFactory $anonymousApiLimiter): Response
+ public function index(Request $request, RateLimiterFactoryInterface $anonymousApiLimiter): Response
{
// create a limiter based on a unique identifier of the client
// (e.g. the client's IP address, a username/email, an API key, etc.)
@@ -291,11 +297,11 @@ using the ``reserve()`` method::
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
- use Symfony\Component\RateLimiter\RateLimiterFactory;
+ use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
class ApiController extends AbstractController
{
- public function registerUser(Request $request, RateLimiterFactory $authenticatedApiLimiter): Response
+ public function registerUser(Request $request, RateLimiterFactoryInterface $authenticatedApiLimiter): Response
{
$apiKey = $request->headers->get('apikey');
$limiter = $authenticatedApiLimiter->create($apiKey);
@@ -350,11 +356,11 @@ the :class:`Symfony\\Component\\RateLimiter\\Reservation` object returned by the
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
- use Symfony\Component\RateLimiter\RateLimiterFactory;
+ use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
class ApiController extends AbstractController
{
- public function index(Request $request, RateLimiterFactory $anonymousApiLimiter): Response
+ public function index(Request $request, RateLimiterFactoryInterface $anonymousApiLimiter): Response
{
$limiter = $anonymousApiLimiter->create($request->getClientIp());
$limit = $limiter->consume();
@@ -461,9 +467,10 @@ simultaneous requests (e.g. three servers of a company hitting your API at the
same time). Rate limiters use :doc:`locks ` to protect their operations
against these race conditions.
-By default, Symfony uses the global lock configured by ``framework.lock``, but
-you can use a specific :ref:`named lock ` via the
-``lock_factory`` option (or none at all):
+By default, if the :doc:`lock ` component is installed, Symfony uses the
+global lock configured by ``framework.lock``, but you can use a specific
+:ref:`named lock ` via the ``lock_factory`` option (or none
+at all):
.. configuration-block::
@@ -534,6 +541,129 @@ you can use a specific :ref:`named lock ` via the
;
};
+.. versionadded:: 7.3
+
+ Before Symfony 7.3, configuring a rate limiter and using the default configured
+ lock factory (``lock.factory``) failed if the Symfony Lock component was not
+ installed in the application.
+
+Compound Rate Limiter
+---------------------
+
+.. versionadded:: 7.3
+
+ Support for configuring compound rate limiters was introduced in Symfony 7.3.
+
+You can configure multiple rate limiters to work together:
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/rate_limiter.yaml
+ framework:
+ rate_limiter:
+ two_per_minute:
+ policy: 'fixed_window'
+ limit: 2
+ interval: '1 minute'
+ five_per_hour:
+ policy: 'fixed_window'
+ limit: 5
+ interval: '1 hour'
+ contact_form:
+ policy: 'compound'
+ limiters: [two_per_minute, five_per_hour]
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+
+
+ two_per_minute
+ five_per_hour
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/rate_limiter.php
+ use Symfony\Config\FrameworkConfig;
+
+ return static function (FrameworkConfig $framework): void {
+ $framework->rateLimiter()
+ ->limiter('two_per_minute')
+ ->policy('fixed_window')
+ ->limit(2)
+ ->interval('1 minute')
+ ;
+
+ $framework->rateLimiter()
+ ->limiter('two_per_minute')
+ ->policy('fixed_window')
+ ->limit(5)
+ ->interval('1 hour')
+ ;
+
+ $framework->rateLimiter()
+ ->limiter('contact_form')
+ ->policy('compound')
+ ->limiters(['two_per_minute', 'five_per_hour'])
+ ;
+ };
+
+Then, inject and use as normal::
+
+ // src/Controller/ContactController.php
+ namespace App\Controller;
+
+ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+ use Symfony\Component\HttpFoundation\Request;
+ use Symfony\Component\HttpFoundation\Response;
+ use Symfony\Component\RateLimiter\RateLimiterFactory;
+
+ class ContactController extends AbstractController
+ {
+ public function registerUser(Request $request, RateLimiterFactoryInterface $contactFormLimiter): Response
+ {
+ $limiter = $contactFormLimiter->create($request->getClientIp());
+
+ if (false === $limiter->consume(1)->isAccepted()) {
+ // either of the two limiters has been reached
+ }
+
+ // ...
+ }
+
+ // ...
+ }
+
.. _`DoS attacks`: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html
.. _`Apache mod_ratelimit`: https://httpd.apache.org/docs/current/mod/mod_ratelimit.html
.. _`NGINX rate limiting`: https://www.nginx.com/blog/rate-limiting-nginx/
diff --git a/reference/attributes.rst b/reference/attributes.rst
index a8399dafe28..968c7df1568 100644
--- a/reference/attributes.rst
+++ b/reference/attributes.rst
@@ -14,7 +14,7 @@ Doctrine Bridge
Command
~~~~~~~
-* :ref:`AsCommand `
+* :ref:`AsCommand `
Contracts
~~~~~~~~~
@@ -123,6 +123,9 @@ Twig
~~~~
* :ref:`Template `
+* :ref:`AsTwigFilter `
+* :ref:`AsTwigFunction `
+* ``AsTwigTest``
Symfony UX
~~~~~~~~~~
diff --git a/reference/configuration/framework.rst b/reference/configuration/framework.rst
index a079ca32f01..ff900b132e1 100644
--- a/reference/configuration/framework.rst
+++ b/reference/configuration/framework.rst
@@ -1061,8 +1061,8 @@ exceptions
**type**: ``array``
-Defines the :ref:`log level ` and HTTP status code applied to the
-exceptions that match the given exception class:
+Defines the :ref:`log level `, :ref:`log channel `
+and HTTP status code applied to the exceptions that match the given exception class:
.. configuration-block::
@@ -1074,6 +1074,7 @@ exceptions that match the given exception class:
Symfony\Component\HttpKernel\Exception\BadRequestHttpException:
log_level: 'debug'
status_code: 422
+ log_channel: 'custom_channel'
.. code-block:: xml
@@ -1091,6 +1092,7 @@ exceptions that match the given exception class:
class="Symfony\Component\HttpKernel\Exception\BadRequestHttpException"
log-level="debug"
status-code="422"
+ log-channel="custom_channel"
/>
@@ -1106,9 +1108,14 @@ exceptions that match the given exception class:
$framework->exception(BadRequestHttpException::class)
->logLevel('debug')
->statusCode(422)
+ ->logChannel('custom_channel')
;
};
+.. versionadded:: 7.3
+
+ The ``log_channel`` option was introduced in Symfony 7.3.
+
The order in which you configure exceptions is important because Symfony will
use the configuration of the first exception that matches ``instanceof``:
@@ -2404,10 +2411,14 @@ collect_serializer_data
**type**: ``boolean`` **default**: ``false``
-Set this option to ``true`` to enable the serializer data collector and its
-profiler panel. When this option is ``true``, all normalizers and encoders are
+When this option is ``true``, all normalizers and encoders are
decorated by traceable implementations that collect profiling information about them.
+.. deprecated:: 7.3
+
+ Setting the ``collect_serializer_data`` option to ``false`` is deprecated
+ since Symfony 7.3.
+
.. _profiler-dsn:
dsn
@@ -2508,6 +2519,18 @@ enabled
**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation
+with_constructor_extractor
+..........................
+
+**type**: ``boolean`` **default**: ``false``
+
+Configures the ``property_info`` service to extract property information from the constructor arguments
+using the :ref:`ConstructorExtractor `.
+
+.. versionadded:: 7.3
+
+ The ``with_constructor_extractor`` option was introduced in Symfony 7.3.
+
rate_limiter
~~~~~~~~~~~~
@@ -3639,6 +3662,22 @@ Defines the Doctrine entities that will be introspected to add
]);
};
+.. _reference-validation-disable_translation:
+
+disable_translation
+...................
+
+**type**: ``boolean`` **default**: ``false``
+
+Validation error messages are automatically translated to the current application
+locale. Set this option to ``true`` to disable translation of validation messages.
+This is useful to avoid "missing translation" errors in applications that use
+only a single language.
+
+.. versionadded:: 7.3
+
+ The ``disable_translation`` option was introduced in Symfony 7.3.
+
.. _reference-validation-email_validation_mode:
email_validation_mode
diff --git a/reference/configuration/security.rst b/reference/configuration/security.rst
index 6f4fcd8db33..ef7247e330e 100644
--- a/reference/configuration/security.rst
+++ b/reference/configuration/security.rst
@@ -23,7 +23,8 @@ key in your application configuration.
* `access_denied_url`_
* `erase_credentials`_
-* `hide_user_not_found`_
+* `expose_security_errors`_
+* `hide_user_not_found`_ (deprecated)
* `session_fixation_strategy`_
**Advanced Options**:
@@ -51,13 +52,59 @@ erase_credentials
**type**: ``boolean`` **default**: ``true``
If ``true``, the ``eraseCredentials()`` method of the user object is called
-after authentication.
+after authentication::
+
+ use Symfony\Component\Security\Core\User\UserInterface;
+
+ class User implements UserInterface
+ {
+ // ...
+
+ public function eraseCredentials(): void
+ {
+ // If you store any temporary, sensitive data on the user, clear it here
+ // $this->plainPassword = null;
+ }
+ }
+
+.. deprecated:: 7.3
+
+ Since Symfony 7.3, ``eraseCredentials()`` methods are deprecated and are
+ not called if they have the ``#[\Deprecated]`` attribute.
+
+expose_security_errors
+----------------------
+
+**type**: ``string`` **default**: ``'none'``
+
+.. versionadded:: 7.3
+
+ The ``expose_security_errors`` option was introduced in Symfony 7.3
+
+User enumeration is a common security issue where attackers infer valid usernames
+based on error messages. For example, a message like "This user does not exist"
+shown by your login form reveals whether a username exists.
+
+This option lets you hide some or all errors related to user accounts
+(e.g. blocked or expired accounts) to prevent this issue. Instead, these
+errors will trigger a generic ``BadCredentialsException``. The value of this
+option can be one of the following:
+
+* ``'none'``: hides all user-related security exceptions;
+* ``'account_status'``: shows account-related exceptions (e.g. blocked or expired
+ accounts) but only for users who provided the correct password;
+* ``'all'``: shows all security-related exceptions.
hide_user_not_found
-------------------
**type**: ``boolean`` **default**: ``true``
+.. deprecated:: 7.3
+
+ The ``hide_user_not_found`` option was deprecated in favor of the
+ ``expose_security_errors`` option in Symfony 7.3.
+
If ``true``, when a user is not found a generic exception of type
:class:`Symfony\\Component\\Security\\Core\\Exception\\BadCredentialsException`
is thrown with the message "Bad credentials".
diff --git a/reference/configuration/twig.rst b/reference/configuration/twig.rst
index 3c4dc1b30ac..360309fef8f 100644
--- a/reference/configuration/twig.rst
+++ b/reference/configuration/twig.rst
@@ -71,16 +71,27 @@ application harder to maintain.
cache
~~~~~
-**type**: ``string`` | ``false`` **default**: ``%kernel.cache_dir%/twig``
+**type**: ``string`` | ``boolean`` **default**: ``true``
Before using the Twig templates to render some contents, they are compiled into
regular PHP code. Compilation is a costly process, so the result is cached in
the directory defined by this configuration option.
+You can either specify a custom path where the cache should be stored (as a
+string) or use ``true`` to let Symfony decide the default path. When set to
+``true``, the cache is stored in ``%kernel.cache_dir%/twig`` by default. However,
+if ``auto_reload`` is disabled and ``%kernel.build_dir%`` differs from
+``%kernel.cache_dir%``, the cache will be stored in ``%kernel.build_dir%/twig`` instead.
+
Set this option to ``false`` to disable Twig template compilation. However, this
-is not recommended; not even in the ``dev`` environment, because the
-``auto_reload`` option ensures that cached templates which have changed get
-compiled again.
+is not recommended, not even in the ``dev`` environment, because the ``auto_reload``
+option ensures that cached templates which have changed get compiled again.
+
+.. versionadded:: 7.3
+
+ Support for using ``true`` as a value was introduced in Symfony 7.3. It also
+ became the default value for this option, replacing the explicit path
+ ``%kernel.cache_dir%/twig``.
charset
~~~~~~~
diff --git a/reference/configuration/web_profiler.rst b/reference/configuration/web_profiler.rst
index de706c73fef..c3b57d37c55 100644
--- a/reference/configuration/web_profiler.rst
+++ b/reference/configuration/web_profiler.rst
@@ -53,8 +53,21 @@ on the given link to perform the redirect.
toolbar
~~~~~~~
+enabled
+.......
**type**: ``boolean`` **default**: ``false``
It enables and disables the toolbar entirely. Usually you set this to ``true``
in the ``dev`` and ``test`` environments and to ``false`` in the ``prod``
environment.
+
+ajax_replace
+............
+**type**: ``boolean`` **default**: ``false``
+
+If you set this option to ``true``, the toolbar is replaced on AJAX requests.
+This only works in combination with an enabled toolbar.
+
+.. versionadded:: 7.3
+
+ The ``ajax_replace`` configuration option was introduced in Symfony 7.3.
diff --git a/reference/constraints/All.rst b/reference/constraints/All.rst
index 3aa05b1d2d0..43ff4d6ac9d 100644
--- a/reference/constraints/All.rst
+++ b/reference/constraints/All.rst
@@ -79,12 +79,12 @@ entry in that array:
{
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('favoriteColors', new Assert\All([
- 'constraints' => [
+ $metadata->addPropertyConstraint('favoriteColors', new Assert\All(
+ constraints: [
new Assert\NotBlank(),
- new Assert\Length(['min' => 5]),
+ new Assert\Length(min: 5),
],
- ]));
+ ));
}
}
@@ -97,7 +97,7 @@ Options
``constraints``
~~~~~~~~~~~~~~~
-**type**: ``array`` [:ref:`default option `]
+**type**: ``array``
This required option is the array of validation constraints that you want
to apply to each element of the underlying array.
diff --git a/reference/constraints/AtLeastOneOf.rst b/reference/constraints/AtLeastOneOf.rst
index 0a6ab618aa5..fecbe617f5a 100644
--- a/reference/constraints/AtLeastOneOf.rst
+++ b/reference/constraints/AtLeastOneOf.rst
@@ -115,23 +115,23 @@ The following constraints ensure that:
{
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('password', new Assert\AtLeastOneOf([
- 'constraints' => [
- new Assert\Regex(['pattern' => '/#/']),
- new Assert\Length(['min' => 10]),
+ $metadata->addPropertyConstraint('password', new Assert\AtLeastOneOf(
+ constraints: [
+ new Assert\Regex(pattern: '/#/'),
+ new Assert\Length(min: 10),
],
- ]));
+ ));
- $metadata->addPropertyConstraint('grades', new Assert\AtLeastOneOf([
- 'constraints' => [
- new Assert\Count(['min' => 3]),
- new Assert\All([
- 'constraints' => [
+ $metadata->addPropertyConstraint('grades', new Assert\AtLeastOneOf(
+ constraints: [
+ new Assert\Count(min: 3),
+ new Assert\All(
+ constraints: [
new Assert\GreaterThanOrEqual(5),
],
- ]),
+ ),
],
- ]));
+ ));
}
}
@@ -141,7 +141,7 @@ Options
constraints
~~~~~~~~~~~
-**type**: ``array`` [:ref:`default option `]
+**type**: ``array``
This required option is the array of validation constraints from which at least one of
has to be satisfied in order for the validation to succeed.
diff --git a/reference/constraints/Callback.rst b/reference/constraints/Callback.rst
index f4c78a9642a..017b9435cff 100644
--- a/reference/constraints/Callback.rst
+++ b/reference/constraints/Callback.rst
@@ -259,7 +259,7 @@ Options
``callback``
~~~~~~~~~~~~
-**type**: ``string``, ``array`` or ``Closure`` [:ref:`default option `]
+**type**: ``string``, ``array`` or ``Closure``
The callback option accepts three different formats for specifying the
callback method:
diff --git a/reference/constraints/CardScheme.rst b/reference/constraints/CardScheme.rst
index 6e98e6fab98..a2ed9c568c3 100644
--- a/reference/constraints/CardScheme.rst
+++ b/reference/constraints/CardScheme.rst
@@ -77,12 +77,12 @@ on an object that will contain a credit card number.
{
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('cardNumber', new Assert\CardScheme([
- 'schemes' => [
+ $metadata->addPropertyConstraint('cardNumber', new Assert\CardScheme(
+ schemes: [
Assert\CardScheme::VISA,
],
- 'message' => 'Your credit card number is invalid.',
- ]));
+ message: 'Your credit card number is invalid.',
+ ));
}
}
@@ -114,7 +114,7 @@ Parameter Description
``schemes``
~~~~~~~~~~~
-**type**: ``mixed`` [:ref:`default option `]
+**type**: ``mixed``
This option is required and represents the name of the number scheme used
to validate the credit card number, it can either be a string or an array.
diff --git a/reference/constraints/Choice.rst b/reference/constraints/Choice.rst
index cd7f5c7837d..72e1ae6ecf7 100644
--- a/reference/constraints/Choice.rst
+++ b/reference/constraints/Choice.rst
@@ -100,10 +100,10 @@ If your valid choice list is simple, you can pass them in directly via the
new Assert\Choice(['New York', 'Berlin', 'Tokyo'])
);
- $metadata->addPropertyConstraint('genre', new Assert\Choice([
- 'choices' => ['fiction', 'non-fiction'],
- 'message' => 'Choose a valid genre.',
- ]));
+ $metadata->addPropertyConstraint('genre', new Assert\Choice(
+ choices: ['fiction', 'non-fiction'],
+ message: 'Choose a valid genre.',
+ ));
}
}
@@ -182,9 +182,9 @@ constraint.
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('genre', new Assert\Choice([
- 'callback' => 'getGenres',
- ]));
+ $metadata->addPropertyConstraint('genre', new Assert\Choice(
+ callback: 'getGenres',
+ ));
}
}
@@ -250,9 +250,9 @@ you can pass the class name and the method as an array.
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('genre', new Assert\Choice([
- 'callback' => [Genre::class, 'getGenres'],
- ]));
+ $metadata->addPropertyConstraint('genre', new Assert\Choice(
+ callback: [Genre::class, 'getGenres'],
+ ));
}
}
@@ -271,7 +271,7 @@ to return the choices array. See
``choices``
~~~~~~~~~~~
-**type**: ``array`` [:ref:`default option `]
+**type**: ``array``
A required option (unless `callback`_ is specified) - this is the array
of options that should be considered in the valid set. The input value
diff --git a/reference/constraints/Collection.rst b/reference/constraints/Collection.rst
index 2d16d201b17..c35a0103581 100644
--- a/reference/constraints/Collection.rst
+++ b/reference/constraints/Collection.rst
@@ -139,8 +139,8 @@ following:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('profileData', new Assert\Collection([
- 'fields' => [
+ $metadata->addPropertyConstraint('profileData', new Assert\Collection(
+ fields: [
'personal_email' => new Assert\Email(),
'short_bio' => [
new Assert\NotBlank(),
@@ -150,8 +150,8 @@ following:
]),
],
],
- 'allowMissingFields' => true,
- ]));
+ allowMissingFields: true,
+ ));
}
}
@@ -267,15 +267,15 @@ you can do the following:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('profileData', new Assert\Collection([
- 'fields' => [
+ $metadata->addPropertyConstraint('profileData', new Assert\Collection(
+ fields: [
'personal_email' => new Assert\Required([
new Assert\NotBlank(),
new Assert\Email(),
]),
'alternate_email' => new Assert\Optional(new Assert\Email()),
],
- ]));
+ ));
}
}
@@ -291,28 +291,28 @@ groups. Take the following example::
use Symfony\Component\Validator\Constraints as Assert;
- $constraint = new Assert\Collection([
- 'fields' => [
+ $constraint = new Assert\Collection(
+ fields: [
'name' => new Assert\NotBlank(['groups' => 'basic']),
'email' => new Assert\NotBlank(['groups' => 'contact']),
],
- ]);
+ );
This will result in the following configuration::
- $constraint = new Assert\Collection([
- 'fields' => [
- 'name' => new Assert\Required([
- 'constraints' => new Assert\NotBlank(['groups' => 'basic']),
- 'groups' => ['basic', 'strict'],
- ]),
- 'email' => new Assert\Required([
- "constraints" => new Assert\NotBlank(['groups' => 'contact']),
- 'groups' => ['basic', 'strict'],
- ]),
+ $constraint = new Assert\Collection(
+ fields: [
+ 'name' => new Assert\Required(
+ constraints: new Assert\NotBlank(groups: ['basic']),
+ groups: ['basic', 'strict'],
+ ),
+ 'email' => new Assert\Required(
+ constraints: new Assert\NotBlank(groups: ['contact']),
+ groups: ['basic', 'strict'],
+ ),
],
- 'groups' => ['basic', 'strict'],
- ]);
+ groups: ['basic', 'strict'],
+ );
The default ``allowMissingFields`` option requires the fields in all groups.
So when validating in ``contact`` group, ``$name`` can be empty but the key is
@@ -360,7 +360,7 @@ Parameter Description
``fields``
~~~~~~~~~~
-**type**: ``array`` [:ref:`default option `]
+**type**: ``array``
This option is required and is an associative array defining all of the
keys in the collection and, for each key, exactly which validator(s) should
diff --git a/reference/constraints/Compound.rst b/reference/constraints/Compound.rst
index 0d0dc933ae0..4d2c7743176 100644
--- a/reference/constraints/Compound.rst
+++ b/reference/constraints/Compound.rst
@@ -35,9 +35,9 @@ you can create your own named set or requirements to be reused consistently ever
return [
new Assert\NotBlank(),
new Assert\Type('string'),
- new Assert\Length(['min' => 12]),
+ new Assert\Length(min: 12),
new Assert\NotCompromisedPassword(),
- new Assert\PasswordStrength(['minScore' => 4]),
+ new Assert\PasswordStrength(minScore: 4),
];
}
}
diff --git a/reference/constraints/Count.rst b/reference/constraints/Count.rst
index 0bf40aca8e9..d33c54c0812 100644
--- a/reference/constraints/Count.rst
+++ b/reference/constraints/Count.rst
@@ -82,12 +82,12 @@ you might add the following:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('emails', new Assert\Count([
- 'min' => 1,
- 'max' => 5,
- 'minMessage' => 'You must specify at least one email',
- 'maxMessage' => 'You cannot specify more than {{ limit }} emails',
- ]));
+ $metadata->addPropertyConstraint('emails', new Assert\Count(
+ min: 1,
+ max: 5,
+ minMessage: 'You must specify at least one email',
+ maxMessage: 'You cannot specify more than {{ limit }} emails',
+ ));
}
}
diff --git a/reference/constraints/CssColor.rst b/reference/constraints/CssColor.rst
index fbbc982087d..b9c78ec25ac 100644
--- a/reference/constraints/CssColor.rst
+++ b/reference/constraints/CssColor.rst
@@ -110,15 +110,15 @@ the named CSS colors:
{
$metadata->addPropertyConstraint('defaultColor', new Assert\CssColor());
- $metadata->addPropertyConstraint('accentColor', new Assert\CssColor([
- 'formats' => Assert\CssColor::HEX_LONG,
- 'message' => 'The accent color must be a 6-character hexadecimal color.',
- ]));
-
- $metadata->addPropertyConstraint('currentColor', new Assert\CssColor([
- 'formats' => [Assert\CssColor::BASIC_NAMED_COLORS, Assert\CssColor::EXTENDED_NAMED_COLORS],
- 'message' => 'The color "{{ value }}" is not a valid CSS color name.',
- ]));
+ $metadata->addPropertyConstraint('accentColor', new Assert\CssColor(
+ formats: Assert\CssColor::HEX_LONG,
+ message: 'The accent color must be a 6-character hexadecimal color.',
+ ));
+
+ $metadata->addPropertyConstraint('currentColor', new Assert\CssColor(
+ formats: [Assert\CssColor::BASIC_NAMED_COLORS, Assert\CssColor::EXTENDED_NAMED_COLORS],
+ message: 'The color "{{ value }}" is not a valid CSS color name.',
+ ));
}
}
diff --git a/reference/constraints/DateTime.rst b/reference/constraints/DateTime.rst
index f6bcce7e5f5..ffcfbf55dda 100644
--- a/reference/constraints/DateTime.rst
+++ b/reference/constraints/DateTime.rst
@@ -99,11 +99,16 @@ This message is shown if the underlying data is not a valid datetime.
You can use the following parameters in this message:
-=============== ==============================================================
-Parameter Description
-=============== ==============================================================
-``{{ value }}`` The current (invalid) value
-``{{ label }}`` Corresponding form field label
-=============== ==============================================================
+================ ==============================================================
+Parameter Description
+================ ==============================================================
+``{{ value }}`` The current (invalid) value
+``{{ label }}`` Corresponding form field label
+``{{ format }}`` The date format defined in ``format``
+================ ==============================================================
+
+.. versionadded:: 7.3
+
+ The ``{{ format }}`` parameter was introduced in Symfony 7.3.
.. include:: /reference/constraints/_payload-option.rst.inc
diff --git a/reference/constraints/DivisibleBy.rst b/reference/constraints/DivisibleBy.rst
index dd90ad9a0fd..23b36023cff 100644
--- a/reference/constraints/DivisibleBy.rst
+++ b/reference/constraints/DivisibleBy.rst
@@ -92,9 +92,9 @@ The following constraints ensure that:
{
$metadata->addPropertyConstraint('weight', new Assert\DivisibleBy(0.25));
- $metadata->addPropertyConstraint('quantity', new Assert\DivisibleBy([
- 'value' => 5,
- ]));
+ $metadata->addPropertyConstraint('quantity', new Assert\DivisibleBy(
+ value: 5,
+ ));
}
}
diff --git a/reference/constraints/Email.rst b/reference/constraints/Email.rst
index 516d6d07dca..41012e5e935 100644
--- a/reference/constraints/Email.rst
+++ b/reference/constraints/Email.rst
@@ -70,9 +70,9 @@ Basic Usage
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('email', new Assert\Email([
- 'message' => 'The email "{{ value }}" is not a valid email.',
- ]));
+ $metadata->addPropertyConstraint('email', new Assert\Email(
+ message: 'The email "{{ value }}" is not a valid email.',
+ ));
}
}
diff --git a/reference/constraints/EqualTo.rst b/reference/constraints/EqualTo.rst
index d5d78f60a0f..fdc402b1a97 100644
--- a/reference/constraints/EqualTo.rst
+++ b/reference/constraints/EqualTo.rst
@@ -91,9 +91,9 @@ and that the ``age`` is ``20``, you could do the following:
{
$metadata->addPropertyConstraint('firstName', new Assert\EqualTo('Mary'));
- $metadata->addPropertyConstraint('age', new Assert\EqualTo([
- 'value' => 20,
- ]));
+ $metadata->addPropertyConstraint('age', new Assert\EqualTo(
+ value: 20,
+ ));
}
}
diff --git a/reference/constraints/Expression.rst b/reference/constraints/Expression.rst
index bf015d17573..518c5c1f160 100644
--- a/reference/constraints/Expression.rst
+++ b/reference/constraints/Expression.rst
@@ -111,10 +111,10 @@ One way to accomplish this is with the Expression constraint:
{
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addConstraint(new Assert\Expression([
- 'expression' => 'this.getCategory() in ["php", "symfony"] or !this.isTechnicalPost()',
- 'message' => 'If this is a tech post, the category should be either php or symfony!',
- ]));
+ $metadata->addConstraint(new Assert\Expression(
+ expression: 'this.getCategory() in ["php", "symfony"] or !this.isTechnicalPost()',
+ message: 'If this is a tech post, the category should be either php or symfony!',
+ ));
}
// ...
@@ -200,10 +200,10 @@ assert that the expression must return ``true`` for validation to fail.
{
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('isTechnicalPost', new Assert\Expression([
- 'expression' => 'this.getCategory() in ["php", "symfony"] or value == false',
- 'message' => 'If this is a tech post, the category should be either php or symfony!',
- ]));
+ $metadata->addPropertyConstraint('isTechnicalPost', new Assert\Expression(
+ expression: 'this.getCategory() in ["php", "symfony"] or value == false',
+ message: 'If this is a tech post, the category should be either php or symfony!',
+ ));
}
// ...
@@ -227,7 +227,7 @@ Options
``expression``
~~~~~~~~~~~~~~
-**type**: ``string`` [:ref:`default option `]
+**type**: ``string``
The expression that will be evaluated. If the expression evaluates to a false
value (using ``==``, not ``===``), validation will fail. Learn more about the
@@ -343,10 +343,10 @@ type (numeric, boolean, strings, null, etc.)
{
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('metric', new Assert\Expression([
- 'expression' => 'value + error_margin < threshold',
- 'values' => ['error_margin' => 0.25, 'threshold' => 1.5],
- ]));
+ $metadata->addPropertyConstraint('metric', new Assert\Expression(
+ expression: 'value + error_margin < threshold',
+ values: ['error_margin' => 0.25, 'threshold' => 1.5],
+ ));
}
// ...
diff --git a/reference/constraints/ExpressionSyntax.rst b/reference/constraints/ExpressionSyntax.rst
index c1d086790c1..37e0ad7de4a 100644
--- a/reference/constraints/ExpressionSyntax.rst
+++ b/reference/constraints/ExpressionSyntax.rst
@@ -90,9 +90,9 @@ The following constraints ensure that:
{
$metadata->addPropertyConstraint('promotion', new Assert\ExpressionSyntax());
- $metadata->addPropertyConstraint('shippingOptions', new Assert\ExpressionSyntax([
- 'allowedVariables' => ['user', 'shipping_centers'],
- ]));
+ $metadata->addPropertyConstraint('shippingOptions', new Assert\ExpressionSyntax(
+ allowedVariables: ['user', 'shipping_centers'],
+ ));
}
}
diff --git a/reference/constraints/File.rst b/reference/constraints/File.rst
index 6d9b72d17b8..62efa6cc08e 100644
--- a/reference/constraints/File.rst
+++ b/reference/constraints/File.rst
@@ -119,13 +119,13 @@ below a certain file size and a valid PDF, add the following:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('bioFile', new Assert\File([
- 'maxSize' => '1024k',
- 'extensions' => [
+ $metadata->addPropertyConstraint('bioFile', new Assert\File(
+ maxSize: '1024k',
+ extensions: [
'pdf',
],
- 'extensionsMessage' => 'Please upload a valid PDF',
- ]));
+ extensionsMessage: 'Please upload a valid PDF',
+ ));
}
}
@@ -274,6 +274,31 @@ You can find a list of existing mime types on the `IANA website`_.
If set, the validator will check that the filename of the underlying file
doesn't exceed a certain length.
+``filenameCountUnit``
+~~~~~~~~~~~~~~~~~~~~~
+
+**type**: ``string`` **default**: ``File::FILENAME_COUNT_BYTES``
+
+The character count unit to use for the filename max length check.
+By default :phpfunction:`strlen` is used, which counts the length of the string in bytes.
+
+Can be one of the following constants of the
+:class:`Symfony\\Component\\Validator\\Constraints\\File` class:
+
+* ``FILENAME_COUNT_BYTES``: Uses :phpfunction:`strlen` counting the length of the
+ string in bytes.
+* ``FILENAME_COUNT_CODEPOINTS``: Uses :phpfunction:`mb_strlen` counting the length
+ of the string in Unicode code points. Simple (multibyte) Unicode characters count
+ as 1 character, while for example ZWJ sequences of composed emojis count as
+ multiple characters.
+* ``FILENAME_COUNT_GRAPHEMES``: Uses :phpfunction:`grapheme_strlen` counting the
+ length of the string in graphemes, i.e. even emojis and ZWJ sequences of composed
+ emojis count as 1 character.
+
+.. versionadded:: 7.3
+
+ The ``filenameCountUnit`` option was introduced in Symfony 7.3.
+
``filenameTooLongMessage``
~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -290,6 +315,35 @@ Parameter Description
``{{ filename_max_length }}`` Maximum number of characters allowed
============================== ==============================================================
+``filenameCharset``
+~~~~~~~~~~~~~~~~~~~
+
+**type**: ``string`` **default**: ``null``
+
+The charset to be used when computing value's filename max length with the
+:phpfunction:`mb_check_encoding` and :phpfunction:`mb_strlen`
+PHP functions.
+
+``filenameCharsetMessage``
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+**type**: ``string`` **default**: ``This filename does not match the expected charset.``
+
+The message that will be shown if the value is not using the given `filenameCharsetMessage`_.
+
+You can use the following parameters in this message:
+
+================= ============================================================
+Parameter Description
+================= ============================================================
+``{{ charset }}`` The expected charset
+``{{ name }}`` The current (invalid) value
+================= ============================================================
+
+.. versionadded:: 7.3
+
+ The ``filenameCharset`` and ``filenameCharsetMessage`` options were introduced in Symfony 7.3.
+
``extensionsMessage``
~~~~~~~~~~~~~~~~~~~~~
diff --git a/reference/constraints/GreaterThan.rst b/reference/constraints/GreaterThan.rst
index 4f2e34bcbfa..d1b79028acd 100644
--- a/reference/constraints/GreaterThan.rst
+++ b/reference/constraints/GreaterThan.rst
@@ -89,9 +89,9 @@ The following constraints ensure that:
{
$metadata->addPropertyConstraint('siblings', new Assert\GreaterThan(5));
- $metadata->addPropertyConstraint('age', new Assert\GreaterThan([
- 'value' => 18,
- ]));
+ $metadata->addPropertyConstraint('age', new Assert\GreaterThan(
+ value: 18,
+ ));
}
}
diff --git a/reference/constraints/GreaterThanOrEqual.rst b/reference/constraints/GreaterThanOrEqual.rst
index e5a71e5f788..63c2ade6197 100644
--- a/reference/constraints/GreaterThanOrEqual.rst
+++ b/reference/constraints/GreaterThanOrEqual.rst
@@ -88,9 +88,9 @@ The following constraints ensure that:
{
$metadata->addPropertyConstraint('siblings', new Assert\GreaterThanOrEqual(5));
- $metadata->addPropertyConstraint('age', new Assert\GreaterThanOrEqual([
- 'value' => 18,
- ]));
+ $metadata->addPropertyConstraint('age', new Assert\GreaterThanOrEqual(
+ value: 18,
+ ));
}
}
diff --git a/reference/constraints/Hostname.rst b/reference/constraints/Hostname.rst
index 95b10d1736e..58ac0364669 100644
--- a/reference/constraints/Hostname.rst
+++ b/reference/constraints/Hostname.rst
@@ -72,9 +72,9 @@ will contain a host name.
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('name', new Assert\Hostname([
- 'message' => 'The server name must be a valid hostname.',
- ]));
+ $metadata->addPropertyConstraint('name', new Assert\Hostname(
+ message: 'The server name must be a valid hostname.',
+ ));
}
}
diff --git a/reference/constraints/Iban.rst b/reference/constraints/Iban.rst
index 849d93cb565..fdc955c81b0 100644
--- a/reference/constraints/Iban.rst
+++ b/reference/constraints/Iban.rst
@@ -77,9 +77,9 @@ will contain an International Bank Account Number.
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('bankAccountNumber', new Assert\Iban([
- 'message' => 'This is not a valid International Bank Account Number (IBAN).',
- ]));
+ $metadata->addPropertyConstraint('bankAccountNumber', new Assert\Iban(
+ message: 'This is not a valid International Bank Account Number (IBAN).',
+ ));
}
}
diff --git a/reference/constraints/IdenticalTo.rst b/reference/constraints/IdenticalTo.rst
index 5b6d853dc0b..f8844f90a72 100644
--- a/reference/constraints/IdenticalTo.rst
+++ b/reference/constraints/IdenticalTo.rst
@@ -94,9 +94,9 @@ The following constraints ensure that:
{
$metadata->addPropertyConstraint('firstName', new Assert\IdenticalTo('Mary'));
- $metadata->addPropertyConstraint('age', new Assert\IdenticalTo([
- 'value' => 20,
- ]));
+ $metadata->addPropertyConstraint('age', new Assert\IdenticalTo(
+ value: 20,
+ ));
}
}
diff --git a/reference/constraints/Image.rst b/reference/constraints/Image.rst
index 042c6041423..5dd270c44f8 100644
--- a/reference/constraints/Image.rst
+++ b/reference/constraints/Image.rst
@@ -116,12 +116,12 @@ that it is between a certain size, add the following:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('headshot', new Assert\Image([
- 'minWidth' => 200,
- 'maxWidth' => 400,
- 'minHeight' => 200,
- 'maxHeight' => 400,
- ]));
+ $metadata->addPropertyConstraint('headshot', new Assert\Image(
+ minWidth: 200,
+ maxWidth: 400,
+ minHeight: 200,
+ maxHeight: 400,
+ ));
}
}
@@ -187,10 +187,10 @@ following code:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('headshot', new Assert\Image([
- 'allowLandscape' => false,
- 'allowPortrait' => false,
- ]));
+ $metadata->addPropertyConstraint('headshot', new Assert\Image(
+ allowLandscape: false,
+ allowPortrait: false,
+ ));
}
}
@@ -210,10 +210,9 @@ add several other options.
If this option is false, the image cannot be landscape oriented.
-.. note::
+.. versionadded:: 7.3
- This option does not apply to SVG files. If you use it with SVG files,
- you'll see the error message defined in the ``sizeNotDetectedMessage`` option.
+ The ``allowLandscape`` option support for SVG files was introduced in Symfony 7.3.
``allowLandscapeMessage``
~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -239,10 +238,9 @@ Parameter Description
If this option is false, the image cannot be portrait oriented.
-.. note::
+.. versionadded:: 7.3
- This option does not apply to SVG files. If you use it with SVG files,
- you'll see the error message defined in the ``sizeNotDetectedMessage`` option.
+ The ``allowPortrait`` option support for SVG files was introduced in Symfony 7.3.
``allowPortraitMessage``
~~~~~~~~~~~~~~~~~~~~~~~~
@@ -270,10 +268,9 @@ If this option is false, the image cannot be a square. If you want to force
a square image, then leave this option as its default ``true`` value
and set `allowLandscape`_ and `allowPortrait`_ both to ``false``.
-.. note::
+.. versionadded:: 7.3
- This option does not apply to SVG files. If you use it with SVG files,
- you'll see the error message defined in the ``sizeNotDetectedMessage`` option.
+ The ``allowSquare`` option support for SVG files was introduced in Symfony 7.3.
``allowSquareMessage``
~~~~~~~~~~~~~~~~~~~~~~
@@ -373,10 +370,9 @@ Parameter Description
If set, the aspect ratio (``width / height``) of the image file must be less
than or equal to this value.
-.. note::
+.. versionadded:: 7.3
- This option does not apply to SVG files. If you use it with SVG files,
- you'll see the error message defined in the ``sizeNotDetectedMessage`` option.
+ The ``maxRatio`` option support for SVG files was introduced in Symfony 7.3.
``maxRatioMessage``
~~~~~~~~~~~~~~~~~~~
@@ -497,10 +493,9 @@ Parameter Description
If set, the aspect ratio (``width / height``) of the image file must be greater
than or equal to this value.
-.. note::
+.. versionadded:: 7.3
- This option does not apply to SVG files. If you use it with SVG files,
- you'll see the error message defined in the ``sizeNotDetectedMessage`` option.
+ The ``minRatio`` option support for SVG files was introduced in Symfony 7.3.
``minRatioMessage``
~~~~~~~~~~~~~~~~~~~
@@ -555,11 +550,5 @@ options has been set.
This message has no parameters.
-.. note::
-
- Detecting the size of SVG images is not supported. This error message will
- be displayed if you use any of the following options: ``allowLandscape``,
- ``allowPortrait``, ``allowSquare``, ``maxRatio``, and ``minRatio``.
-
.. _`IANA website`: https://www.iana.org/assignments/media-types/media-types.xhtml
.. _`PHP GD extension`: https://www.php.net/manual/en/book.image.php
diff --git a/reference/constraints/IsFalse.rst b/reference/constraints/IsFalse.rst
index 0b9ebe77491..3d0a1665944 100644
--- a/reference/constraints/IsFalse.rst
+++ b/reference/constraints/IsFalse.rst
@@ -93,9 +93,9 @@ method returns **false**:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addGetterConstraint('stateInvalid', new Assert\IsFalse([
- 'message' => "You've entered an invalid state.",
- ]));
+ $metadata->addGetterConstraint('stateInvalid', new Assert\IsFalse(
+ message: "You've entered an invalid state.",
+ ));
}
public function isStateInvalid(): bool
diff --git a/reference/constraints/IsTrue.rst b/reference/constraints/IsTrue.rst
index 678371f6e69..b50ba4f3e8b 100644
--- a/reference/constraints/IsTrue.rst
+++ b/reference/constraints/IsTrue.rst
@@ -97,9 +97,9 @@ Then you can validate this method with ``IsTrue`` as follows:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addGetterConstraint('tokenValid', new IsTrue([
- 'message' => 'The token is invalid.',
- ]));
+ $metadata->addGetterConstraint('tokenValid', new IsTrue(
+ message: 'The token is invalid.',
+ ));
}
public function isTokenValid(): bool
diff --git a/reference/constraints/Isbn.rst b/reference/constraints/Isbn.rst
index 954bff233d5..52d10565fe5 100644
--- a/reference/constraints/Isbn.rst
+++ b/reference/constraints/Isbn.rst
@@ -76,10 +76,10 @@ on an object that will contain an ISBN.
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('isbn', new Assert\Isbn([
- 'type' => Assert\Isbn::ISBN_10,
- 'message' => 'This value is not valid.',
- ]));
+ $metadata->addPropertyConstraint('isbn', new Assert\Isbn(
+ type: Assert\Isbn::ISBN_10,
+ message: 'This value is not valid.',
+ ));
}
}
diff --git a/reference/constraints/Json.rst b/reference/constraints/Json.rst
index 28e15976f3c..337b2dc6a1e 100644
--- a/reference/constraints/Json.rst
+++ b/reference/constraints/Json.rst
@@ -69,9 +69,9 @@ The ``Json`` constraint can be applied to a property or a "getter" method:
{
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('chapters', new Assert\Json([
- 'message' => 'You\'ve entered an invalid Json.',
- ]));
+ $metadata->addPropertyConstraint('chapters', new Assert\Json(
+ message: 'You\'ve entered an invalid Json.',
+ ));
}
}
diff --git a/reference/constraints/Length.rst b/reference/constraints/Length.rst
index 9a4478f509b..c1a8575070b 100644
--- a/reference/constraints/Length.rst
+++ b/reference/constraints/Length.rst
@@ -85,12 +85,12 @@ and ``50``, you might add the following:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('firstName', new Assert\Length([
- 'min' => 2,
- 'max' => 50,
- 'minMessage' => 'Your first name must be at least {{ limit }} characters long',
- 'maxMessage' => 'Your first name cannot be longer than {{ limit }} characters',
- ]));
+ $metadata->addPropertyConstraint('firstName', new Assert\Length(
+ min: 2,
+ max: 50,
+ minMessage: 'Your first name must be at least {{ limit }} characters long',
+ maxMessage: 'Your first name cannot be longer than {{ limit }} characters',
+ ));
}
}
diff --git a/reference/constraints/LessThan.rst b/reference/constraints/LessThan.rst
index 964bfbb3527..3d23bcda445 100644
--- a/reference/constraints/LessThan.rst
+++ b/reference/constraints/LessThan.rst
@@ -89,9 +89,9 @@ The following constraints ensure that:
{
$metadata->addPropertyConstraint('siblings', new Assert\LessThan(5));
- $metadata->addPropertyConstraint('age', new Assert\LessThan([
- 'value' => 80,
- ]));
+ $metadata->addPropertyConstraint('age', new Assert\LessThan(
+ value: 80,
+ ));
}
}
diff --git a/reference/constraints/LessThanOrEqual.rst b/reference/constraints/LessThanOrEqual.rst
index 9420c3e4376..ac66c62d7d0 100644
--- a/reference/constraints/LessThanOrEqual.rst
+++ b/reference/constraints/LessThanOrEqual.rst
@@ -88,9 +88,9 @@ The following constraints ensure that:
{
$metadata->addPropertyConstraint('siblings', new Assert\LessThanOrEqual(5));
- $metadata->addPropertyConstraint('age', new Assert\LessThanOrEqual([
- 'value' => 80,
- ]));
+ $metadata->addPropertyConstraint('age', new Assert\LessThanOrEqual(
+ value: 80,
+ ));
}
}
diff --git a/reference/constraints/Locale.rst b/reference/constraints/Locale.rst
index 49edd473d05..4bba45ae12b 100644
--- a/reference/constraints/Locale.rst
+++ b/reference/constraints/Locale.rst
@@ -78,9 +78,9 @@ Basic Usage
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('locale', new Assert\Locale([
- 'canonicalize' => true,
- ]));
+ $metadata->addPropertyConstraint('locale', new Assert\Locale(
+ canonicalize: true,
+ ));
}
}
diff --git a/reference/constraints/Luhn.rst b/reference/constraints/Luhn.rst
index 8f5ef34c4ba..0c835204091 100644
--- a/reference/constraints/Luhn.rst
+++ b/reference/constraints/Luhn.rst
@@ -72,9 +72,9 @@ will contain a credit card number.
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('cardNumber', new Assert\Luhn([
- 'message' => 'Please check your credit card number',
- ]));
+ $metadata->addPropertyConstraint('cardNumber', new Assert\Luhn(
+ message: 'Please check your credit card number',
+ ));
}
}
diff --git a/reference/constraints/NotEqualTo.rst b/reference/constraints/NotEqualTo.rst
index b8ee4cac32f..dd3f633b4a1 100644
--- a/reference/constraints/NotEqualTo.rst
+++ b/reference/constraints/NotEqualTo.rst
@@ -93,9 +93,9 @@ the following:
{
$metadata->addPropertyConstraint('firstName', new Assert\NotEqualTo('Mary'));
- $metadata->addPropertyConstraint('age', new Assert\NotEqualTo([
- 'value' => 15,
- ]));
+ $metadata->addPropertyConstraint('age', new Assert\NotEqualTo(
+ value: 15,
+ ));
}
}
diff --git a/reference/constraints/NotIdenticalTo.rst b/reference/constraints/NotIdenticalTo.rst
index 9ea93dc4b86..b2c20027292 100644
--- a/reference/constraints/NotIdenticalTo.rst
+++ b/reference/constraints/NotIdenticalTo.rst
@@ -94,9 +94,9 @@ The following constraints ensure that:
{
$metadata->addPropertyConstraint('age', new Assert\NotIdenticalTo('Mary'));
- $metadata->addPropertyConstraint('age', new Assert\NotIdenticalTo([
- 'value' => 15,
- ]));
+ $metadata->addPropertyConstraint('age', new Assert\NotIdenticalTo(
+ value: 15,
+ ));
}
}
diff --git a/reference/constraints/PasswordStrength.rst b/reference/constraints/PasswordStrength.rst
index 60125a763a1..0b242cacf08 100644
--- a/reference/constraints/PasswordStrength.rst
+++ b/reference/constraints/PasswordStrength.rst
@@ -101,9 +101,9 @@ or by a custom password strength estimator.
class User
{
- #[Assert\PasswordStrength([
- 'minScore' => PasswordStrength::STRENGTH_VERY_STRONG, // Very strong password required
- ])]
+ #[Assert\PasswordStrength(
+ minScore: PasswordStrength::STRENGTH_VERY_STRONG, // Very strong password required
+ )]
protected $rawPassword;
}
@@ -123,9 +123,9 @@ The default message supplied when the password does not reach the minimum requir
class User
{
- #[Assert\PasswordStrength([
- 'message' => 'Your password is too easy to guess. Company\'s security policy requires to use a stronger password.'
- ])]
+ #[Assert\PasswordStrength(
+ message: 'Your password is too easy to guess. Company\'s security policy requires to use a stronger password.'
+ )]
protected $rawPassword;
}
diff --git a/reference/constraints/Range.rst b/reference/constraints/Range.rst
index edd199c48b9..46a9e3799b3 100644
--- a/reference/constraints/Range.rst
+++ b/reference/constraints/Range.rst
@@ -78,11 +78,11 @@ you might add the following:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('height', new Assert\Range([
- 'min' => 120,
- 'max' => 180,
- 'notInRangeMessage' => 'You must be between {{ min }}cm and {{ max }}cm tall to enter',
- ]));
+ $metadata->addPropertyConstraint('height', new Assert\Range(
+ min: 120,
+ max: 180,
+ notInRangeMessage: 'You must be between {{ min }}cm and {{ max }}cm tall to enter',
+ ));
}
}
@@ -154,10 +154,10 @@ date must lie within the current year like this:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('startDate', new Assert\Range([
- 'min' => 'first day of January',
- 'max' => 'first day of January next year',
- ]));
+ $metadata->addPropertyConstraint('startDate', new Assert\Range(
+ min: 'first day of January',
+ max: 'first day of January next year',
+ ));
}
}
@@ -224,10 +224,10 @@ dates. If you want to fix the timezone, append it to the date string:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('startDate', new Assert\Range([
- 'min' => 'first day of January UTC',
- 'max' => 'first day of January next year UTC',
- ]));
+ $metadata->addPropertyConstraint('startDate', new Assert\Range(
+ min: 'first day of January UTC',
+ max: 'first day of January next year UTC',
+ ));
}
}
@@ -294,10 +294,10 @@ can check that a delivery date starts within the next five hours like this:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('deliveryDate', new Assert\Range([
- 'min' => 'now',
- 'max' => '+5 hours',
- ]));
+ $metadata->addPropertyConstraint('deliveryDate', new Assert\Range(
+ min: 'now',
+ max: '+5 hours',
+ ));
}
}
diff --git a/reference/constraints/Regex.rst b/reference/constraints/Regex.rst
index 2e11a8d04fc..e3b4d4711b2 100644
--- a/reference/constraints/Regex.rst
+++ b/reference/constraints/Regex.rst
@@ -71,9 +71,9 @@ more word characters at the beginning of your string:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('description', new Assert\Regex([
- 'pattern' => '/^\w+/',
- ]));
+ $metadata->addPropertyConstraint('description', new Assert\Regex(
+ pattern: '/^\w+/',
+ ));
}
}
@@ -145,11 +145,11 @@ it a custom message:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('firstName', new Assert\Regex([
- 'pattern' => '/\d/',
- 'match' => false,
- 'message' => 'Your name cannot contain a number',
- ]));
+ $metadata->addPropertyConstraint('firstName', new Assert\Regex(
+ pattern: '/\d/',
+ match: false,
+ message: 'Your name cannot contain a number',
+ ));
}
}
@@ -236,10 +236,10 @@ need to specify the HTML5 compatible pattern in the ``htmlPattern`` option:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('name', new Assert\Regex([
- 'pattern' => '/^[a-z]+$/i',
- 'htmlPattern' => '[a-zA-Z]+',
- ]));
+ $metadata->addPropertyConstraint('name', new Assert\Regex(
+ pattern: '/^[a-z]+$/i',
+ htmlPattern: '[a-zA-Z]+',
+ ));
}
}
@@ -275,7 +275,7 @@ Parameter Description
``pattern``
~~~~~~~~~~~
-**type**: ``string`` [:ref:`default option `]
+**type**: ``string``
This required option is the regular expression pattern that the input will
be matched against. By default, this validator will fail if the input string
diff --git a/reference/constraints/Sequentially.rst b/reference/constraints/Sequentially.rst
index 7620997f0a3..078be338cdf 100644
--- a/reference/constraints/Sequentially.rst
+++ b/reference/constraints/Sequentially.rst
@@ -110,7 +110,7 @@ You can validate each of these constraints sequentially to solve these issues:
$metadata->addPropertyConstraint('address', new Assert\Sequentially([
new Assert\NotNull(),
new Assert\Type('string'),
- new Assert\Length(['min' => 10]),
+ new Assert\Length(min: 10),
new Assert\Regex(self::ADDRESS_REGEX),
new AcmeAssert\Geolocalizable(),
]));
@@ -123,7 +123,7 @@ Options
``constraints``
~~~~~~~~~~~~~~~
-**type**: ``array`` [:ref:`default option `]
+**type**: ``array``
This required option is the array of validation constraints that you want
to apply sequentially.
diff --git a/reference/constraints/Twig.rst b/reference/constraints/Twig.rst
new file mode 100644
index 00000000000..e38b4507d7a
--- /dev/null
+++ b/reference/constraints/Twig.rst
@@ -0,0 +1,130 @@
+Twig Constraint
+===============
+
+.. versionadded:: 7.3
+
+ The ``Twig`` constraint was introduced in Symfony 7.3.
+
+Validates that a given string contains valid :ref:`Twig syntax `.
+This is particularly useful when template content is user-generated or
+configurable, and you want to ensure it can be rendered by the Twig engine.
+
+.. note::
+
+ Using this constraint requires having the ``symfony/twig-bridge`` package
+ installed in your application (e.g. by running ``composer require symfony/twig-bridge``).
+
+========== ===================================================================
+Applies to :ref:`property or method `
+Class :class:`Symfony\\Bridge\\Twig\\Validator\\Constraints\\Twig`
+Validator :class:`Symfony\\Bridge\\Twig\\Validator\\Constraints\\TwigValidator`
+========== ===================================================================
+
+Basic Usage
+-----------
+
+Apply the ``Twig`` constraint to validate the contents of any property or the
+returned value of any method::
+
+ use Symfony\Bridge\Twig\Validator\Constraints\Twig;
+
+ class Template
+ {
+ #[Twig]
+ private string $templateCode;
+ }
+
+.. configuration-block::
+
+ .. code-block:: php-attributes
+
+ // src/Entity/Page.php
+ namespace App\Entity;
+
+ use Symfony\Bridge\Twig\Validator\Constraints\Twig;
+
+ class Page
+ {
+ #[Twig]
+ private string $templateCode;
+ }
+
+ .. code-block:: yaml
+
+ # config/validator/validation.yaml
+ App\Entity\Page:
+ properties:
+ templateCode:
+ - Symfony\Bridge\Twig\Validator\Constraints\Twig: ~
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // src/Entity/Page.php
+ namespace App\Entity;
+
+ use Symfony\Bridge\Twig\Validator\Constraints\Twig;
+ use Symfony\Component\Validator\Mapping\ClassMetadata;
+
+ class Page
+ {
+ // ...
+
+ public static function loadValidatorMetadata(ClassMetadata $metadata): void
+ {
+ $metadata->addPropertyConstraint('templateCode', new Twig());
+ }
+ }
+
+Constraint Options
+------------------
+
+``message``
+~~~~~~~~~~~
+
+**type**: ``message`` **default**: ``This value is not a valid Twig template.``
+
+This is the message displayed when the given string does *not* contain valid Twig syntax::
+
+ // ...
+
+ class Page
+ {
+ #[Twig(message: 'Check this Twig code; it contains errors.')]
+ private string $templateCode;
+ }
+
+This message has no parameters.
+
+``skipDeprecations``
+~~~~~~~~~~~~~~~~~~~~
+
+**type**: ``boolean`` **default**: ``true``
+
+If ``true``, Twig deprecation warnings are ignored during validation. Set it to
+``false`` to trigger validation errors when the given Twig code contains any deprecations::
+
+ // ...
+
+ class Page
+ {
+ #[Twig(skipDeprecations: false)]
+ private string $templateCode;
+ }
+
+This can be helpful when enforcing stricter template rules or preparing for major
+Twig version upgrades.
diff --git a/reference/constraints/Type.rst b/reference/constraints/Type.rst
index b99e8ce1c54..b49536dff8b 100644
--- a/reference/constraints/Type.rst
+++ b/reference/constraints/Type.rst
@@ -127,14 +127,14 @@ The following example checks if ``emailAddress`` is an instance of ``Symfony\Com
$metadata->addPropertyConstraint('firstName', new Assert\Type('string'));
- $metadata->addPropertyConstraint('age', new Assert\Type([
- 'type' => 'integer',
- 'message' => 'The value {{ value }} is not a valid {{ type }}.',
- ]));
-
- $metadata->addPropertyConstraint('accessCode', new Assert\Type([
- 'type' => ['alpha', 'digit'],
- ]));
+ $metadata->addPropertyConstraint('age', new Assert\Type(
+ type: 'integer',
+ message: 'The value {{ value }} is not a valid {{ type }}.',
+ ));
+
+ $metadata->addPropertyConstraint('accessCode', new Assert\Type(
+ type: ['alpha', 'digit'],
+ ));
}
}
@@ -169,7 +169,7 @@ Parameter Description
``type``
~~~~~~~~
-**type**: ``string`` or ``array`` [:ref:`default option `]
+**type**: ``string`` or ``array``
This required option defines the type or collection of types allowed for the
given value. Each type is either the FQCN (fully qualified class name) of some
diff --git a/reference/constraints/Unique.rst b/reference/constraints/Unique.rst
index 68754738271..9ce84139cd5 100644
--- a/reference/constraints/Unique.rst
+++ b/reference/constraints/Unique.rst
@@ -162,9 +162,9 @@ collection::
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('coordinates', new Assert\Unique([
- 'fields' => ['latitude', 'longitude'],
- ]));
+ $metadata->addPropertyConstraint('coordinates', new Assert\Unique(
+ fields: ['latitude', 'longitude'],
+ ));
}
}
@@ -216,4 +216,17 @@ trailing whitespace during validation.
.. include:: /reference/constraints/_payload-option.rst.inc
+``stopOnFirstError``
+~~~~~~~~~~~~~~~~~~~~
+
+**type**: ``boolean`` **default**: ``true``
+
+By default, this constraint stops at the first violation. If this option is set
+to ``false``, validation continues on all elements and returns all detected
+:class:`Symfony\\Component\\Validator\\ConstraintViolation` objects.
+
+.. versionadded:: 7.3
+
+ The ``stopOnFirstError`` option was introduced in Symfony 7.3.
+
.. _`PHP callable`: https://www.php.net/callable
diff --git a/reference/constraints/UniqueEntity.rst b/reference/constraints/UniqueEntity.rst
index a25d2d398b0..0ab2c0a8cbd 100644
--- a/reference/constraints/UniqueEntity.rst
+++ b/reference/constraints/UniqueEntity.rst
@@ -95,9 +95,9 @@ between all of the rows in your user table:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addConstraint(new UniqueEntity([
- 'fields' => 'email',
- ]));
+ $metadata->addConstraint(new UniqueEntity(
+ fields: 'email',
+ ));
$metadata->addPropertyConstraint('email', new Assert\Email());
}
@@ -260,7 +260,7 @@ Now, the message would be bound to the ``port`` field with this configuration.
``fields``
~~~~~~~~~~
-**type**: ``array`` | ``string`` [:ref:`default option `]
+**type**: ``array`` | ``string``
This required option is the field (or list of fields) on which this entity
should be unique. For example, if you specified both the ``email`` and ``name``
@@ -346,10 +346,10 @@ this option to specify one or more fields to only ignore ``null`` values on them
{
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
- $metadata->addConstraint(new UniqueEntity([
- 'fields' => ['email', 'phoneNumber'],
- 'ignoreNull' => 'phoneNumber',
- ]));
+ $metadata->addConstraint(new UniqueEntity(
+ fields: ['email', 'phoneNumber'],
+ ignoreNull: 'phoneNumber',
+ ));
// ...
}
diff --git a/reference/constraints/Url.rst b/reference/constraints/Url.rst
index 1e9bb8ac62e..c3fac520f96 100644
--- a/reference/constraints/Url.rst
+++ b/reference/constraints/Url.rst
@@ -152,9 +152,9 @@ Parameter Description
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('bioUrl', new Assert\Url([
- 'message' => 'The url "{{ value }}" is not a valid url.',
- ]));
+ $metadata->addPropertyConstraint('bioUrl', new Assert\Url(
+ message: 'The url "{{ value }}" is not a valid url.',
+ ));
}
}
@@ -231,9 +231,9 @@ the ``ftp://`` type URLs to be valid, redefine the ``protocols`` array, listing
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('bioUrl', new Assert\Url([
- 'protocols' => ['http', 'https', 'ftp'],
- ]));
+ $metadata->addPropertyConstraint('bioUrl', new Assert\Url(
+ protocols: ['http', 'https', 'ftp'],
+ ));
}
}
@@ -302,9 +302,9 @@ also relative URLs that contain no protocol (e.g. ``//example.com``).
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('bioUrl', new Assert\Url([
- 'relativeProtocol' => true,
- ]));
+ $metadata->addPropertyConstraint('bioUrl', new Assert\Url(
+ relativeProtocol: true,
+ ));
}
}
@@ -414,10 +414,10 @@ Parameter Description
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('homepageUrl', new Assert\Url([
- 'requireTld' => true,
- 'tldMessage' => 'Add at least one TLD to the {{ value }} URL.',
- ]));
+ $metadata->addPropertyConstraint('homepageUrl', new Assert\Url(
+ requireTld: true,
+ tldMessage: 'Add at least one TLD to the {{ value }} URL.',
+ ));
}
}
diff --git a/reference/constraints/Valid.rst b/reference/constraints/Valid.rst
index 1f99b666419..61a2c1d992c 100644
--- a/reference/constraints/Valid.rst
+++ b/reference/constraints/Valid.rst
@@ -149,7 +149,7 @@ stores an ``Address`` instance in the ``$address`` property::
{
$metadata->addPropertyConstraint('street', new Assert\NotBlank());
$metadata->addPropertyConstraint('zipCode', new Assert\NotBlank());
- $metadata->addPropertyConstraint('zipCode', new Assert\Length(['max' => 5]));
+ $metadata->addPropertyConstraint('zipCode', new Assert\Length(max: 5));
}
}
@@ -166,7 +166,7 @@ stores an ``Address`` instance in the ``$address`` property::
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$metadata->addPropertyConstraint('firstName', new Assert\NotBlank());
- $metadata->addPropertyConstraint('firstName', new Assert\Length(['min' => 4]));
+ $metadata->addPropertyConstraint('firstName', new Assert\Length(min: 4));
$metadata->addPropertyConstraint('lastName', new Assert\NotBlank());
}
}
diff --git a/reference/constraints/Week.rst b/reference/constraints/Week.rst
index f107d8b4192..b3c1b0ca122 100644
--- a/reference/constraints/Week.rst
+++ b/reference/constraints/Week.rst
@@ -79,10 +79,10 @@ the following:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('startWeek', new Assert\Week([
- 'min' => '2022-W01',
- 'max' => '2022-W20',
- ]));
+ $metadata->addPropertyConstraint('startWeek', new Assert\Week(
+ min: '2022-W01',
+ max: '2022-W20',
+ ));
}
}
diff --git a/reference/constraints/When.rst b/reference/constraints/When.rst
index 2a05e58ee9c..6eca8b4895f 100644
--- a/reference/constraints/When.rst
+++ b/reference/constraints/When.rst
@@ -9,6 +9,7 @@ Applies to :ref:`class `
or :ref:`property/method `
Options - `expression`_
- `constraints`_
+ _ `otherwise`_
- `groups`_
- `payload`_
- `values`_
@@ -47,7 +48,7 @@ properties::
To validate the object, you have some requirements:
A) If ``type`` is ``percent``, then ``value`` must be less than or equal 100;
-B) If ``type`` is ``absolute``, then ``value`` can be anything;
+B) If ``type`` is not ``percent``, then ``value`` must be less than 9999;
C) No matter the value of ``type``, the ``value`` must be greater than 0.
One way to accomplish this is with the When constraint:
@@ -69,6 +70,9 @@ One way to accomplish this is with the When constraint:
constraints: [
new Assert\LessThanOrEqual(100, message: 'The value should be between 1 and 100!')
],
+ otherwise: [
+ new Assert\LessThan(9999, message: 'The value should be less than 9999!')
+ ],
)]
private ?int $value;
@@ -88,6 +92,10 @@ One way to accomplish this is with the When constraint:
- LessThanOrEqual:
value: 100
message: "The value should be between 1 and 100!"
+ otherwise:
+ - LessThan:
+ value: 9999
+ message: "The value should be less than 9999!"
.. code-block:: xml
@@ -109,6 +117,12 @@ One way to accomplish this is with the When constraint:
+
@@ -127,15 +141,21 @@ One way to accomplish this is with the When constraint:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$metadata->addPropertyConstraint('value', new Assert\GreaterThan(0));
- $metadata->addPropertyConstraint('value', new Assert\When([
- 'expression' => 'this.getType() == "percent"',
- 'constraints' => [
- new Assert\LessThanOrEqual([
- 'value' => 100,
- 'message' => 'The value should be between 1 and 100!',
- ]),
+ $metadata->addPropertyConstraint('value', new Assert\When(
+ expression: 'this.getType() == "percent"',
+ constraints: [
+ new Assert\LessThanOrEqual(
+ value: 100,
+ message: 'The value should be between 1 and 100!',
+ ),
],
- ]));
+ otherwise: [
+ new Assert\LessThan(
+ value: 9999,
+ message: 'The value should be less than 9999!',
+ ),
+ ],
+ ));
}
// ...
@@ -154,17 +174,15 @@ Options
``expression``
~~~~~~~~~~~~~~
-**type**: ``string``
-
-The condition written with the expression language syntax that will be evaluated.
-If the expression evaluates to a falsey value (i.e. using ``==``, not ``===``),
-validation of constraints won't be triggered.
+**type**: ``string|Closure``
-To learn more about the expression language syntax, see
-:doc:`/reference/formats/expression_language`.
+The condition evaluated to decide if the constraint is applied or not. It can be
+defined as a closure or a string using the :doc:`expression language syntax `.
+If the result is a falsey value (``false``, ``null``, ``0``, an empty string or
+an empty array) the constraints defined in the ``constraints`` option won't be
+applied but the constraints defined in ``otherwise`` option (if provided) will be applied.
-Depending on how you use the constraint, you have access to different variables
-in your expression:
+**When using an expression**, you access to the following variables:
``this``
The object being validated (e.g. an instance of Discount).
@@ -180,8 +198,12 @@ in your expression:
The ``context`` variable in expressions was introduced in Symfony 7.2.
-The ``value`` variable can be used when you want to execute more complex
-validation based on its value:
+**When using a closure**, the first argument is the object being validated.
+
+.. versionadded:: 7.3
+
+ The support for closures in the ``expression`` option was introduced in Symfony 7.3
+ and requires PHP 8.5.
.. configuration-block::
@@ -195,11 +217,21 @@ validation based on its value:
class Discount
{
+ // either using an expression...
#[Assert\When(
expression: 'value == "percent"',
constraints: [new Assert\Callback('doComplexValidation')],
)]
+
+ // ... or using a closure
+ #[Assert\When(
+ expression: static function (Discount $discount) {
+ return $discount->getType() === 'percent';
+ },
+ constraints: [new Assert\Callback('doComplexValidation')],
+ )]
private ?string $type;
+
// ...
public function doComplexValidation(ExecutionContextInterface $context, $payload): void
@@ -256,12 +288,12 @@ validation based on its value:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('type', new Assert\When([
- 'expression' => 'value == "percent"',
- 'constraints' => [
+ $metadata->addPropertyConstraint('type', new Assert\When(
+ expression: 'value == "percent"',
+ constraints: [
new Assert\Callback('doComplexValidation'),
],
- ]));
+ ));
}
public function doComplexValidation(ExecutionContextInterface $context, $payload): void
@@ -279,6 +311,17 @@ You can also pass custom variables using the `values`_ option.
One or multiple constraints that are applied if the expression returns true.
+``otherwise``
+~~~~~~~~~~~~~
+
+**type**: ``array|Constraint``
+
+One or multiple constraints that are applied if the expression returns false.
+
+.. versionadded:: 7.3
+
+ The ``otherwise`` option was introduced in Symfony 7.3.
+
.. include:: /reference/constraints/_groups-option.rst.inc
.. include:: /reference/constraints/_payload-option.rst.inc
diff --git a/reference/constraints/WordCount.rst b/reference/constraints/WordCount.rst
index 74c79216898..392f8a5bcb7 100644
--- a/reference/constraints/WordCount.rst
+++ b/reference/constraints/WordCount.rst
@@ -78,10 +78,10 @@ class contains between 100 and 200 words, you could do the following:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('content', new Assert\WordCount([
- 'min' => 100,
- 'max' => 200,
- ]));
+ $metadata->addPropertyConstraint('content', new Assert\WordCount(
+ min: 100,
+ max: 200,
+ ));
}
}
diff --git a/reference/constraints/Yaml.rst b/reference/constraints/Yaml.rst
index 49b65f251e8..0d1564f4f8a 100644
--- a/reference/constraints/Yaml.rst
+++ b/reference/constraints/Yaml.rst
@@ -73,9 +73,9 @@ The ``Yaml`` constraint can be applied to a property or a "getter" method:
{
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('customConfiguration', new Assert\Yaml([
- 'message' => 'Your configuration doesn\'t have valid YAML syntax.',
- ]));
+ $metadata->addPropertyConstraint('customConfiguration', new Assert\Yaml(
+ message: 'Your configuration doesn\'t have valid YAML syntax.',
+ ));
}
}
@@ -122,10 +122,10 @@ Its value is a combination of one or more of the :ref:`flags defined by the Yaml
{
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('customConfiguration', new Assert\Yaml([
- 'message' => 'Your configuration doesn\'t have valid YAML syntax.',
- 'flags' => Yaml::PARSE_CONSTANT | Yaml::PARSE_CUSTOM_TAGS | Yaml::PARSE_DATETIME,
- ]));
+ $metadata->addPropertyConstraint('customConfiguration', new Assert\Yaml(
+ message: 'Your configuration doesn\'t have valid YAML syntax.',
+ flags: Yaml::PARSE_CONSTANT | Yaml::PARSE_CUSTOM_TAGS | Yaml::PARSE_DATETIME,
+ ));
}
}
diff --git a/reference/constraints/_comparison-value-option.rst.inc b/reference/constraints/_comparison-value-option.rst.inc
index c8abdfb5af0..91ab28a2e94 100644
--- a/reference/constraints/_comparison-value-option.rst.inc
+++ b/reference/constraints/_comparison-value-option.rst.inc
@@ -1,7 +1,7 @@
``value``
~~~~~~~~~
-**type**: ``mixed`` [:ref:`default option `]
+**type**: ``mixed``
This option is required. It defines the comparison value. It can be a
string, number or object.
diff --git a/reference/constraints/map.rst.inc b/reference/constraints/map.rst.inc
index f23f5b71aa3..06680e42207 100644
--- a/reference/constraints/map.rst.inc
+++ b/reference/constraints/map.rst.inc
@@ -33,6 +33,7 @@ String Constraints
* :doc:`NotCompromisedPassword `
* :doc:`PasswordStrength `
* :doc:`Regex `
+* :doc:`Twig `
* :doc:`Ulid `
* :doc:`Url `
* :doc:`UserPassword `
diff --git a/reference/forms/types/money.rst b/reference/forms/types/money.rst
index a02b695abd4..967fe9e4ce4 100644
--- a/reference/forms/types/money.rst
+++ b/reference/forms/types/money.rst
@@ -83,6 +83,9 @@ input
By default, the money value is converted to a ``float`` PHP type. If you need the
value to be converted into an integer (e.g. because some library needs money
values stored in cents as integers) set this option to ``integer``.
+You can also set this option to ``string``, it can be useful if the underlying
+data is a string for precision reasons (for example, Doctrine uses strings for
+the decimal type).
.. versionadded:: 7.1
diff --git a/reference/twig_reference.rst b/reference/twig_reference.rst
index f2735ec758a..633d4c7f0c6 100644
--- a/reference/twig_reference.rst
+++ b/reference/twig_reference.rst
@@ -187,20 +187,41 @@ is_granted
.. code-block:: twig
- {{ is_granted(role, object = null, field = null) }}
+ {{ is_granted(role, object = null) }}
``role``
**type**: ``string``
``object`` *(optional)*
**type**: ``object``
-``field`` *(optional)*
- **type**: ``string``
Returns ``true`` if the current user has the given role.
Optionally, an object can be passed to be used by the voter. More information
can be found in :ref:`security-template`.
+is_granted_for_user
+~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 7.3
+
+ The ``is_granted_for_user()`` function was introduced in Symfony 7.3.
+
+.. code-block:: twig
+
+ {{ is_granted_for_user(user, attribute, subject = null) }}
+
+``user``
+ **type**: ``object``
+``attribute``
+ **type**: ``string``
+``subject`` *(optional)*
+ **type**: ``object``
+
+Returns ``true`` if the user is authorized for the specified attribute.
+
+Optionally, an object can be passed to be used by the voter. More information
+can be found in :ref:`security-template`.
+
logout_path
~~~~~~~~~~~
@@ -523,6 +544,7 @@ explained in the article about :doc:`customizing form rendering `
* :ref:`form_rest() `
* :ref:`field_name() `
+* :ref:`field_id() `
* :ref:`field_value() `
* :ref:`field_label() `
* :ref:`field_help() `
diff --git a/routing.rst b/routing.rst
index 772f0bb2bce..4f31e70da64 100644
--- a/routing.rst
+++ b/routing.rst
@@ -18,6 +18,8 @@ your favorite.
:ref:`Symfony recommends attributes `
because it's convenient to put the route and controller in the same place.
+.. _routing-route-attributes:
+
Creating Routes as Attributes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -495,6 +497,18 @@ evaluates them:
blog_show ANY ANY ANY /blog/{slug}
---------------- ------- ------- ----- --------------------------------------------
+ # pass this option to also display all the defined route aliases
+ $ php bin/console debug:router --show-aliases
+
+ # pass this option to only display routes that match the given HTTP method
+ # (you can use the special value ANY to see routes that match any method)
+ $ php bin/console debug:router --method=GET
+ $ php bin/console debug:router --method=ANY
+
+.. versionadded:: 7.3
+
+ The ``--method`` option was introduced in Symfony 7.3.
+
Pass the name (or part of the name) of some route to this argument to print the
route details:
@@ -512,11 +526,6 @@ route details:
| | utf8: true |
+-------------+---------------------------------------------------------+
-.. tip::
-
- Use the ``--show-aliases`` option to show all available aliases for a given
- route.
-
The other command is called ``router:match`` and it shows which route will match
the given URL. It's useful to find out why some URL is not executing the
controller action that you expect:
@@ -1075,6 +1084,25 @@ corresponding ``BlogPost`` object from the database using the slug.
Route parameter mapping was introduced in Symfony 7.1.
+When mapping multiple entities from route parameters, name collisions can occur.
+In this example, the route tries to define two mappings: one for an author and one
+for a category; both using the same ``name`` parameter. This isn't allowed because
+the route ends up declaring ``name`` twice::
+
+ #[Route('/search-book/{name:author}/{name:category}')]
+
+Such routes should instead be defined using the following syntax::
+
+ #[Route('/search-book/{authorName:author.name}/{categoryName:category.name}')]
+
+This way, the route parameter names are unique (``authorName`` and ``categoryName``),
+and the "param converter" can correctly map them to controller arguments (``$author``
+and ``$category``), loading them both by their name.
+
+.. versionadded:: 7.3
+
+ This more advanced style of route parameter mapping was introduced in Symfony 7.3.
+
More advanced mappings can be achieved using the ``#[MapEntity]`` attribute.
Check out the :ref:`Doctrine param conversion documentation `
to learn how to customize the database queries used to fetch the object from the route
@@ -1376,6 +1404,23 @@ have been renamed. Let's say you have a route called ``product_show``:
.. configuration-block::
+ .. code-block:: php-attributes
+
+ // src/Controller/ProductController.php
+ namespace App\Controller;
+
+ use Symfony\Component\HttpFoundation\Response;
+ use Symfony\Component\Routing\Attribute\Route;
+
+ class ProductController
+ {
+ #[Route('/product/{id}', name: 'product_show')]
+ public function show(): Response
+ {
+ // ...
+ }
+ }
+
.. code-block:: yaml
# config/routes.yaml
@@ -1412,6 +1457,25 @@ Instead of duplicating the original route, you can create an alias for it.
.. configuration-block::
+ .. code-block:: php-attributes
+
+ // src/Controller/ProductController.php
+ namespace App\Controller;
+
+ use Symfony\Component\HttpFoundation\Response;
+ use Symfony\Component\Routing\Attribute\Route;
+
+ class ProductController
+ {
+ // the "alias" argument assigns an alternate name to this route;
+ // the alias will point to the actual route "product_show"
+ #[Route('/product/{id}', name: 'product_show', alias: ['product_details'])]
+ public function show(): Response
+ {
+ // ...
+ }
+ }
+
.. code-block:: yaml
# config/routes.yaml
@@ -1449,9 +1513,22 @@ Instead of duplicating the original route, you can create an alias for it.
$routes->alias('product_details', 'product_show');
};
+.. versionadded:: 7.3
+
+ Support for route aliases in PHP attributes was introduced in Symfony 7.3.
+
In this example, both ``product_show`` and ``product_details`` routes can
be used in the application and will produce the same result.
+.. note::
+
+ YAML, XML, and PHP configuration formats are the only ways to define an alias
+ for a route that you do not own. You can't do this when using PHP attributes.
+
+ This allows you for example to use your own route name for URL generation,
+ while still targeting a route defined by a third-party bundle. The alias and
+ the original route do not need to be declared in the same file or format.
+
.. _routing-alias-deprecation:
Deprecating Route Aliases
@@ -1472,6 +1549,42 @@ This way, the ``product_show`` alias could be deprecated.
.. configuration-block::
+ .. code-block:: php-attributes
+
+ // src/Controller/ProductController.php
+ namespace App\Controller;
+
+ use Symfony\Component\HttpFoundation\Response;
+ use Symfony\Component\Routing\Attribute\Route;
+
+ class ProductController
+ {
+ // this outputs the following generic deprecation message:
+ // Since acme/package 1.2: The "product_show" route alias is deprecated. You should stop using it, as it will be removed in the future.
+ #[Route('/product/{id}',
+ name: 'product_details',
+ alias: new DeprecatedAlias(
+ aliasName: 'product_show',
+ package: 'acme/package',
+ version: '1.2',
+ ),
+ )]
+ // Or, you can also define a custom deprecation message (%alias_id% placeholder is available)
+ #[Route('/product/{id}',
+ name: 'product_details',
+ alias: new DeprecatedAlias(
+ aliasName: 'product_show',
+ package: 'acme/package',
+ version: '1.2',
+ message: 'The "%alias_id%" route alias is deprecated. Please use "product_details" instead.',
+ ),
+ )]
+ public function show(): Response
+ {
+ // ...
+ }
+ }
+
.. code-block:: yaml
# Move the concrete route definition under ``product_details``
@@ -2618,23 +2731,23 @@ The solution is to configure the ``default_uri`` option to define the
Now you'll get the expected results when generating URLs in your commands::
- // src/Command/SomeCommand.php
+ // src/Command/MyCommand.php
namespace App\Command;
- use Symfony\Component\Console\Command\Command;
- use Symfony\Component\Console\Input\InputInterface;
- use Symfony\Component\Console\Output\OutputInterface;
+ use Symfony\Component\Console\Attribute\AsCommand;
+ use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
// ...
- class SomeCommand extends Command
+ #[AsCommand(name: 'app:my-command')]
+ class MyCommand
{
- public function __construct(private UrlGeneratorInterface $urlGenerator)
- {
- parent::__construct();
+ public function __construct(
+ private UrlGeneratorInterface $urlGenerator,
+ ) {
}
- protected function execute(InputInterface $input, OutputInterface $output): int
+ public function __invoke(SymfonyStyle $io): int
{
// generate a URL with no route arguments
$signUpPage = $this->urlGenerator->generate('sign_up');
@@ -2944,11 +3057,41 @@ argument of :method:`Symfony\\Component\\HttpFoundation\\UriSigner::sign`::
The feature to add an expiration date for a signed URI was introduced in Symfony 7.1.
-.. note::
+If you need to know the reason why a signed URI is invalid, you can use the
+``verify()`` method which throws exceptions on failure::
+
+ use Symfony\Component\HttpFoundation\Exception\ExpiredSignedUriException;
+ use Symfony\Component\HttpFoundation\Exception\UnsignedUriException;
+ use Symfony\Component\HttpFoundation\Exception\UnverifiedSignedUriException;
+
+ // ...
+
+ try {
+ $uriSigner->verify($uri); // $uri can be a string or Request object
+
+ // the URI is valid
+ } catch (UnsignedUriException) {
+ // the URI isn't signed
+ } catch (UnverifiedSignedUriException) {
+ // the URI is signed but the signature is invalid
+ } catch (ExpiredSignedUriException) {
+ // the URI is signed but expired
+ }
+
+.. versionadded:: 7.3
+
+ The ``verify()`` method was introduced in Symfony 7.3.
+
+.. tip::
+
+ If ``symfony/clock`` is installed, it will be used to create and verify
+ expirations. This allows you to :ref:`mock the current time in your tests
+ `.
+
+.. versionadded:: 7.3
- The generated URI hashes may include the ``/`` and ``+`` characters, which
- can cause issues with certain clients. If you encounter this problem, replace
- them using the following: ``strtr($hash, ['/' => '_', '+' => '-'])``.
+ Support for :doc:`Symfony Clock ` in ``UriSigner`` was
+ introduced in Symfony 7.3.
Troubleshooting
---------------
diff --git a/scheduler.rst b/scheduler.rst
index 8458aed6b20..ed6ada8b5ed 100644
--- a/scheduler.rst
+++ b/scheduler.rst
@@ -438,10 +438,10 @@ by adding one of these attributes to a service or a command:
:class:`Symfony\\Component\\Scheduler\\Attribute\\AsCronTask`.
For both of these attributes, you have the ability to define the schedule to
-use via the ``schedule``option. By default, the ``default`` named schedule will
+use via the ``schedule`` option. By default, the ``default`` named schedule will
be used. Also, by default, the ``__invoke`` method of your service will be called
-but, it's also possible to specify the method to call via the ``method``option
-and you can define arguments via ``arguments``option if necessary.
+but, it's also possible to specify the method to call via the ``method`` option
+and you can define arguments via ``arguments`` option if necessary.
.. _scheduler-attributes-cron-task:
@@ -478,7 +478,8 @@ The attribute takes more parameters to customize the trigger::
// when applying this attribute to a Symfony console command, you can pass
// arguments and options to the command using the 'arguments' option:
#[AsCronTask('0 0 * * *', arguments: 'some_argument --some-option --another-option=some_value')]
- class MyCommand extends Command
+ #[AsCommand(name: 'app:my-command')]
+ class MyCommand
.. _scheduler-attributes-periodic-task:
@@ -527,7 +528,8 @@ The ``#[AsPeriodicTask]`` attribute takes many parameters to customize the trigg
// when applying this attribute to a Symfony console command, you can pass
// arguments and options to the command using the 'arguments' option:
#[AsPeriodicTask(frequency: '1 day', arguments: 'some_argument --some-option --another-option=some_value')]
- class MyCommand extends Command
+ #[AsCommand(name: 'app:my-command')]
+ class MyCommand
Managing Scheduled Messages
---------------------------
@@ -743,10 +745,15 @@ after a message is consumed::
$schedule = $event->getSchedule();
$context = $event->getMessageContext();
$message = $event->getMessage();
+ $result = $event->getResult();
- // do something with the schedule, context or message
+ // do something with the schedule, context, message or result
}
+.. versionadded:: 7.3
+
+ The ``getResult()`` method was introduced in Symfony 7.3.
+
Execute this command to find out which listeners are registered for this event
and their priorities:
diff --git a/security.rst b/security.rst
index a05bce6b72d..9d2df6165d0 100644
--- a/security.rst
+++ b/security.rst
@@ -193,14 +193,7 @@ from the `MakerBundle`_:
return $this;
}
- /**
- * @see UserInterface
- */
- public function eraseCredentials(): void
- {
- // If you store any temporary, sensitive data on the user, clear it here
- // $this->plainPassword = null;
- }
+ // [...]
}
.. tip::
@@ -2546,6 +2539,17 @@ the built-in ``is_granted()`` helper function in any Twig template:
.. _security-isgranted:
+Similarly, if you want to check if a specific user has a certain role, you can use
+the built-in ``is_granted_for_user()`` helper function:
+
+.. code-block:: html+twig
+
+ {% if is_granted_for_user(user, 'ROLE_ADMIN') %}
+ Delete
+ {% endif %}
+
+.. _security-isgrantedforuser:
+
Securing other Services
.......................
@@ -2582,6 +2586,19 @@ want to include extra details only for users that have a ``ROLE_SALES_ADMIN`` ro
// ...
}
+
+.. tip::
+
+ The ``isGranted()`` method checks authorization for the currently logged-in user.
+ If you need to check authorization for a different user or when the user session
+ is unavailable (e.g., in a CLI context such as a message queue or cron job), you
+ can use the ``isGrantedForUser()`` method to explicitly set the target user.
+
+ .. versionadded:: 7.3
+
+ The :method:`Symfony\\Bundle\\SecurityBundle\\Security::isGrantedForUser`
+ method was introduced in Symfony 7.3.
+
If you're using the :ref:`default services.yaml configuration `,
Symfony will automatically pass the ``security.helper`` to your service
thanks to autowiring and the ``Security`` type-hint.
@@ -2677,13 +2694,14 @@ anonymous users access by checking if there is no user set on the token::
// ...
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\User\UserInterface;
+ use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class PostVoter extends Voter
{
// ...
- protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
+ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{
// ...
@@ -2695,6 +2713,11 @@ anonymous users access by checking if there is no user set on the token::
}
}
+.. versionadded:: 7.3
+
+ The ``$vote`` argument of the ``voteOnAttribute()`` method was introduced
+ in Symfony 7.3.
+
Setting Individual User Permissions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2762,7 +2785,35 @@ object) are "compared" to see if they are "equal". By default, the core
your user will be logged out. This is a security measure to make sure that malicious
users can be de-authenticated if core user data changes.
-However, in some cases, this process can cause unexpected authentication problems.
+Storing the (plain or hashed) password in the session can be a security risk.
+To mitigate this, implement the ``__serialize()`` magic method in your user class
+to exclude or transform the password before storing the serialized user object
+in the session.
+
+Two strategies are supported:
+
+#. Remove the password completely. After unserialization, ``getPassword()`` returns
+ ``null`` and Symfony refreshes the user without checking the password. Use this
+ only if you store plaintext passwords (not recommended).
+#. Hash the password using the ``crc32c`` algorithm. Symfony will hash the password
+ of the refreshed user and compare it to the session value. This approach avoids
+ storing the real hash and lets you invalidate sessions on password change.
+
+ Example (assuming the password is stored in a private property called ``password``)::
+
+ public function __serialize(): array
+ {
+ $data = (array) $this;
+ $data["\0".self::class."\0password"] = hash('crc32c', $this->password);
+
+ return $data;
+ }
+
+.. versionadded:: 7.3
+
+ Support for hashing passwords with ``crc32c`` in session serialization was
+ introduced in Symfony 7.3.
+
If you're having problems authenticating, it could be that you *are* authenticating
successfully, but you immediately lose authentication after the first redirect.
diff --git a/security/access_token.rst b/security/access_token.rst
index c0ff4692676..70c9e21980e 100644
--- a/security/access_token.rst
+++ b/security/access_token.rst
@@ -411,6 +411,76 @@ and retrieve the user info:
;
};
+To enable `OpenID Connect Discovery`_, the ``OidcUserInfoTokenHandler``
+requires the ``symfony/cache`` package to store the OIDC configuration in
+the cache. If you haven't installed it yet, run the following command:
+
+.. code-block:: terminal
+
+ $ composer require symfony/cache
+
+Next, configure the ``base_uri`` and ``discovery`` options:
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/security.yaml
+ security:
+ firewalls:
+ main:
+ access_token:
+ token_handler:
+ oidc_user_info:
+ base_uri: https://www.example.com/realms/demo/
+ discovery:
+ cache: cache.app
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/security.php
+ use Symfony\Config\SecurityConfig;
+
+ return static function (SecurityConfig $security) {
+ $security->firewall('main')
+ ->accessToken()
+ ->tokenHandler()
+ ->oidcUserInfo()
+ ->baseUri('https://www.example.com/realms/demo/')
+ ->discovery()
+ ->cache('cache.app')
+ ;
+ };
+
+.. versionadded:: 7.3
+
+ Support for OpenID Connect Discovery was introduced in Symfony 7.3.
+
Following the `OpenID Connect Specification`_, the ``sub`` claim is used as user
identifier by default. To use another claim, specify it on the configuration:
@@ -545,8 +615,8 @@ If you haven't installed it yet, run this command:
$ composer require web-token/jwt-library
-Symfony provides a generic ``OidcTokenHandler`` to decode your token, validate
-it and retrieve the user info from it:
+Symfony provides a generic ``OidcTokenHandler`` that decodes the token, validates
+it, and retrieves the user information from it. Optionally, the token can be encrypted (JWE):
.. configuration-block::
@@ -567,6 +637,11 @@ it and retrieve the user info from it:
audience: 'api-example'
# Issuers (`iss` claim): required for validation purpose
issuers: ['https://oidc.example.com']
+ encryption:
+ enabled: true # Default to false
+ enforce: false # Default to false, requires an encrypted token when true
+ algorithms: ['ECDH-ES', 'A128GCM']
+ keyset: '{"keys": [...]}' # Encryption private keyset
.. code-block:: xml
@@ -592,6 +667,10 @@ it and retrieve the user info from it:
ES256RS256https://oidc.example.com
+
+ ECDH-ES
+ A128GCM
+
@@ -611,12 +690,20 @@ it and retrieve the user info from it:
->oidc()
// Algorithm used to sign the JWS
->algorithms(['ES256', 'RS256'])
- // A JSON-encoded JWK
+ // A JSON-encoded JWKSet (public keys)
->keyset('{"keys":[{"kty":"...","k":"..."}]}')
// Audience (`aud` claim): required for validation purpose
->audience('api-example')
// Issuers (`iss` claim): required for validation purpose
->issuers(['https://oidc.example.com'])
+ ->encryption()
+ ->enabled(true) //Default to false
+ ->enforce(false) //Default to false, requires an encrypted token when true
+ // Algorithm used to decrypt the JWE
+ ->algorithms(['ECDH-ES', 'A128GCM'])
+ // A JSON-encoded JWKSet (private keys)
+ ->keyset('{"keys":[...]}')
+
;
};
@@ -625,6 +712,88 @@ it and retrieve the user info from it:
The support of multiple algorithms to sign the JWS was introduced in Symfony 7.1.
In previous versions, only the ``ES256`` algorithm was supported.
+.. versionadded:: 7.3
+
+ Support for encryption algorithms to decrypt JWEs was introduced in Symfony 7.3.
+
+To enable `OpenID Connect Discovery`_, the ``OidcTokenHandler`` requires the
+``symfony/cache`` package to store the OIDC configuration in the cache. If you
+haven't installed it yet, run the following command:
+
+.. code-block:: terminal
+
+ $ composer require symfony/cache
+
+Then, you can remove the ``keyset`` configuration option (it will be imported
+from the OpenID Connect Discovery), and configure the ``discovery`` option:
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/security.yaml
+ security:
+ firewalls:
+ main:
+ access_token:
+ token_handler:
+ oidc:
+ claim: email
+ algorithms: ['ES256', 'RS256']
+ audience: 'api-example'
+ issuers: ['https://oidc.example.com']
+ discovery:
+ base_uri: https://www.example.com/realms/demo/
+ cache: cache.app
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+ ES256
+ RS256
+ https://oidc.example.com
+
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/security.php
+ use Symfony\Config\SecurityConfig;
+
+ return static function (SecurityConfig $security) {
+ $security->firewall('main')
+ ->accessToken()
+ ->tokenHandler()
+ ->oidc()
+ ->claim('email')
+ ->algorithms(['ES256', 'RS256'])
+ ->audience('api-example')
+ ->issuers(['https://oidc.example.com'])
+ ->discovery()
+ ->baseUri('https://www.example.com/realms/demo/')
+ ->cache('cache.app')
+ ;
+ };
+
Following the `OpenID Connect Specification`_, the ``sub`` claim is used by
default as user identifier. To use another claim, specify it on the
configuration:
@@ -925,5 +1094,6 @@ for :ref:`stateless firewalls `.
.. _`JSON Web Tokens (JWT)`: https://datatracker.ietf.org/doc/html/rfc7519
.. _`OpenID Connect (OIDC)`: https://en.wikipedia.org/wiki/OpenID#OpenID_Connect_(OIDC)
.. _`OpenID Connect Specification`: https://openid.net/specs/openid-connect-core-1_0.html
+.. _`OpenID Connect Discovery`: https://openid.net/specs/openid-connect-discovery-1_0.html
.. _`RFC6750`: https://datatracker.ietf.org/doc/html/rfc6750
.. _`SAML2 (XML structures)`: https://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html
diff --git a/security/csrf.rst b/security/csrf.rst
index 29fe96fa689..8797b4e7553 100644
--- a/security/csrf.rst
+++ b/security/csrf.rst
@@ -310,11 +310,26 @@ object evaluated to the id::
// ... do something, like deleting an object
}
+By default, the ``IsCsrfTokenValid`` attribute performs the CSRF token check for
+all HTTP methods. You can restrict this validation to specific methods using the
+``methods`` parameter. If the request uses a method not listed in the ``methods``
+array, the attribute is ignored for that request, and no CSRF validation occurs::
+
+ #[IsCsrfTokenValid('delete-item', tokenKey: 'token', methods: ['DELETE'])]
+ public function delete(Post $post): Response
+ {
+ // ... delete the object
+ }
+
.. versionadded:: 7.1
The :class:`Symfony\\Component\\Security\\Http\\Attribute\\IsCsrfTokenValid`
attribute was introduced in Symfony 7.1.
+.. versionadded:: 7.3
+
+ The ``methods`` parameter was introduced in Symfony 7.3.
+
CSRF Tokens and Compression Side-Channel Attacks
------------------------------------------------
diff --git a/security/custom_authenticator.rst b/security/custom_authenticator.rst
index f1a14013e4a..462ec21521c 100644
--- a/security/custom_authenticator.rst
+++ b/security/custom_authenticator.rst
@@ -229,13 +229,20 @@ requires a user and some sort of "credentials" (e.g. a password).
Use the
:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\UserBadge`
to attach the user to the passport. The ``UserBadge`` requires a user
-identifier (e.g. the username or email), which is used to load the user
-using :ref:`the user provider `::
+identifier (e.g. the username or email)::
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
// ...
- $passport = new Passport(new UserBadge($email), $credentials);
+ $passport = new Passport(new UserBadge($userIdentifier), $credentials);
+
+User Identifier
+~~~~~~~~~~~~~~~
+
+The user identifier is a unique string that identifies the user. It is often
+something like their email address or username, but it can be any unique value
+associated with the user. It allows loading the user through the configured
+:ref:`user provider `.
.. note::
@@ -275,6 +282,69 @@ using :ref:`the user provider `::
}
}
+Some applications normalize user identifiers before processing them. For example,
+lowercasing identifiers helps treat values like "john.doe", "John.Doe", or
+"JOHN.DOE" as equivalent in systems where identifiers are case-insensitive.
+
+If needed, you can pass a normalizer as the third argument to ``UserBadge``.
+This callable receives the ``$userIdentifier`` and must return a string.
+
+.. versionadded:: 7.3
+
+ Support for user identifier normalizers was introduced in Symfony 7.3.
+
+The example below uses a normalizer that converts usernames to a normalized,
+ASCII-only, lowercase format::
+
+ // src/Security/NormalizedUserBadge.php
+ namespace App\Security;
+
+ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
+ use function Symfony\Component\String\u;
+
+ final class NormalizedUserBadge extends UserBadge
+ {
+ public function __construct(string $identifier)
+ {
+ $callback = static fn (string $identifier): string => u($identifier)->normalize(UnicodeString::NFKC)->ascii()->lower()->toString();
+
+ parent::__construct($identifier, null, $callback);
+ }
+ }
+
+::
+
+ // src/Security/PasswordAuthenticator.php
+ namespace App\Security;
+
+ final class PasswordAuthenticator extends AbstractLoginFormAuthenticator
+ {
+ // simplified for brevity
+ public function authenticate(Request $request): Passport
+ {
+ $username = (string) $request->request->get('username', '');
+ $password = (string) $request->request->get('password', '');
+
+ $request->getSession()
+ ->set(SecurityRequestAttributes::LAST_USERNAME, $username);
+
+ return new Passport(
+ new NormalizedUserBadge($username),
+ new PasswordCredentials($password),
+ [
+ // all other useful badges
+ ]
+ );
+ }
+ }
+
+User Credential
+~~~~~~~~~~~~~~~
+
+The user credential is used to authenticate the user; that is, to verify
+the validity of the provided information (such as a password, an API token,
+or custom credentials).
+
The following credential classes are supported by default:
:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Credentials\\PasswordCredentials`
diff --git a/security/expressions.rst b/security/expressions.rst
index 569c7f093bf..a4ec02c7b84 100644
--- a/security/expressions.rst
+++ b/security/expressions.rst
@@ -201,6 +201,38 @@ Inside the subject's expression, you have access to two variables:
``args``
An array of controller arguments that are passed to the controller.
+Additionally to expressions, the ``#[IsGranted]`` attribute also accepts
+closures that return a boolean value. The subject can also be a closure that
+returns an array of values that will be injected into the closure::
+
+ // src/Controller/MyController.php
+ namespace App\Controller;
+
+ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+ use Symfony\Component\HttpFoundation\Response;
+ use Symfony\Component\Security\Http\Attribute\IsGranted;
+ use Symfony\Component\Security\Http\Attribute\IsGrantedContext;
+
+ class MyController extends AbstractController
+ {
+ #[IsGranted(static function (IsGrantedContext $context, mixed $subject) {
+ return $context->user === $subject['post']->getAuthor();
+ }, subject: static function (array $args) {
+ return [
+ 'post' => $args['post'],
+ ];
+ })]
+ public function index($post): Response
+ {
+ // ...
+ }
+ }
+
+.. versionadded:: 7.3
+
+ The support for closures in the ``#[IsGranted]`` attribute was introduced
+ in Symfony 7.3 and requires PHP 8.5.
+
Learn more
----------
diff --git a/security/ldap.rst b/security/ldap.rst
index 081be764290..c4c3646122b 100644
--- a/security/ldap.rst
+++ b/security/ldap.rst
@@ -256,6 +256,24 @@ This is the default role you wish to give to a user fetched from the LDAP
server. If you do not configure this key, your users won't have any roles,
and will not be considered as authenticated fully.
+role_fetcher
+............
+
+**Type**: ``string`` **Default**: ``null``
+
+When your LDAP service provides user roles, this option allows you to define
+the service that retrieves these roles. The role fetcher service must implement
+the ``Symfony\Component\Ldap\Security\RoleFetcherInterface``. When this option
+is set, the ``default_roles`` option is ignored.
+
+Symfony provides ``Symfony\Component\Ldap\Security\MemberOfRoles``, a concrete
+implementation of the interface that fetches roles from the ``ismemberof``
+attribute.
+
+.. versionadded:: 7.3
+
+ The ``role_fetcher`` configuration option was introduced in Symfony 7.3.
+
uid_key
.......
diff --git a/security/voters.rst b/security/voters.rst
index 28d71f7b7c8..e621263abb4 100644
--- a/security/voters.rst
+++ b/security/voters.rst
@@ -40,14 +40,20 @@ or extend :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\Vote
which makes creating a voter even easier::
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+ use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
abstract class Voter implements VoterInterface
{
abstract protected function supports(string $attribute, mixed $subject): bool;
- abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool;
+ abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool;
}
+.. versionadded:: 7.3
+
+ The ``$vote`` argument of the ``voteOnAttribute()`` method was introduced
+ in Symfony 7.3.
+
.. _how-to-use-the-voter-in-a-controller:
Setup: Checking for Access in a Controller
@@ -132,6 +138,7 @@ would look like this::
use App\Entity\Post;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+ use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class PostVoter extends Voter
@@ -155,12 +162,13 @@ would look like this::
return true;
}
- protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
+ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
+ $vote?->addReason('The user is not logged in.');
return false;
}
@@ -170,7 +178,7 @@ would look like this::
return match($attribute) {
self::VIEW => $this->canView($post, $user),
- self::EDIT => $this->canEdit($post, $user),
+ self::EDIT => $this->canEdit($post, $user, $vote),
default => throw new \LogicException('This code should not be reached!')
};
}
@@ -186,10 +194,19 @@ would look like this::
return !$post->isPrivate();
}
- private function canEdit(Post $post, User $user): bool
+ private function canEdit(Post $post, User $user, ?Vote $vote): bool
{
- // this assumes that the Post object has a `getOwner()` method
- return $user === $post->getOwner();
+ // this assumes that the Post object has a `getAuthor()` method
+ if ($user === $post->getAuthor()) {
+ return true;
+ }
+
+ $vote?->addReason(sprintf(
+ 'The logged in user (username: %s) is not the author of this post (id: %d).',
+ $user->getUsername(), $post->getId()
+ ));
+
+ return false;
}
}
@@ -207,11 +224,12 @@ To recap, here's what's expected from the two abstract methods:
return ``true`` if the attribute is ``view`` or ``edit`` and if the object is
a ``Post`` instance.
-``voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token)``
+``voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null)``
If you return ``true`` from ``supports()``, then this method is called. Your
job is to return ``true`` to allow access and ``false`` to deny access.
- The ``$token`` can be used to find the current user object (if any). In this
- example, all of the complex business logic is included to determine access.
+ The ``$token`` can be used to find the current user object (if any).
+ The ``$vote`` argument can be used to provide an explanation for the vote.
+ This explanation is included in log messages and on exception pages.
.. _declaring-the-voter-as-a-service:
@@ -248,7 +266,7 @@ with ``ROLE_SUPER_ADMIN``::
) {
}
- protected function voteOnAttribute($attribute, mixed $subject, TokenInterface $token): bool
+ protected function voteOnAttribute($attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{
// ...
diff --git a/serializer.rst b/serializer.rst
index 4d87277b653..68357bbe6c5 100644
--- a/serializer.rst
+++ b/serializer.rst
@@ -1387,6 +1387,14 @@ normalizers (in order of priority):
By default, an exception is thrown when data is not a valid backed enumeration. If you
want ``null`` instead, you can set the ``BackedEnumNormalizer::ALLOW_INVALID_VALUES`` option.
+:class:`Symfony\\Component\\Serializer\\Normalizer\\NumberNormalizer`
+ This normalizer converts between :phpclass:`BcMath\\Number` or :phpclass:`GMP` objects and
+ strings or integers.
+
+.. versionadded:: 7.2
+
+ The ``NumberNormalizer`` was introduced in Symfony 7.2.
+
:class:`Symfony\\Component\\Serializer\\Normalizer\\DataUriNormalizer`
This normalizer converts between :phpclass:`SplFileInfo` objects and a
`data URI`_ string (``data:...``) such that files can be embedded into
@@ -2376,6 +2384,70 @@ correct class for properties typed as ``InvoiceItemInterface``::
$invoiceLine = $serializer->deserialize($jsonString, InvoiceLine::class, 'json');
// $invoiceLine contains new InvoiceLine(new Product(...))
+You can add a default type to avoid the need to add the type property
+when deserializing:
+
+.. configuration-block::
+
+ .. code-block:: php-attributes
+
+ namespace App\Model;
+
+ use Symfony\Component\Serializer\Attribute\DiscriminatorMap;
+
+ #[DiscriminatorMap(
+ typeProperty: 'type',
+ mapping: [
+ 'product' => Product::class,
+ 'shipping' => Shipping::class,
+ ],
+ defaultType: 'product',
+ )]
+ interface InvoiceItemInterface
+ {
+ // ...
+ }
+
+ .. code-block:: yaml
+
+ App\Model\InvoiceItemInterface:
+ discriminator_map:
+ type_property: type
+ mapping:
+ product: 'App\Model\Product'
+ shipping: 'App\Model\Shipping'
+ default_type: product
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+
+Now it deserializes like this:
+
+.. configuration-block::
+
+ .. code-block:: php
+
+ // $jsonString does NOT contain "type" in "invoiceItem"
+ $invoiceLine = $serializer->deserialize('{"invoiceItem":{...},...}', InvoiceLine::class, 'json');
+ // $invoiceLine contains new InvoiceLine(new Product(...))
+
+.. versionadded:: 7.3
+
+ The ``defaultType`` parameter was added in Symfony 7.3.
+
.. _serializer-unwrapping-denormalizer:
Deserializing Input Partially (Unwrapping)
diff --git a/serializer/encoders.rst b/serializer/encoders.rst
index 619631967c9..8238d4d057d 100644
--- a/serializer/encoders.rst
+++ b/serializer/encoders.rst
@@ -205,11 +205,16 @@ These are the options available on the :ref:`serializer context &]/``)
A regular expression pattern to determine if a value should be wrapped
in a CDATA section.
+``ignore_empty_attributes`` (default: ``false``)
+ If set to true, ignores all attributes with empty values in the generated XML
.. versionadded:: 7.1
The ``cdata_wrapping_pattern`` option was introduced in Symfony 7.1.
+.. versionadded:: 7.3
+
+ The ``ignore_empty_attributes`` option was introduced in Symfony 7.3.
Example with a custom ``context``::
diff --git a/service_container.rst b/service_container.rst
index 30b69b8aa14..6086ae1d946 100644
--- a/service_container.rst
+++ b/service_container.rst
@@ -162,10 +162,6 @@ each time you ask for it.
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
- exclude:
- - '../src/DependencyInjection/'
- - '../src/Entity/'
- - '../src/Kernel.php'
# order is important in this file because service definitions
# always *replace* previous ones; add your own service configuration below
@@ -187,7 +183,7 @@ each time you ask for it.
-
+
@@ -212,8 +208,7 @@ each time you ask for it.
// makes classes in src/ available to be used as services
// this creates a service per class whose id is the fully-qualified class name
- $services->load('App\\', '../src/')
- ->exclude('../src/{DependencyInjection,Entity,Kernel.php}');
+ $services->load('App\\', '../src/');
// order is important in this file because service definitions
// always *replace* previous ones; add your own service configuration below
@@ -221,15 +216,57 @@ each time you ask for it.
.. tip::
- The value of the ``resource`` and ``exclude`` options can be any valid
- `glob pattern`_. The value of the ``exclude`` option can also be an
- array of glob patterns.
+ The value of the ``resource`` option can be any valid `glob pattern`_.
Thanks to this configuration, you can automatically use any classes from the
``src/`` directory as a service, without needing to manually configure
it. Later, you'll learn how to :ref:`import many services at once
` with resource.
+ If some files or directories in your project should not become services, you
+ can exclude them using the ``exclude`` option:
+
+ .. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/services.yaml
+ services:
+ # ...
+ App\:
+ resource: '../src/'
+ exclude:
+ - '../src/SomeDirectory/'
+ - '../src/AnotherDirectory/'
+ - '../src/SomeFile.php'
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // config/services.php
+ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
+
+ return function(ContainerConfigurator $container): void {
+ // ...
+
+ $services->load('App\\', '../src/')
+ ->exclude('../src/{SomeDirectory,AnotherDirectory,Kernel.php}');
+ };
+
If you'd prefer to manually wire your service, you can
:ref:`use explicit configuration `.
diff --git a/service_container/alias_private.rst b/service_container/alias_private.rst
index f99f7cb5f3e..22bf649d861 100644
--- a/service_container/alias_private.rst
+++ b/service_container/alias_private.rst
@@ -181,6 +181,32 @@ This means that when using the container directly, you can access the
# ...
app.mailer: '@App\Mail\PhpMailer'
+The ``#[AsAlias]`` attribute can also be limited to one or more specific
+:ref:`config environments ` using the ``when`` argument::
+
+ // src/Mail/PhpMailer.php
+ namespace App\Mail;
+
+ // ...
+ use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+
+ #[AsAlias(id: 'app.mailer', when: 'dev')]
+ class PhpMailer
+ {
+ // ...
+ }
+
+ // pass an array to apply it in multiple config environments
+ #[AsAlias(id: 'app.mailer', when: ['dev', 'test'])]
+ class PhpMailer
+ {
+ // ...
+ }
+
+.. versionadded:: 7.3
+
+ The ``when`` argument of the ``#[AsAlias]`` attribute was introduced in Symfony 7.3.
+
.. tip::
When using ``#[AsAlias]`` attribute, you may omit passing ``id`` argument
diff --git a/service_container/debug.rst b/service_container/debug.rst
index c09413e7213..9e3e28a5343 100644
--- a/service_container/debug.rst
+++ b/service_container/debug.rst
@@ -52,5 +52,8 @@ its id:
$ php bin/console debug:container App\Service\Mailer
- # to show the service arguments:
- $ php bin/console debug:container App\Service\Mailer --show-arguments
+.. deprecated:: 7.3
+
+ Starting in Symfony 7.3, this command displays the service arguments by default.
+ In earlier Symfony versions, you needed to use the ``--show-arguments`` option,
+ which is now deprecated.
diff --git a/service_container/import.rst b/service_container/import.rst
index 91021762eae..47af34d3a34 100644
--- a/service_container/import.rst
+++ b/service_container/import.rst
@@ -82,7 +82,6 @@ a relative or absolute path to the imported file:
App\:
resource: '../src/*'
- exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
# ...
@@ -104,8 +103,7 @@ a relative or absolute path to the imported file:
-
+
@@ -127,8 +125,7 @@ a relative or absolute path to the imported file:
->autoconfigure()
;
- $services->load('App\\', '../src/*')
- ->exclude('../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}');
+ $services->load('App\\', '../src/*');
};
When loading a configuration file, Symfony first processes all imported files in
diff --git a/service_container/lazy_services.rst b/service_container/lazy_services.rst
index 23d76a4cfbf..abb3c2cca7f 100644
--- a/service_container/lazy_services.rst
+++ b/service_container/lazy_services.rst
@@ -26,9 +26,6 @@ until you interact with the proxy in some way.
Lazy services do not support `final`_ or ``readonly`` classes, but you can use
`Interface Proxifying`_ to work around this limitation.
- In PHP versions prior to 8.0 lazy services do not support parameters with
- default values for built-in PHP classes (e.g. ``PDO``).
-
.. _lazy-services_configuration:
Configuration
@@ -78,11 +75,6 @@ same signature of the class representing the service should be injected. A lazy
itself when being accessed for the first time). The same happens when calling
``Container::get()`` directly.
-To check if your lazy service works you can check the interface of the received object::
-
- dump(class_implements($service));
- // the output should include "Symfony\Component\VarExporter\LazyObjectInterface"
-
You can also configure your service's laziness thanks to the
:class:`Symfony\\Component\\DependencyInjection\\Attribute\\Autoconfigure` attribute.
For example, to define your service as lazy use the following::
diff --git a/service_container/service_closures.rst b/service_container/service_closures.rst
index cedbaaa2bf9..88b0ab64002 100644
--- a/service_container/service_closures.rst
+++ b/service_container/service_closures.rst
@@ -52,6 +52,13 @@ argument of type ``service_closure``:
# In case the dependency is optional
# arguments: [!service_closure '@?mailer']
+ # you can also use the special '@>' syntax as a shortcut of '!service_closure'
+ App\Service\AnotherService:
+ arguments: ['@>mailer']
+
+ # the shortcut also works for optional dependencies
+ # arguments: ['@>?mailer']
+
.. code-block:: xml
@@ -90,6 +97,10 @@ argument of type ``service_closure``:
// ->args([service_closure('mailer')->ignoreOnInvalid()]);
};
+.. versionadded:: 7.3
+
+ The ``@>`` shortcut syntax for YAML was introduced in Symfony 7.3.
+
.. seealso::
Service closures can be injected :ref:`by using autowiring `
diff --git a/service_container/tags.rst b/service_container/tags.rst
index bc3d28c90ff..3a547042de7 100644
--- a/service_container/tags.rst
+++ b/service_container/tags.rst
@@ -1289,4 +1289,19 @@ be used directly on the class of the service you want to configure::
// ...
}
+You can apply the ``#[AsTaggedItem]`` attribute multiple times to register the
+same service under different indexes::
+
+ #[AsTaggedItem(index: 'handler_one', priority: 5)]
+ #[AsTaggedItem(index: 'handler_two', priority: 20)]
+ class SomeService
+ {
+ // ...
+ }
+
+.. versionadded:: 7.3
+
+ The feature to apply the ``#[AsTaggedItem]`` attribute multiple times was
+ introduced in Symfony 7.3.
+
.. _`PHP constructor promotion`: https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion
diff --git a/setup.rst b/setup.rst
index b93b62f989c..20d71d112eb 100644
--- a/setup.rst
+++ b/setup.rst
@@ -48,10 +48,10 @@ application:
.. code-block:: terminal
# run this if you are building a traditional web application
- $ symfony new my_project_directory --version="7.2.x" --webapp
+ $ symfony new my_project_directory --version="7.3.x-dev" --webapp
# run this if you are building a microservice, console application or API
- $ symfony new my_project_directory --version="7.2.x"
+ $ symfony new my_project_directory --version="7.3.x-dev"
The only difference between these two commands is the number of packages
installed by default. The ``--webapp`` option installs extra packages to give
@@ -63,12 +63,12 @@ Symfony application using Composer:
.. code-block:: terminal
# run this if you are building a traditional web application
- $ composer create-project symfony/skeleton:"7.2.x" my_project_directory
+ $ composer create-project symfony/skeleton:"7.3.x-dev" my_project_directory
$ cd my_project_directory
$ composer require webapp
# run this if you are building a microservice, console application or API
- $ composer create-project symfony/skeleton:"7.2.x" my_project_directory
+ $ composer create-project symfony/skeleton:"7.3.x-dev" my_project_directory
No matter which command you run to create the Symfony application. All of them
will create a new ``my_project_directory/`` directory, download some dependencies
diff --git a/string.rst b/string.rst
index 43d3a236ab6..e51e7d1b502 100644
--- a/string.rst
+++ b/string.rst
@@ -234,8 +234,10 @@ Methods to Change Case
u('Foo: Bar-baz.')->snake(); // 'foo_bar_baz'
// changes all graphemes/code points to kebab-case
u('Foo: Bar-baz.')->kebab(); // 'foo-bar-baz'
- // other cases can be achieved by chaining methods. E.g. PascalCase:
- u('Foo: Bar-baz.')->camel()->title(); // 'FooBarBaz'
+ // changes all graphemes/code points to PascalCase
+ u('Foo: Bar-baz.')->pascal(); // 'FooBarBaz'
+ // other cases can be achieved by chaining methods, e.g. :
+ u('Foo: Bar-baz.')->camel()->upper(); // 'FOOBARBAZ'
.. versionadded:: 7.1
@@ -246,6 +248,10 @@ Methods to Change Case
The ``kebab()`` method was introduced in Symfony 7.2.
+.. versionadded:: 7.3
+
+ The ``pascal()`` method was introduced in Symfony 7.3.
+
The methods of all string classes are case-sensitive by default. You can perform
case-insensitive operations with the ``ignoreCase()`` method::
diff --git a/templates.rst b/templates.rst
index 1347b3303a5..c33088e18dc 100644
--- a/templates.rst
+++ b/templates.rst
@@ -870,7 +870,6 @@ errors. It's useful to run it before deploying your application to production
$ php bin/console lint:twig templates/article/recent_list.html.twig
# you can also show the deprecated features used in your templates
- # (only the first deprecation is shown, so run multiple times to catch all)
$ php bin/console lint:twig --show-deprecations templates/email/
# you can also excludes directories
@@ -880,6 +879,11 @@ errors. It's useful to run it before deploying your application to production
The option to exclude directories was introduced in Symfony 7.1.
+.. versionadded:: 7.3
+
+ Before Symfony 7.3, the ``--show-deprecations`` option only displayed the
+ first deprecation found, so you had to run the command repeatedly.
+
When running the linter inside `GitHub Actions`_, the output is automatically
adapted to the format required by GitHub, but you can force that format too:
@@ -1549,23 +1553,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);
@@ -1575,24 +1576,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;
@@ -1604,6 +1600,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
..................................
@@ -1627,10 +1635,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 9356f2013a7..09cddfa55bb 100644
--- a/testing.rst
+++ b/testing.rst
@@ -714,6 +714,29 @@ stores in the session of the test client. If you need to define custom
attributes in this token, you can use the ``tokenAttributes`` argument of the
:method:`Symfony\\Bundle\\FrameworkBundle\\KernelBrowser::loginUser` method.
+You can also use an :ref:`in-memory user ` in your tests
+by instantiating :class:`Symfony\\Component\\Security\\Core\\User\\InMemoryUser` directly::
+
+ // tests/Controller/ProfileControllerTest.php
+ use Symfony\Component\Security\Core\User\InMemoryUser;
+
+ $client = static::createClient();
+ $testUser = new InMemoryUser('admin', 'password', ['ROLE_ADMIN']);
+ $client->loginUser($testUser);
+
+Before doing this, you must define the in-memory user in your test environment
+configuration to ensure it exists and can be authenticated::
+
+.. code-block:: yaml
+
+ # config/packages/security.yaml
+ when@test:
+ security:
+ users_in_memory:
+ memory:
+ users:
+ admin: { password: password, roles: ROLE_ADMIN }
+
To set a specific firewall (``main`` is set by default)::
$client->loginUser($testUser, 'my_firewall');
diff --git a/translation.rst b/translation.rst
index f41d0cb6489..d431e1cd215 100644
--- a/translation.rst
+++ b/translation.rst
@@ -416,6 +416,84 @@ You can also specify the message domain and pass some additional variables:
major difference: automatic output escaping is **not** applied to translations
using a tag.
+Global Translation Parameters
+-----------------------------
+
+.. versionadded:: 7.3
+
+ The global translation parameters feature was introduced in Symfony 7.3.
+
+If the content of a translation parameter is repeated across multiple
+translation messages (e.g. a company name, or a version number), you can define
+it as a global translation parameter. This helps you avoid repeating the same
+values manually in each message.
+
+You can configure these global parameters in the ``translations.globals`` option
+of your main configuration file using either ``%...%`` or ``{...}`` syntax:
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/translator.yaml
+ translator:
+ # ...
+ globals:
+ # when using the '%' wrapping characters, you must escape them
+ '%%app_name%%': 'My application'
+ '{app_version}': '1.2.3'
+ '{url}': { message: 'url', parameters: { scheme: 'https://' }, domain: 'global' }
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+ My application
+
+
+ https://
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/translator.php
+ use Symfony\Config\TwigConfig;
+
+ return static function (TwigConfig $translator): void {
+ // ...
+ // when using the '%' wrapping characters, you must escape them
+ $translator->globals('%%app_name%%')->value('My application');
+ $translator->globals('{app_version}')->value('1.2.3');
+ $translator->globals('{url}')->value(['message' => 'url', 'parameters' => ['scheme' => 'https://']]);
+ };
+
+Once defined, you can use these parameters in translation messages anywhere in
+your application:
+
+.. code-block:: twig
+
+ {{ 'Application version: {app_version}'|trans }}
+ {# output: "Application version: 1.2.3" #}
+
+ {# parameters passed to the message override global parameters #}
+ {{ 'Package version: {app_version}'|trans({'{app_version}': '2.3.4'}) }}
+ # Displays "Package version: 2.3.4"
+
Forcing the Translator Locale
-----------------------------
diff --git a/validation.rst b/validation.rst
index 4905283b18b..cfa8154b627 100644
--- a/validation.rst
+++ b/validation.rst
@@ -327,99 +327,13 @@ literature genre mostly associated with the author, which can be set to either
{
// ...
- $metadata->addPropertyConstraint('genre', new Assert\Choice([
- 'choices' => ['fiction', 'non-fiction'],
- 'message' => 'Choose a valid genre.',
- ]));
+ $metadata->addPropertyConstraint('genre', new Assert\Choice(
+ choices: ['fiction', 'non-fiction'],
+ message: 'Choose a valid genre.',
+ ));
}
}
-.. _validation-default-option:
-
-The options of a constraint can always be passed in as an array. Some constraints,
-however, also allow you to pass the value of one, "*default*", option in place
-of the array. In the case of the ``Choice`` constraint, the ``choices``
-options can be specified in this way.
-
-.. configuration-block::
-
- .. code-block:: php-attributes
-
- // src/Entity/Author.php
- namespace App\Entity;
-
- // ...
- use Symfony\Component\Validator\Constraints as Assert;
-
- class Author
- {
- #[Assert\Choice(['fiction', 'non-fiction'])]
- private string $genre;
-
- // ...
- }
-
- .. code-block:: yaml
-
- # config/validator/validation.yaml
- App\Entity\Author:
- properties:
- genre:
- - Choice: [fiction, non-fiction]
- # ...
-
- .. code-block:: xml
-
-
-
-
-
-
-
-
- fiction
- non-fiction
-
-
-
-
-
-
-
- .. code-block:: php
-
- // src/Entity/Author.php
- namespace App\Entity;
-
- // ...
- use Symfony\Component\Validator\Constraints as Assert;
- use Symfony\Component\Validator\Mapping\ClassMetadata;
-
- class Author
- {
- private string $genre;
-
- public static function loadValidatorMetadata(ClassMetadata $metadata): void
- {
- // ...
-
- $metadata->addPropertyConstraint(
- 'genre',
- new Assert\Choice(['fiction', 'non-fiction'])
- );
- }
- }
-
-This is purely meant to make the configuration of the most common option of
-a constraint shorter and quicker.
-
-If you're ever unsure of how to specify an option, either check the namespace
-``Symfony\Component\Validator\Constraints`` for the constraint or play it safe
-by always passing in an array of options (the first method shown above).
-
Constraints in Form Classes
---------------------------
@@ -520,7 +434,7 @@ class to have at least 3 characters.
$metadata->addPropertyConstraint('firstName', new Assert\NotBlank());
$metadata->addPropertyConstraint(
'firstName',
- new Assert\Length(['min' => 3])
+ new Assert\Length(min: 3)
);
}
}
@@ -603,9 +517,9 @@ this method must return ``true``:
{
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue([
- 'message' => 'The password cannot match your first name',
- ]));
+ $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue(
+ message: 'The password cannot match your first name',
+ ));
}
}
diff --git a/validation/custom_constraint.rst b/validation/custom_constraint.rst
index ff70eebdd65..10584a36383 100644
--- a/validation/custom_constraint.rst
+++ b/validation/custom_constraint.rst
@@ -181,6 +181,7 @@ The ``addViolation()`` method call finally adds the violation to the context.
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
-----------------------
@@ -254,7 +255,7 @@ You can use custom validators like the ones provided by Symfony itself:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$metadata->addPropertyConstraint('name', new NotBlank());
- $metadata->addPropertyConstraint('name', new ContainsAlphanumeric(['mode' => 'loose']));
+ $metadata->addPropertyConstraint('name', new ContainsAlphanumeric(mode: 'loose'));
}
}
@@ -279,6 +280,7 @@ define those options as public properties on the constraint class::
// src/Validator/Foo.php
namespace App\Validator;
+ use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
#[\Attribute]
@@ -288,6 +290,7 @@ define those options as public properties on the constraint class::
public $message = 'This value is invalid';
public $optionalBarOption = false;
+ #[HasNamedArguments]
public function __construct(
$mandatoryFooOption,
?string $message = null,
@@ -408,10 +411,10 @@ the custom options like you pass any other option in built-in constraints:
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addPropertyConstraint('name', new NotBlank());
- $metadata->addPropertyConstraint('name', new Foo([
- 'mandatoryFooOption' => 'bar',
- 'optionalBarOption' => true,
- ]));
+ $metadata->addPropertyConstraint('name', new Foo(
+ mandatoryFooOption: 'bar',
+ optionalBarOption: true,
+ ));
}
}
diff --git a/validation/groups.rst b/validation/groups.rst
index 8d84e52c0da..55625be702d 100644
--- a/validation/groups.rst
+++ b/validation/groups.rst
@@ -101,21 +101,21 @@ user registers and when a user updates their contact information later:
{
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('email', new Assert\Email([
- 'groups' => ['registration'],
- ]));
-
- $metadata->addPropertyConstraint('password', new Assert\NotBlank([
- 'groups' => ['registration'],
- ]));
- $metadata->addPropertyConstraint('password', new Assert\Length([
- 'min' => 7,
- 'groups' => ['registration'],
- ]));
-
- $metadata->addPropertyConstraint('city', new Assert\Length([
- 'min' => 2,
- ]));
+ $metadata->addPropertyConstraint('email', new Assert\Email(
+ groups: ['registration'],
+ ));
+
+ $metadata->addPropertyConstraint('password', new Assert\NotBlank(
+ groups: ['registration'],
+ ));
+ $metadata->addPropertyConstraint('password', new Assert\Length(
+ min: 7,
+ groups: ['registration'],
+ ));
+
+ $metadata->addPropertyConstraint('city', new Assert\Length(
+ min: 2,
+ ));
}
}
diff --git a/validation/sequence_provider.rst b/validation/sequence_provider.rst
index 836568c2327..c316a85d249 100644
--- a/validation/sequence_provider.rst
+++ b/validation/sequence_provider.rst
@@ -104,10 +104,10 @@ username and the password are different only if all other validation passes
$metadata->addPropertyConstraint('username', new Assert\NotBlank());
$metadata->addPropertyConstraint('password', new Assert\NotBlank());
- $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue([
- 'message' => 'The password cannot match your first name',
- 'groups' => ['Strict'],
- ]));
+ $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue(
+ message: 'The password cannot match your first name',
+ groups: ['Strict'],
+ ));
$metadata->setGroupSequence(['User', 'Strict']);
}
@@ -249,10 +249,10 @@ entity and a new constraint group called ``Premium``:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$metadata->addPropertyConstraint('name', new Assert\NotBlank());
- $metadata->addPropertyConstraint('creditCard', new Assert\CardScheme([
- 'schemes' => [Assert\CardScheme::VISA],
- 'groups' => ['Premium'],
- ]));
+ $metadata->addPropertyConstraint('creditCard', new Assert\CardScheme(
+ schemes: [Assert\CardScheme::VISA],
+ groups: ['Premium'],
+ ));
}
}
diff --git a/validation/severity.rst b/validation/severity.rst
index 632a99519d9..154c13d5e3e 100644
--- a/validation/severity.rst
+++ b/validation/severity.rst
@@ -105,15 +105,15 @@ Use the ``payload`` option to configure the error level for each constraint:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('username', new Assert\NotBlank([
- 'payload' => ['severity' => 'error'],
- ]));
- $metadata->addPropertyConstraint('password', new Assert\NotBlank([
- 'payload' => ['severity' => 'error'],
- ]));
- $metadata->addPropertyConstraint('bankAccountNumber', new Assert\Iban([
- 'payload' => ['severity' => 'warning'],
- ]));
+ $metadata->addPropertyConstraint('username', new Assert\NotBlank(
+ payload: ['severity' => 'error'],
+ ));
+ $metadata->addPropertyConstraint('password', new Assert\NotBlank(
+ payload: ['severity' => 'error'],
+ ));
+ $metadata->addPropertyConstraint('bankAccountNumber', new Assert\Iban(
+ payload: ['severity' => 'warning'],
+ ));
}
}
diff --git a/validation/translations.rst b/validation/translations.rst
index 9a4ece17736..db2cd518eb7 100644
--- a/validation/translations.rst
+++ b/validation/translations.rst
@@ -83,9 +83,9 @@ property is not empty, add the following:
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
- $metadata->addPropertyConstraint('name', new NotBlank([
- 'message' => 'author.name.not_blank',
- ]));
+ $metadata->addPropertyConstraint('name', new NotBlank(
+ message: 'author.name.not_blank',
+ ));
}
}
@@ -136,13 +136,13 @@ You can also use :class:`Symfony\\Component\\Translation\\TranslatableMessage` t
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
-
+
#[Assert\Callback]
public function validate(ExecutionContextInterface $context, mixed $payload): void
{
// somehow you have an array of "fake names"
$fakeNames = [/* ... */];
-
+
// check if the name is actually a fake name
if (in_array($this->getFirstName(), $fakeNames, true)) {
$context->buildViolation(new TranslatableMessage('author.name.fake', [], 'validators'))
diff --git a/webhook.rst b/webhook.rst
index d3c16d2a411..d27a6e6d906 100644
--- a/webhook.rst
+++ b/webhook.rst
@@ -28,6 +28,7 @@ Currently, the following third-party mailer providers support webhooks:
============== ============================================
Mailer Service Parser service name
============== ============================================
+AhaSend ``mailer.webhook.request_parser.ahasend``
Brevo ``mailer.webhook.request_parser.brevo``
Mandrill ``mailer.webhook.request_parser.mailchimp``
MailerSend ``mailer.webhook.request_parser.mailersend``
@@ -47,9 +48,13 @@ Sweego ``mailer.webhook.request_parser.sweego``
.. versionadded:: 7.2
- The ``Mandrill``, ``Mailomat``, ``Mailtrap``, and ``Sweego`` integrations were introduced in
+ The ``Mandrill``, ``Mailomat``, ``Mailtrap``, and ``Sweego`` integrations were introduced in
Symfony 7.2.
+.. versionadded:: 7.3
+
+ The ``AhaSend`` integration was introduced in Symfony 7.3.
+
.. note::
Install the third-party mailer provider you want to use as described in the
@@ -171,6 +176,7 @@ Currently, the following third-party SMS transports support webhooks:
SMS service Parser service name
============ ==========================================
Twilio ``notifier.webhook.request_parser.twilio``
+Smsbox ``notifier.webhook.request_parser.smsbox``
Sweego ``notifier.webhook.request_parser.sweego``
Vonage ``notifier.webhook.request_parser.vonage``
============ ==========================================
diff --git a/workflow.rst b/workflow.rst
index 6949053d75a..8ebaf05e784 100644
--- a/workflow.rst
+++ b/workflow.rst
@@ -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
----------