+
Then retrieve it from your JS file:
.. code-block:: javascript
@@ -288,6 +296,9 @@ Then retrieve it from your JS file:
const eventSource = new EventSource(url);
// ...
+ // with Stimulus
+ this.eventSource = new EventSource(this.mercureUrlValue);
+
Mercure also allows subscribing to several topics,
and to use URI Templates or the special value ``*`` (matched by all topics)
as patterns:
@@ -307,24 +318,24 @@ as patterns:
}
+However, on the client side (i.e. in JavaScript's ``EventSource``), there is no
+built-in way to know which topic a certain message originates from. If this (or
+any other meta information) is important to you, you need to include it in the
+message's data (e.g. by adding a key to the JSON, or a ``data-*`` attribute to
+the HTML).
+
.. tip::
- Google Chrome DevTools natively integrate a `practical UI`_ displaying in live
- the received events:
+ Test if a URI Template matches a URL using `the online debugger`_
- .. image:: /_images/mercure/chrome.png
- :alt: The Chrome DevTools showing the EventStream tab containing information about each SSE event.
-
- To use it:
+.. tip::
- * open the DevTools
- * select the "Network" tab
- * click on the request to the Mercure hub
- * click on the "EventStream" sub-tab.
+ Google Chrome features a practical UI to display the received events:
-.. tip::
+ .. image:: /_images/mercure/chrome.png
+ :alt: The Chrome DevTools showing the EventStream tab containing information about each SSE event.
- Test if a URI Template match a URL using `the online debugger`_
+ In DevTools, select the "Network" tab, then click on the request to the Mercure hub, then on the "EventStream" sub-tab.
Discovery
---------
@@ -446,7 +457,7 @@ Using cookies is the most secure and preferred way when the client is a web
browser. If the client is not a web browser, then using an authorization header
is the way to go.
-.. caution::
+.. warning::
To use the cookie authentication method, the Symfony app and the Hub
must be served from the same domain (can be different sub-domains).
@@ -501,14 +512,12 @@ And here is the controller::
}
}
-
.. tip::
You cannot use the ``mercure()`` helper and the ``setCookie()``
method at the same time (it would set the cookie twice on a single request). Choose
either one method or the other.
-
Programmatically Generating The JWT Used to Publish
---------------------------------------------------
@@ -675,10 +684,11 @@ sent:
.. code-block:: yaml
# config/services_test.yaml
- mercure.hub.default:
- class: App\Tests\Functional\Stub\HubStub
+ services:
+ mercure.hub.default:
+ class: App\Tests\Functional\Stub\HubStub
-As MercureBundle support multiple hubs, you may have to replace
+As MercureBundle supports multiple hubs, you may have to replace
the other service definitions accordingly.
.. tip::
@@ -692,8 +702,6 @@ Debugging
The WebProfiler panel was introduced in MercureBundle 0.2.
-Enable the panel in your configuration, as follows:
-
MercureBundle is shipped with a debug panel. Install the Debug pack to
enable it::
@@ -705,6 +713,9 @@ enable it::
:alt: The Mercure panel of the Symfony Profiler, showing information like time, memory, topics and data of each message sent by Mercure.
:class: with-browser
+The Mercure hub itself provides a debug tool that can be enabled and it's
+available on ``/.well-known/mercure/ui/``
+
Async dispatching
-----------------
@@ -768,7 +779,6 @@ Going further
.. _`JSON Web Token`: https://tools.ietf.org/html/rfc7519
.. _`example JWT`: https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.iHLdpAEjX4BqCsHJEegxRmO-Y6sMxXwNATrQyRNt3GY
.. _`IRI`: https://tools.ietf.org/html/rfc3987
-.. _`practical UI`: https://twitter.com/ChromeDevTools/status/562324683194785792
.. _`the dedicated API Platform documentation`: https://api-platform.com/docs/core/mercure/
.. _`the online debugger`: https://uri-template-tester.mercure.rocks
.. _`a feature to test applications using Mercure`: https://github.com/symfony/panther#creating-isolated-browsers-to-test-apps-using-mercure-or-websocket
diff --git a/messenger.rst b/messenger.rst
index 02f4c532dbf..9083e621cbc 100644
--- a/messenger.rst
+++ b/messenger.rst
@@ -71,10 +71,6 @@ message class (or a message interface)::
methods. You may use the attribute on as many methods in a single class as you
like, allowing you to group the handling of multiple related types of messages.
-.. versionadded:: 6.1
-
- Support for ``#[AsMessageHandler]`` on methods was introduced in Symfony 6.1.
-
Thanks to :ref:`autoconfiguration
` and the ``SmsNotification``
type-hint, Symfony knows that this handler should be called when an ``SmsNotification``
message is dispatched. Most of the time, this is all you need to do. But you can
@@ -207,8 +203,23 @@ Routing Messages to a Transport
Now that you have a transport configured, instead of handling a message immediately,
you can configure them to be sent to a transport:
+.. _messenger-message-attribute:
+
.. configuration-block::
+ .. code-block:: php-attributes
+
+ // src/Message/SmsNotification.php
+ namespace App\Message;
+
+ use Symfony\Component\Messenger\Attribute\AsMessage;
+
+ #[AsMessage('async')]
+ class SmsNotification
+ {
+ // ...
+ }
+
.. code-block:: yaml
# config/packages/messenger.yaml
@@ -255,15 +266,26 @@ you can configure them to be sent to a transport:
;
};
+.. versionadded:: 7.2
+
+ The ``#[AsMessage]`` attribute was introduced in Symfony 7.2.
+
Thanks to this, the ``App\Message\SmsNotification`` will be sent to the ``async``
transport and its handler(s) will *not* be called immediately. Any messages not
matched under ``routing`` will still be handled immediately, i.e. synchronously.
.. note::
- You may use a partial PHP namespace like ``'App\Message\*'`` to match all
- the messages within the matching namespace. The only requirement is that the
- ``'*'`` wildcard has to be placed at the end of the namespace.
+ If you configure routing with both YAML/XML/PHP configuration files and
+ PHP attributes, the configuration always takes precedence over the class
+ attribute. This behavior allows you to override routing on a per-environment basis.
+
+.. note::
+
+ When configuring the routing in separate YAML/XML/PHP files, you can use a partial
+ PHP namespace like ``'App\Message\*'`` to match all the messages within the
+ matching namespace. The only requirement is that the ``'*'`` wildcard has to
+ be placed at the end of the namespace.
You may use ``'*'`` as the message class. This will act as a default routing
rule for any message not matched under ``routing``. This is useful to ensure
@@ -279,6 +301,27 @@ to multiple transports:
.. configuration-block::
+ .. code-block:: php-attributes
+
+ // src/Message/SmsNotification.php
+ namespace App\Message;
+
+ use Symfony\Component\Messenger\Attribute\AsMessage;
+
+ #[AsMessage(['async', 'audit'])]
+ class SmsNotification
+ {
+ // ...
+ }
+
+ // if you prefer, you can also apply multiple attributes to the message class
+ #[AsMessage('async')]
+ #[AsMessage('audit')]
+ class SmsNotification
+ {
+ // ...
+ }
+
.. code-block:: yaml
# config/packages/messenger.yaml
@@ -349,11 +392,6 @@ to multiple transports:
name as its only argument. For more information about stamps, see
`Envelopes & Stamps`_.
-.. versionadded:: 6.2
-
- The :class:`Symfony\\Component\\Messenger\\Stamp\\TransportNamesStamp`
- stamp was introduced in Symfony 6.2.
-
Doctrine Entities in Messages
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -494,6 +532,42 @@ The first argument is the receiver's name (or service id if you routed to a
custom service). By default, the command will run forever: looking for new messages
on your transport and handling them. This command is called your "worker".
+If you want to consume messages from all available receivers, you can use the
+command with the ``--all`` option:
+
+.. code-block:: terminal
+
+ $ php bin/console messenger:consume --all
+
+.. versionadded:: 7.1
+
+ The ``--all`` option was introduced in Symfony 7.1.
+
+Messages that take a long time to process may be redelivered prematurely because
+some transports assume that an unacknowledged message is lost. To prevent this
+issue, use the ``--keepalive`` command option to specify an interval (in seconds;
+default value = ``5``) at which the message is marked as "in progress". This prevents
+the message from being redelivered until the worker completes processing it:
+
+.. code-block:: terminal
+
+ $ php bin/console messenger:consume --keepalive
+
+.. note::
+
+ This option is only available for the following transports: Beanstalkd, AmazonSQS, Doctrine and Redis.
+
+.. versionadded:: 7.2
+
+ The ``--keepalive`` option was introduced in Symfony 7.2.
+
+.. tip::
+
+ In a development environment and if you're using the Symfony CLI tool,
+ you can configure workers to be automatically run along with the webserver.
+ You can find more information in the
+ :ref:`Symfony CLI Workers ` documentation.
+
.. tip::
To properly stop a worker, throw an instance of
@@ -532,7 +606,7 @@ On production, there are a few important things to think about:
**Use the Same Cache Between Deploys**
If your deploy strategy involves the creation of new target directories, you
- should set a value for the :ref:`cache.prefix.seed `
+ should set a value for the :ref:`cache.prefix_seed `
configuration option in order to use the same cache namespace between deployments.
Otherwise, the ``cache.app`` pool will use the value of the ``kernel.project_dir``
parameter as base for the namespace, which will lead to different namespaces
@@ -564,7 +638,7 @@ different messages to them. For example:
# name: high
#queues:
# messages_high: ~
- # or redis try "group"
+ # for redis try "group"
async_priority_low:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
@@ -679,15 +753,19 @@ of some or all transports:
# shows stats only for some transports
$ php bin/console messenger:stats my_transport_name other_transport_name
+ # you can also output the stats in JSON format
+ $ php bin/console messenger:stats --format=json
+ $ php bin/console messenger:stats my_transport_name other_transport_name --format=json
+
+.. versionadded:: 7.2
+
+ The ``format`` option was introduced in Symfony 7.2.
+
.. note::
In order for this command to work, the configured transport's receiver must implement
:class:`Symfony\\Component\\Messenger\\Transport\\Receiver\\MessageCountAwareInterface`.
-.. versionadded:: 6.2
-
- The ``messenger:stats`` command was introduced in Symfony 6.2.
-
.. _messenger-supervisor:
Supervisor Configuration
@@ -723,7 +801,7 @@ times:
Change the ``async`` argument to use the name of your transport (or transports)
and ``user`` to the Unix user on your server.
-.. caution::
+.. warning::
During a deployment, something might be unavailable (e.g. the
database) causing the consumer to fail to start. In this situation,
@@ -740,7 +818,7 @@ If you use the Redis Transport, note that each worker needs a unique consumer
name to avoid the same message being handled by multiple workers. One way to
achieve this is to set an environment variable in the Supervisor configuration
file, which you can then refer to in ``messenger.yaml``
-(see the ref:`Redis section ` below):
+(see the :ref:`Redis section ` below):
.. code-block:: ini
@@ -771,11 +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
-.. versionadded:: 6.3
+ # config/packages/messenger.yaml
+ framework:
+ messenger:
+ stop_worker_on_signals:
+ - SIGTERM
+ - SIGINT
+ - SIGUSR1
- The ``framework.messenger.stop_worker_on_signals`` option was introduced in Symfony 6.3.
+ .. 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
@@ -807,6 +930,8 @@ directory. For example, you can create a new ``messenger-worker.service`` file.
[Service]
ExecStart=php /path/to/your/app/bin/console messenger:consume async --time-limit=3600
+ # for Redis, set a custom consumer name for each instance
+ Environment="MESSENGER_CONSUMER_NAME=symfony-%n-%i"
Restart=always
RestartSec=30
@@ -896,13 +1021,64 @@ properties in the ``reset()`` method.
If you don't want to reset the container, add the ``--no-reset`` option when
running the ``messenger:consume`` command.
-.. deprecated:: 6.1
+.. _messenger-retries-failures:
+
+Rate Limited Transport
+~~~~~~~~~~~~~~~~~~~~~~
- In Symfony versions previous to 6.1, the service container didn't reset
- automatically between messages and you had to set the
- ``framework.messenger.reset_on_message`` option to ``true``.
+Sometimes you might need to rate limit your message worker. You can configure a
+rate limiter on a transport (requires the :doc:`RateLimiter component `)
+by setting its ``rate_limiter`` option:
-.. _messenger-retries-failures:
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/messenger.yaml
+ framework:
+ messenger:
+ transports:
+ async:
+ rate_limiter: your_rate_limiter_name
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/messenger.php
+ use Symfony\Config\FrameworkConfig;
+
+ return static function (FrameworkConfig $framework) {
+ $framework->messenger()
+ ->transport('async')
+ ->options(['rate_limiter' => 'your_rate_limiter_name'])
+ ;
+ };
+
+.. warning::
+
+ When a rate limiter is configured on a transport, it will block the whole
+ worker when the limit is hit. You should make sure you configure a dedicated
+ worker for a rate limited transport to avoid other transports to be blocked.
Retries & Failures
------------------
@@ -934,6 +1110,9 @@ this is configurable for each transport:
# e.g. 1 second delay, 2 seconds, 4 seconds
multiplier: 2
max_delay: 0
+ # applies randomness to the delay that can prevent the thundering herd effect
+ # the value (between 0 and 1.0) is the percentage of 'delay' that will be added/subtracted
+ jitter: 0.1
# override all of this with a service that
# implements Symfony\Component\Messenger\Retry\RetryStrategyInterface
# service: null
@@ -953,7 +1132,7 @@ this is configurable for each transport:
-
+
@@ -978,12 +1157,19 @@ this is configurable for each transport:
// e.g. 1 second delay, 2 seconds, 4 seconds
->multiplier(2)
->maxDelay(0)
+ // applies randomness to the delay that can prevent the thundering herd effect
+ // the value (between 0 and 1.0) is the percentage of 'delay' that will be added/subtracted
+ ->jitter(0.1)
// override all of this with a service that
// implements Symfony\Component\Messenger\Retry\RetryStrategyInterface
->service(null)
;
};
+.. versionadded:: 7.1
+
+ The ``jitter`` option was introduced in Symfony 7.1.
+
.. tip::
Symfony triggers a :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageRetriedEvent`
@@ -995,10 +1181,6 @@ this is configurable for each transport:
the serialized form of the message is saved, which prevents to serialize it
again if the message is later retried.
- .. versionadded:: 6.1
-
- The ``SerializedMessageStamp`` class was introduced in Symfony 6.1.
-
Avoiding Retrying
~~~~~~~~~~~~~~~~~
@@ -1007,6 +1189,12 @@ and should not be retried. If you throw
:class:`Symfony\\Component\\Messenger\\Exception\\UnrecoverableMessageHandlingException`,
the message will not be retried.
+.. note::
+
+ Messages that will not be retried, will still show up in the configured failure transport.
+ If you want to avoid that, consider handling the error yourself and let the handler
+ successfully end.
+
Forcing Retrying
~~~~~~~~~~~~~~~~
@@ -1015,6 +1203,15 @@ and must be retried. If you throw
:class:`Symfony\\Component\\Messenger\\Exception\\RecoverableMessageHandlingException`,
the message will always be retried infinitely and ``max_retries`` setting will be ignored.
+You can define a custom retry delay (e.g., to use the value from the ``Retry-After``
+header in an HTTP response) by setting the ``retryDelay`` argument in the
+constructor of the ``RecoverableMessageHandlingException``.
+
+.. versionadded:: 7.2
+
+ The ``retryDelay`` argument and the ``getRetryDelay()`` method were introduced
+ in Symfony 7.2.
+
.. _messenger-failure-transport:
Saving & Retrying Failed Messages
@@ -1091,8 +1288,8 @@ to retry them:
# see the 10 first messages
$ php bin/console messenger:failed:show --max=10
- # see only MyClass messages
- $ php bin/console messenger:failed:show --class-filter='MyClass'
+ # see only App\Message\MyMessage messages
+ $ php bin/console messenger:failed:show --class-filter='App\Message\MyMessage'
# see the number of messages by message class
$ php bin/console messenger:failed:show --stats
@@ -1100,7 +1297,7 @@ to retry them:
# see details about a specific failure
$ php bin/console messenger:failed:show 20 -vv
- # view and retry messages one-by-one
+ # for each message, this command asks whether to retry, skip, or delete
$ php bin/console messenger:failed:retry -vv
# retry specific messages
@@ -1115,18 +1312,22 @@ to retry them:
# remove all messages in the failure transport
$ php bin/console messenger:failed:remove --all
-.. versionadded:: 6.2
+ # remove only App\Message\MyMessage messages
+ $ php bin/console messenger:failed:remove --class-filter='App\Message\MyMessage'
- The ``--class-filter`` and ``--stats`` options were introduced in Symfony 6.2.
+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.
-.. versionadded:: 6.4
+.. versionadded:: 7.2
- The ``--all`` option was introduced in Symfony 6.4.
+ The option to skip a message in the ``messenger:failed:retry`` command was
+ introduced in Symfony 7.2
+.. versionadded:: 7.3
-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.
+ The option to filter by a message class in the ``messenger:failed:remove`` command was
+ introduced in Symfony 7.3
Multiple Failed Transports
~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1339,69 +1540,115 @@ the exchange, queues binding keys and more. See the documentation on
The transport has a number of options:
-============================================ ================================================= ===================================
- Option Description Default
-============================================ ================================================= ===================================
-``auto_setup`` Whether the exchanges and queues should be ``true``
- created automatically during send / get.
-``cacert`` Path to the CA cert file in PEM format.
-``cert`` Path to the client certificate in PEM format.
-``channel_max`` Specifies highest channel number that the server
- permits. 0 means standard extension limit
-``confirm_timeout`` Timeout in seconds for confirmation; if none
- specified, transport will not wait for message
- confirmation. Note: 0 or greater seconds. May be
- fractional.
-``connect_timeout`` Connection timeout. Note: 0 or greater seconds.
- May be fractional.
-``frame_max`` The largest frame size that the server proposes
- for the connection, including frame header and
- end-byte. 0 means standard extension limit
- (depends on librabbimq default frame size limit)
-``heartbeat`` The delay, in seconds, of the connection
- heartbeat that the server wants. 0 means the
- server does not want a heartbeat. Note,
- librabbitmq has limited heartbeat support, which
- means heartbeats checked only during blocking
- calls.
-``host`` Hostname of the AMQP service
-``key`` Path to the client key in PEM format.
-``login`` Username to use to connect the AMQP service
-``password`` Password to use to connect to the AMQP service
-``persistent`` ``'false'``
-``port`` Port of the AMQP service
-``read_timeout`` Timeout in for income activity. Note: 0 or
- greater seconds. May be fractional.
+``auto_setup`` (default: ``true``)
+ Whether the exchanges and queues should be created automatically during
+ send / get.
+
+``cacert``
+ Path to the CA cert file in PEM format.
+
+``cert``
+ Path to the client certificate in PEM format.
+
+``channel_max``
+ Specifies highest channel number that the server permits. 0 means standard
+ extension limit
+
+``confirm_timeout``
+ Timeout in seconds for confirmation; if none specified, transport will not
+ wait for message confirmation. Note: 0 or greater seconds. May be
+ fractional.
+
+``connect_timeout``
+ Connection timeout. Note: 0 or greater seconds. May be fractional.
+
+``frame_max``
+ The largest frame size that the server proposes for the connection,
+ including frame header and end-byte. 0 means standard extension limit
+ (depends on librabbimq default frame size limit)
+
+``heartbeat``
+ The delay, in seconds, of the connection heartbeat that the server wants. 0
+ means the server does not want a heartbeat. Note, librabbitmq has limited
+ heartbeat support, which means heartbeats checked only during blocking
+ calls.
+
+``host``
+ Hostname of the AMQP service
+
+``key``
+ Path to the client key in PEM format.
+
+``login``
+ Username to use to connect the AMQP service
+
+``password``
+ Password to use to connect to the AMQP service
+
+``persistent`` (default: ``'false'``)
+ Whether the connection is persistent
+
+``port``
+ Port of the AMQP service
+
+``read_timeout``
+ Timeout in for income activity. Note: 0 or greater seconds. May be
+ fractional.
+
``retry``
+ (no description available)
+
``sasl_method``
-``connection_name`` For custom connection names (requires at least
- version 1.10 of the PHP AMQP extension)
-``verify`` Enable or disable peer verification. If peer
- verification is enabled then the common name in
- the server certificate must match the server
- name. Peer verification is enabled by default.
-``vhost`` Virtual Host to use with the AMQP service
-``write_timeout`` Timeout in for outcome activity. Note: 0 or
- greater seconds. May be fractional.
-``delay[queue_name_pattern]`` Pattern to use to create the queues ``delay_%exchange_name%_%routing_key%_%delay%``
-``delay[exchange_name]`` Name of the exchange to be used for the ``delays``
- delayed/retried messages
-``queues[name][arguments]`` Extra arguments
-``queues[name][binding_arguments]`` Arguments to be used while binding the queue.
-``queues[name][binding_keys]`` The binding keys (if any) to bind to this queue
-``queues[name][flags]`` Queue flags ``AMQP_DURABLE``
-``exchange[arguments]`` Extra arguments for the exchange (e.g.
- ``alternate-exchange``)
-``exchange[default_publish_routing_key]`` Routing key to use when publishing, if none is
- specified on the message
-``exchange[flags]`` Exchange flags ``AMQP_DURABLE``
-``exchange[name]`` Name of the exchange
-``exchange[type]`` Type of exchange ``fanout``
-============================================ ================================================= ===================================
-
-.. versionadded:: 6.1
-
- The ``connection_name`` option was introduced in Symfony 6.1.
+ (no description available)
+
+``connection_name``
+ For custom connection names (requires at least version 1.10 of the PHP AMQP
+ extension)
+
+``verify``
+ Enable or disable peer verification. If peer verification is enabled then
+ the common name in the server certificate must match the server name. Peer
+ verification is enabled by default.
+
+``vhost``
+ Virtual Host to use with the AMQP service
+
+``write_timeout``
+ Timeout in for outcome activity. Note: 0 or greater seconds. May be
+ fractional.
+
+``delay[queue_name_pattern]`` (default: ``delay_%exchange_name%_%routing_key%_%delay%``)
+ Pattern to use to create the queues
+
+``delay[exchange_name]`` (default: ``delays``)
+ Name of the exchange to be used for the delayed/retried messages
+
+``queues[name][arguments]``
+ Extra arguments
+
+``queues[name][binding_arguments]``
+ Arguments to be used while binding the queue.
+
+``queues[name][binding_keys]``
+ The binding keys (if any) to bind to this queue
+
+``queues[name][flags]`` (default: ``AMQP_DURABLE``)
+ Queue flags
+
+``exchange[arguments]``
+ Extra arguments for the exchange (e.g. ``alternate-exchange``)
+
+``exchange[default_publish_routing_key]``
+ Routing key to use when publishing, if none is specified on the message
+
+``exchange[flags]`` (default: ``AMQP_DURABLE``)
+ Exchange flags
+
+``exchange[name]``
+ Name of the exchange
+
+``exchange[type]`` (default: ``fanout``)
+ Type of exchange
You can also configure AMQP-specific settings on your message by adding
:class:`Symfony\\Component\\Messenger\\Bridge\\Amqp\\Transport\\AmqpStamp` to
@@ -1415,7 +1662,7 @@ your Envelope::
new AmqpStamp('custom-routing-key', AMQP_NOPARAM, $attributes),
]);
-.. caution::
+.. warning::
The consumers do not show up in an admin panel as this transport does not rely on
``\AmqpQueue::consume()`` which is blocking. Having a blocking receiver makes
@@ -1444,7 +1691,7 @@ Install it by running:
$ composer require symfony/doctrine-messenger
-The Doctrine transport DSN may looks like this:
+The Doctrine transport DSN may look like this:
.. code-block:: env
@@ -1466,36 +1713,28 @@ DSN by using the ``table_name`` option:
Or, to create the table yourself, set the ``auto_setup`` option to ``false`` and
:ref:`generate a migration `.
-.. caution::
+The transport has a number of options:
- The datetime property of the messages stored in the database uses the
- timezone of the current system. This may cause issues if multiple machines
- with different timezone configuration use the same storage.
+``table_name`` (default: ``messenger_messages``)
+ Name of the table
-The transport has a number of options:
+``queue_name`` (default: ``default``)
+ Name of the queue (a column in the table, to use one table for multiple
+ transports)
-================== ===================================== ======================
-Option Description Default
-================== ===================================== ======================
-table_name Name of the table messenger_messages
-queue_name Name of the queue (a column in the default
- table, to use one table for
- multiple transports)
-redeliver_timeout Timeout before retrying a message 3600
- that's in the queue but in the
- "handling" state (if a worker stopped
- for some reason, this will occur,
- eventually you should retry the
- message) - in seconds.
-auto_setup Whether the table should be created
- automatically during send / get. true
-================== ===================================== ======================
+``redeliver_timeout`` (default: ``3600``)
+ Timeout before retrying a message that's in the queue but in the "handling"
+ state (if a worker stopped for some reason, this will occur, eventually you
+ should retry the message) - in seconds.
-.. note::
+ .. note::
+
+ Set ``redeliver_timeout`` to a greater value than your slowest message
+ duration. Otherwise, some messages will start a second time while the
+ first one is still being handled.
- Set ``redeliver_timeout`` to a greater value than your slowest message
- duration. Otherwise, some messages will start a second time while the
- first one is still being handled.
+``auto_setup``
+ Whether the table should be created automatically during send / get.
When using PostgreSQL, you have access to the following options to leverage
the `LISTEN/NOTIFY`_ feature. This allow for a more performant approach
@@ -1503,17 +1742,23 @@ than the default polling behavior of the Doctrine transport because
PostgreSQL will directly notify the workers when a new message is inserted
in the table.
-======================= ========================================== ======================
-Option Description Default
-======================= ========================================== ======================
-use_notify Whether to use LISTEN/NOTIFY. true
-check_delayed_interval The interval to check for delayed 60000
- messages, in milliseconds.
- Set to 0 to disable checks.
-get_notify_timeout The length of time to wait for a 0
- response when calling
- ``PDO::pgsqlGetNotify``, in milliseconds.
-======================= ========================================== ======================
+``use_notify`` (default: ``true``)
+ Whether to use LISTEN/NOTIFY.
+
+``check_delayed_interval`` (default: ``60000``)
+ The interval to check for delayed messages, in milliseconds. Set to 0 to
+ disable checks.
+
+``get_notify_timeout`` (default: ``0``)
+ 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
~~~~~~~~~~~~~~~~~~~~
@@ -1537,20 +1782,48 @@ The Beanstalkd transport DSN may looks like this:
The transport has a number of options:
-================== =================================== ======================
- Option Description Default
-================== =================================== ======================
-tube_name Name of the queue default
-timeout Message reservation timeout 0 (will cause the
- - in seconds. server to immediately
- return either a
- response or a
- TransportException
- will be thrown)
-ttr The message time to run before it
- is put back in the ready queue
- - in seconds. 90
-================== =================================== ======================
+``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
+ immediately return either a response or a TransportException will be thrown.
+
+``ttr`` (default: ``90``)
+ 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``.
+
+.. versionadded:: 7.2
+
+ 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:
@@ -1577,64 +1850,110 @@ The Redis transport DSN may looks like this:
MESSENGER_TRANSPORT_DSN=redis://host-01:6379,redis://host-02:6379,redis://host-03:6379,redis://host-04:6379
# Unix Socket Example
MESSENGER_TRANSPORT_DSN=redis:///var/run/redis.sock
+ # TLS Example
+ MESSENGER_TRANSPORT_DSN=rediss://localhost:6379/messages
# Multiple Redis Sentinel Hosts Example
MESSENGER_TRANSPORT_DSN=redis:?host[redis1:26379]&host[redis2:26379]&host[redis3:26379]&sentinel_master=db
A number of options can be configured via the DSN or via the ``options`` key
under the transport in ``messenger.yaml``:
-======================= ===================================== =================================
-Option Description Default
-======================= ===================================== =================================
-stream The Redis stream name messages
-group The Redis consumer group name symfony
-consumer Consumer name used in Redis consumer
-auto_setup Create the Redis group automatically? true
-auth The Redis password
-delete_after_ack If ``true``, messages are deleted true
- automatically after processing them
-delete_after_reject If ``true``, messages are deleted true
- automatically if they are rejected
-lazy Connect only when a connection is false
- really needed
-serializer How to serialize the final payload ``Redis::SERIALIZER_PHP``
- in Redis (the
- ``Redis::OPT_SERIALIZER`` option)
-stream_max_entries The maximum number of entries which ``0`` (which means "no trimming")
- the stream will be trimmed to. Set
- it to a large enough number to
- avoid losing pending messages
-tls Enable TLS support for the connection false
-redeliver_timeout Timeout before retrying a pending ``3600``
- message which is owned by an
- abandoned consumer (if a worker died
- for some reason, this will occur,
- eventually you should retry the
- message) - in seconds.
-claim_interval Interval on which pending/abandoned ``60000`` (1 Minute)
- messages should be checked for to
- claim - in milliseconds
-persistent_id String, if null connection is null
- non-persistent.
-retry_interval Int, value in milliseconds ``0``
-read_timeout Float, value in seconds ``0``
- default indicates unlimited
-timeout Connection timeout. Float, value in ``0``
- seconds default indicates unlimited
-sentinel_master String, if null or empty Sentinel null
- support is disabled
-======================= ===================================== =================================
-
-.. versionadded:: 6.1
-
- The ``persistent_id``, ``retry_interval``, ``read_timeout``, ``timeout``, and
- ``sentinel_master`` options were introduced in Symfony 6.1.
-
-.. versionadded:: 6.4
-
- Support for the multiple Redis Sentinel hosts DNS was introduced in Symfony 6.4.
-
-.. caution::
+``stream`` (default: ``messages``)
+ The Redis stream name
+
+``group`` (default: ``symfony``)
+ The Redis consumer group name
+
+``consumer`` (default: ``consumer``)
+ Consumer name used in Redis. Allows setting an explicit consumer name identifier.
+ Recommended in environments with multiple workers to prevent duplicate message
+ processing. Typically set via an environment variable:
+
+ .. code-block:: yaml
+
+ # config/packages/messenger.yaml
+ framework:
+ messenger:
+ transports:
+ redis:
+ dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
+ options:
+ consumer: '%env(MESSENGER_CONSUMER_NAME)%'
+
+``auto_setup`` (default: ``true``)
+ Whether to create the Redis group automatically
+
+``auth``
+ The Redis password
+
+``delete_after_ack`` (default: ``true``)
+ If ``true``, messages are deleted automatically after processing them
+
+``delete_after_reject`` (default: ``true``)
+ If ``true``, messages are deleted automatically if they are rejected
+
+``lazy`` (default: ``false``)
+ Connect only when a connection is really needed
+
+``serializer`` (default: ``Redis::SERIALIZER_PHP``)
+ How to serialize the final payload in Redis (the ``Redis::OPT_SERIALIZER`` option)
+
+``stream_max_entries`` (default: ``0``)
+ The maximum number of entries which the stream will be trimmed to. Set it to
+ a large enough number to avoid losing pending messages
+
+``redeliver_timeout`` (default: ``3600``)
+ Timeout (in seconds) before retrying a pending message which is owned by an abandoned consumer
+ (if a worker died for some reason, this will occur, eventually you should retry the message).
+
+``claim_interval`` (default: ``60000``)
+ Interval on which pending/abandoned messages should be checked for to claim - in milliseconds
+
+``persistent_id`` (default: ``null``)
+ String, if null connection is non-persistent.
+
+``retry_interval`` (default: ``0``)
+ Int, value in milliseconds
+
+``read_timeout`` (default: ``0``)
+ Float, value in seconds default indicates unlimited
+
+``timeout`` (default: ``0``)
+ Connection timeout. Float, value in seconds default indicates unlimited
+
+``sentinel_master`` (default: ``null``)
+ String, if null or empty Sentinel support is disabled
+
+``redis_sentinel`` (default: ``null``)
+ An alias of the ``sentinel_master`` option
+
+ .. versionadded:: 7.1
+
+ The ``redis_sentinel`` option was introduced in Symfony 7.1.
+
+``ssl`` (default: ``null``)
+ Map of `SSL context options`_ for the TLS channel. This is useful for example
+ to change the requirements for the TLS channel in tests:
+
+ .. code-block:: yaml
+
+ # config/packages/test/messenger.yaml
+ framework:
+ messenger:
+ transports:
+ redis:
+ dsn: "rediss://localhost"
+ options:
+ ssl:
+ allow_self_signed: true
+ capture_peer_cert: true
+ capture_peer_cert_chain: true
+ disable_compression: true
+ SNI_enabled: true
+ verify_peer: true
+ verify_peer_name: true
+
+.. warning::
There should never be more than one ``messenger:consume`` command running with the same
combination of ``stream``, ``group`` and ``consumer``, or messages could end up being
@@ -1652,6 +1971,13 @@ sentinel_master String, if null or empty Sentinel null
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
~~~~~~~~~~~~~~~~~~~
@@ -1720,18 +2046,12 @@ during a request::
$this->assertSame(200, $client->getResponse()->getStatusCode());
- /* @var InMemoryTransport $transport */
+ /** @var InMemoryTransport $transport */
$transport = $this->getContainer()->get('messenger.transport.async_priority_normal');
$this->assertCount(1, $transport->getSent());
}
}
-.. versionadded:: 6.3
-
- The namespace of the ``InMemoryTransport`` class changed in Symfony 6.3 from
- ``Symfony\Component\Messenger\Transport\InMemoryTransport`` to
- ``Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport``.
-
The transport has a number of options:
``serialize`` (boolean, default: ``false``)
@@ -1776,31 +2096,54 @@ The SQS transport DSN may looks like this:
The transport has a number of options:
-====================== ====================================== ===================================
- Option Description Default
-====================== ====================================== ===================================
-``access_key`` AWS access key must be urlencoded
-``account`` Identifier of the AWS account The owner of the credentials
-``auto_setup`` Whether the queue should be created ``true``
- automatically during send / get.
-``buffer_size`` Number of messages to prefetch 9
-``debug`` If ``true`` it logs all HTTP requests ``false``
- and responses (it impacts performance)
-``endpoint`` Absolute URL to the SQS service https://sqs.eu-west-1.amazonaws.com
-``poll_timeout`` Wait for new message duration in 0.1
- seconds
-``queue_name`` Name of the queue messages
-``region`` Name of the AWS region eu-west-1
-``secret_key`` AWS secret key must be urlencoded
-``session_token`` AWS session token
-``visibility_timeout`` Amount of seconds the message will Queue's configuration
- not be visible (`Visibility Timeout`_)
-``wait_time`` `Long polling`_ duration in seconds 20
-====================== ====================================== ===================================
-
-.. versionadded:: 6.1
-
- The ``session_token`` option was introduced in Symfony 6.1.
+``access_key``
+ AWS access key (must be urlencoded)
+
+``account`` (default: The owner of the credentials)
+ Identifier of the AWS account
+
+``auto_setup`` (default: ``true``)
+ Whether the queue should be created automatically during send / get.
+
+``buffer_size`` (default: ``9``)
+ Number of messages to prefetch
+
+``debug`` (default: ``false``)
+ If ``true`` it logs all HTTP requests and responses (it impacts performance)
+
+``endpoint`` (default: ``https://sqs.eu-west-1.amazonaws.com``)
+ Absolute URL to the SQS service
+
+``poll_timeout`` (default: ``0.1``)
+ Wait for new message duration in seconds
+
+``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
+
+``secret_key``
+ AWS secret key (must be urlencoded)
+
+``session_token``
+ AWS session token
+
+``visibility_timeout`` (default: Queue's configuration)
+ Amount of seconds the message will not be visible (`Visibility Timeout`_)
+
+``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::
@@ -1831,17 +2174,16 @@ The transport has a number of options:
You can learn more about middlewares in
:ref:`the dedicated section `.
- .. versionadded:: 6.4
-
- The
- :class:`Symfony\\Component\\Messenger\\Bridge\\AmazonSqs\\Middleware\\AddFifoStampMiddleware`,
- :class:`Symfony\\Component\\Messenger\\Bridge\\AmazonSqs\\MessageDeduplicationAwareInterface`
- and :class:`Symfony\\Component\\Messenger\\Bridge\\AmazonSqs\\MessageGroupAwareInterface`
- were introduced in Symfony 6.4.
-
FIFO queues don't support setting a delay per message, a value of ``delay: 0``
is required in the retry strategy settings.
+The SQS transport supports the ``--keepalive`` option by using the ``ChangeMessageVisibility``
+action to periodically update the ``VisibilityTimeout`` of the message.
+
+.. versionadded:: 7.2
+
+ Keepalive support was introduced in Symfony 7.2.
+
Serializing Messages
~~~~~~~~~~~~~~~~~~~~
@@ -1925,6 +2267,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
---------------------------------------
@@ -1966,12 +2324,6 @@ contains many useful information such as the exit code or the output of the
process. You can refer to the page dedicated on
:ref:`handler results ` for more information.
-.. versionadded:: 6.4
-
- The :class:`Symfony\\Component\\Console\\Messenger\\RunCommandMessage`
- and :class:`Symfony\\Component\\Console\\Messenger\\RunCommandContext`
- classes were introduced in Symfony 6.4.
-
Trigger An External Process
~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1986,8 +2338,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
@@ -1998,18 +2351,40 @@ 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
+factory :method:Symfony\\Component\\Process\\Messenger\\RunProcessMessage::fromShellCommandline::
+
+ 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
process. You can refer to the page dedicated on
:ref:`handler results ` for more information.
-.. versionadded:: 6.4
-
- The :class:`Symfony\\Component\\Process\\Messenger\\RunProcessMessage`
- and :class:`Symfony\\Component\\Process\\Messenger\\RunProcessContext`
- classes were introduced in Symfony 6.4.
-
Pinging A Webservice
--------------------
@@ -2017,7 +2392,7 @@ Sometimes, you may need to regularly ping a webservice to get its status, e.g.
is it up or down. It is possible to do so by dispatching a
:class:`Symfony\\Component\\HttpClient\\Messenger\\PingWebhookMessage`::
- use Symfony\Component\HttpClient\Messenger\RPingWebhookMessage;
+ use Symfony\Component\HttpClient\Messenger\PingWebhookMessage;
use Symfony\Component\Messenger\MessageBusInterface;
class LivenessService
@@ -2029,10 +2404,10 @@ is it up or down. It is possible to do so by dispatching a
public function ping(): void
{
// An HttpExceptionInterface is thrown on 3xx/4xx/5xx
- $this->bus->dispatch(new PingWebhookMessage('GET', 'https://example.com/status');
+ $this->bus->dispatch(new PingWebhookMessage('GET', 'https://example.com/status'));
// Ping, but does not throw on 3xx/4xx/5xx
- $this->bus->dispatch(new PingWebhookMessage('GET', 'https://example.com/status', throw: false);
+ $this->bus->dispatch(new PingWebhookMessage('GET', 'https://example.com/status', throw: false));
// Any valid HttpClientInterface option can be used
$this->bus->dispatch(new PingWebhookMessage('POST', 'https://example.com/status', [
@@ -2050,11 +2425,6 @@ The handler will return a
:class:`Symfony\\Contracts\\HttpClient\\ResponseInterface`, allowing you to
gather and process information returned by the HTTP request.
-.. versionadded:: 6.4
-
- The :class:`Symfony\\Component\\HttpClient\\Messenger\\PingWebhookMessage`
- class was introduced in Symfony 6.4.
-
Getting Results from your Handlers
----------------------------------
@@ -2155,42 +2525,17 @@ wherever you need a query bus behavior instead of the ``MessageBusInterface``::
}
}
-Customizing Handlers
---------------------
-
-Configuring Handlers Using Attributes
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-You can configure your handler by passing options to the attribute::
+You can also add new stamps when handling a message; they will be appended
+to the existing ones::
- // src/MessageHandler/SmsNotificationHandler.php
- namespace App\MessageHandler;
-
- use App\Message\OtherSmsNotification;
- use App\Message\SmsNotification;
- use Symfony\Component\Messenger\Attribute\AsMessageHandler;
+ $this->handle(new SomeMessage($data), [new SomeStamp(), new AnotherStamp()]);
- #[AsMessageHandler(fromTransport: 'async', priority: 10)]
- class SmsNotificationHandler
- {
- public function __invoke(SmsNotification $message): void
- {
- // ...
- }
- }
+.. versionadded:: 7.3
-Possible options to configure with the attribute are:
+ The ``$stamps`` parameter of the ``handle()`` method was introduced in Symfony 7.3.
-============================== ====================================================================================================
-Option Description
-============================== ====================================================================================================
-``bus`` Name of the bus from which the handler can receive messages, by default all buses.
-``fromTransport`` Name of the transport from which the handler can receive messages, by default all transports.
-``handles`` Type of messages (FQCN) that can be processed by the handler, only needed if can't be guessed by
- type-hint.
-``method`` Name of the method that will process the message, only if the target is a class.
-``priority`` Priority of the handler when multiple handlers can process the same message.
-============================== ====================================================================================================
+Customizing Handlers
+--------------------
.. _messenger-handler-config:
@@ -2199,10 +2544,29 @@ Manually Configuring Handlers
Symfony will normally :ref:`find and register your handler automatically `.
But, you can also configure a handler manually - and pass it some extra config -
-by tagging the handler service with ``messenger.message_handler``
+while using ``#AsMessageHandler`` attribute or tagging the handler service
+with ``messenger.message_handler``.
.. configuration-block::
+ .. code-block:: php-attributes
+
+ // src/MessageHandler/SmsNotificationHandler.php
+ namespace App\MessageHandler;
+
+ use App\Message\OtherSmsNotification;
+ use App\Message\SmsNotification;
+ use Symfony\Component\Messenger\Attribute\AsMessageHandler;
+
+ #[AsMessageHandler(fromTransport: 'async', priority: 10)]
+ class SmsNotificationHandler
+ {
+ public function __invoke(SmsNotification $message): void
+ {
+ // ...
+ }
+ }
+
.. code-block:: yaml
# config/services.yaml
@@ -2249,16 +2613,22 @@ by tagging the handler service with ``messenger.message_handler``
Possible options to configure with tags are:
-============================ ====================================================================================================
-Option Description
-============================ ====================================================================================================
-``bus`` Name of the bus from which the handler can receive messages, by default all buses.
-``from_transport`` Name of the transport from which the handler can receive messages, by default all transports.
-``handles`` Type of messages (FQCN) that can be processed by the handler, only needed if can't be guessed by
- type-hint.
-``method`` Name of the method that will process the message.
-``priority`` Priority of the handler when multiple handlers can process the same message.
-============================ ====================================================================================================
+``bus``
+ Name of the bus from which the handler can receive messages, by default all buses.
+
+``from_transport``
+ Name of the transport from which the handler can receive messages, by default
+ all transports.
+
+``handles``
+ Type of messages (FQCN) that can be processed by the handler, only needed if
+ can't be guessed by type-hint.
+
+``method``
+ Name of the method that will process the message.
+
+``priority``
+ Priority of the handler when multiple handlers can process the same message.
.. _handler-subscriber-options:
@@ -2289,12 +2659,6 @@ A single handler class can handle multiple messages. For that add the
}
}
-.. deprecated:: 6.2
-
- Implementing the :class:`Symfony\\Component\\Messenger\\Handler\\MessageSubscriberInterface`
- is another way to handle multiple messages with one handler class. This
- interface was deprecated in Symfony 6.2.
-
.. _messenger-transactional-messages:
Transactional Messages: Handle New Messages After Handling is Done
@@ -2390,7 +2754,7 @@ using the ``DispatchAfterCurrentBusMiddleware`` and adding a
{
public function __construct(
private MailerInterface $mailer,
- EntityManagerInterface $em,
+ private EntityManagerInterface $em,
) {
}
@@ -2413,7 +2777,7 @@ will not be rolled back.
If ``WhenUserRegisteredThenSendWelcomeEmail`` throws an exception, that
exception will be wrapped into a ``DelayedMessageHandlingException``. Using
- ``DelayedMessageHandlingException::getExceptions`` will give you all
+ ``DelayedMessageHandlingException::getWrappedExceptions`` will give you all
exceptions that are thrown while handling a message with the
``DispatchAfterCurrentBusStamp``.
@@ -2533,7 +2897,7 @@ That's it! You can now consume each transport:
$ php bin/console messenger:consume async_priority_normal -vv
-.. caution::
+.. warning::
If a handler does *not* have ``from_transport`` config, it will be executed
on *every* transport that the message is received from.
@@ -2557,7 +2921,7 @@ provided in order to ease the declaration of these special handlers::
{
use BatchHandlerTrait;
- public function __invoke(MyMessage $message, Acknowledger $ack = null): mixed
+ public function __invoke(MyMessage $message, ?Acknowledger $ack = null): mixed
{
return $this->handle($message, $ack);
}
@@ -2576,26 +2940,14 @@ provided in order to ease the declaration of these special handlers::
}
}
- // Optionally, you can either redefine the `shouldFlush()` method
- // of the trait to define your own batch size...
- private function shouldFlush(): bool
- {
- return 100 <= \count($this->jobs);
- }
-
- // ... or redefine the `getBatchSize()` method if the default
- // flush behavior suits your needs
+ // Optionally, you can override some of the trait methods, such as the
+ // `getBatchSize()` method, to specify your own batch size...
private function getBatchSize(): int
{
return 100;
}
}
-.. versionadded:: 6.3
-
- The :method:`Symfony\\Component\\Messenger\\Handler\\BatchHandlerTrait::getBatchSize`
- method was introduced in Symfony 6.3.
-
.. note::
When the ``$ack`` argument of ``__invoke()`` is ``null``, the message is
@@ -2626,8 +2978,8 @@ to your message::
public function index(MessageBusInterface $bus): void
{
+ // wait 5 seconds before processing
$bus->dispatch(new SmsNotification('...'), [
- // wait 5 seconds before processing
new DelayStamp(5000),
]);
@@ -2705,7 +3057,6 @@ and a different instance will be created per bus.
- 'App\Middleware\MyMiddleware'
- 'App\Middleware\AnotherMiddleware'
-
.. code-block:: xml
@@ -2755,6 +3106,11 @@ and a different instance will be created per bus.
$bus->middleware()->id('App\Middleware\AnotherMiddleware');
};
+.. tip::
+
+ If you have installed the MakerBundle, you can use the ``make:messenger-middleware``
+ command to bootstrap the creation of your own messenger middleware.
+
.. _middleware-doctrine:
Middleware for Doctrine
@@ -2923,10 +3279,6 @@ of the process. For each, the event class is the event name:
* :class:`Symfony\\Component\\Messenger\\Event\\WorkerStartedEvent`
* :class:`Symfony\\Component\\Messenger\\Event\\WorkerStoppedEvent`
-.. versionadded:: 6.2
-
- The ``WorkerRateLimitedEvent`` was introduced in Symfony 6.2.
-
Additional Handler Arguments
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2975,11 +3327,6 @@ Then your handler will look like this::
}
}
-.. versionadded:: 6.2
-
- The :class:`Symfony\\Component\\Messenger\\Stamp\\HandlerArgumentsStamp`
- was introduced in Symfony 6.2.
-
Message Serializer For Custom Data Formats
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2999,12 +3346,10 @@ Let's say you want to create a message decoder::
{
public function decode(array $encodedEnvelope): Envelope
{
- $envelope = \json_decode($encodedEnvelope, true);
-
try {
// parse the data you received with your custom fields
- $data = $envelope['data'];
- $data['token'] = $envelope['token'];
+ $data = $encodedEnvelope['data'];
+ $data['token'] = $encodedEnvelope['token'];
// other operations like getting information from stamps
} catch (\Throwable $throwable) {
@@ -3186,10 +3531,6 @@ an **event bus**. The event bus could have zero or more subscribers.
$eventBus->middleware()->id('validation');
};
-.. versionadded:: 6.2
-
- The ``allow_no_senders`` option was introduced in Symfony 6.2.
-
This will create three new services:
* ``command.bus``: autowireable with the :class:`Symfony\\Component\\Messenger\\MessageBusInterface`
@@ -3214,9 +3555,6 @@ you can restrict each handler to a specific bus using the ``messenger.message_ha
services:
App\MessageHandler\SomeCommandHandler:
tags: [{ name: messenger.message_handler, bus: command.bus }]
- # prevent handlers from being registered twice (or you can remove
- # the MessageHandlerInterface that autoconfigure uses to find handlers)
- autoconfigure: false
.. code-block:: xml
@@ -3363,7 +3701,7 @@ You can also restrict the list to a specific bus by providing its name as an arg
Redispatching a Message
-----------------------
-It you want to redispatch a message (using the same transport and envelope), create
+If you want to redispatch a message (using the same transport and envelope), create
a new :class:`Symfony\\Component\\Messenger\\Message\\RedispatchMessage` and dispatch
it through your bus. Reusing the same ``SmsNotification`` example shown earlier::
@@ -3398,12 +3736,6 @@ will take care of this message to redispatch it through the same bus it was
dispatched at first. You can also use the second argument of the ``RedispatchMessage``
constructor to provide transports to use when redispatching the message.
-.. versionadded:: 6.3
-
- The :class:`Symfony\\Component\\Messenger\\Message\\RedispatchMessage`
- and :class:`Symfony\\Component\\Messenger\\Handler\\RedispatchMessageHandler`
- classes were introduced in Symfony 6.3.
-
Learn more
----------
@@ -3417,7 +3749,7 @@ Learn more
.. _`streams`: https://redis.io/topics/streams-intro
.. _`Supervisor docs`: http://supervisord.org/
.. _`PCNTL`: https://www.php.net/manual/book.pcntl.php
-.. _`systemd docs`: https://www.freedesktop.org/wiki/Software/systemd/
+.. _`systemd docs`: https://systemd.io/
.. _`SymfonyCasts' message serializer tutorial`: https://symfonycasts.com/screencast/messenger/transport-serializer
.. _`Long polling`: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-short-and-long-polling.html
.. _`Visibility Timeout`: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html
@@ -3426,3 +3758,6 @@ Learn more
.. _`AMQProxy`: https://github.com/cloudamqp/amqproxy
.. _`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/messenger/custom-transport.rst b/messenger/custom-transport.rst
index d9cc9fa1abf..7d1698126d1 100644
--- a/messenger/custom-transport.rst
+++ b/messenger/custom-transport.rst
@@ -51,7 +51,7 @@ Here is a simplified example of a database transport::
*/
public function __construct(
private FakeDatabase $db,
- SerializerInterface $serializer = null,
+ ?SerializerInterface $serializer = null,
) {
$this->serializer = $serializer ?? new PhpSerializer();
}
diff --git a/migration.rst b/migration.rst
index 16fa43fa281..44485248545 100644
--- a/migration.rst
+++ b/migration.rst
@@ -340,7 +340,6 @@ somewhat like this::
throw new \Exception("Unhandled legacy mapping for $requestPathInfo");
}
-
public static function handleRequest(Request $request, Response $response, string $publicDirectory): void
{
$legacyScriptFilename = LegacyBridge::getLegacyScript($request);
diff --git a/notifier.rst b/notifier.rst
index ba6aaffdbfd..49a1c2d533b 100644
--- a/notifier.rst
+++ b/notifier.rst
@@ -15,13 +15,15 @@ Get the Notifier installed using:
$ composer require symfony/notifier
.. _channels-chatters-texters-email-and-browser:
+.. _channels-chatters-texters-email-browser-and-push:
-Channels: Chatters, Texters, Email, Browser and Push
-----------------------------------------------------
+Channels
+--------
-The notifier component can send notifications to different channels. Each
-channel can integrate with different providers (e.g. Slack or Twilio SMS)
-by using transports.
+Channels refer to the different mediums through which notifications can be delivered.
+These channels include email, SMS, chat services, push notifications, etc. Each
+channel can integrate with different providers (e.g. Slack or Twilio SMS) by
+using transports.
The notifier component supports the following channels:
@@ -31,102 +33,221 @@ The notifier component supports the following channels:
services like Slack and Telegram;
* :ref:`Email channel ` integrates the :doc:`Symfony Mailer `;
* Browser channel uses :ref:`flash messages `.
-* :ref:`Push channel ` sends notifications to phones and browsers via push notifications.
+* :ref:`Push channel ` sends notifications to phones and
+ browsers via push notifications.
+* :ref:`Desktop channel ` displays desktop notifications
+ on the same host machine.
-.. tip::
+.. versionadded:: 7.2
- Use :doc:`secrets ` to securely store your
- API tokens.
+ The ``Desktop`` channel was introduced in Symfony 7.2.
.. _notifier-sms-channel:
SMS Channel
~~~~~~~~~~~
-.. caution::
+The SMS channel uses :class:`Symfony\\Component\\Notifier\\Texter` classes
+to send SMS messages to mobile phones. This feature requires subscribing to
+a third-party service that sends SMS messages. Symfony provides integration
+with a couple popular SMS services:
+
+.. warning::
If any of the DSN values contains any character considered special in a
- URI (such as ``+``, ``@``, ``$``, ``#``, ``/``, ``:``, ``*``, ``!``), you must
+ URI (such as ``: / ? # [ ] @ ! $ & ' ( ) * + , ; =``), you must
encode them. See `RFC 3986`_ for the full list of reserved characters or use the
:phpfunction:`urlencode` function to encode them.
-The SMS channel uses :class:`Symfony\\Component\\Notifier\\Texter` classes
-to send SMS messages to mobile phones. This feature requires subscribing to
-a third-party service that sends SMS messages. Symfony provides integration
-with a couple popular SMS services:
+================== ====================================================================================================================================
+Service
+================== ====================================================================================================================================
+`46elks`_ **Install**: ``composer require symfony/forty-six-elks-notifier`` \
+ **DSN**: ``forty-six-elks://API_USERNAME:API_PASSWORD@default?from=FROM`` \
+ **Webhook support**: No
+`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
+`Bandwidth`_ **Install**: ``composer require symfony/bandwidth-notifier`` \
+ **DSN**: ``bandwidth://USERNAME:PASSWORD@default?from=FROM&account_id=ACCOUNT_ID&application_id=APPLICATION_ID&priority=PRIORITY`` \
+ **Webhook support**: No
+`Brevo`_ **Install**: ``composer require symfony/brevo-notifier`` \
+ **DSN**: ``brevo://API_KEY@default?sender=SENDER`` \
+ **Webhook support**: Yes
+`Clickatell`_ **Install**: ``composer require symfony/clickatell-notifier`` \
+ **DSN**: ``clickatell://ACCESS_TOKEN@default?from=FROM`` \
+ **Webhook support**: No
+`ContactEveryone`_ **Install**: ``composer require symfony/contact-everyone-notifier`` \
+ **DSN**: ``contact-everyone://TOKEN@default?&diffusionname=DIFFUSION_NAME&category=CATEGORY`` \
+ **Webhook support**: No
+`Esendex`_ **Install**: ``composer require symfony/esendex-notifier`` \
+ **DSN**: ``esendex://USER_NAME:PASSWORD@default?accountreference=ACCOUNT_REFERENCE&from=FROM`` \
+ **Webhook support**: No
+`FakeSms`_ **Install**: ``composer require symfony/fake-sms-notifier`` \
+ **DSN**: ``fakesms+email://MAILER_SERVICE_ID?to=TO&from=FROM`` or ``fakesms+logger://default`` \
+ **Webhook support**: No
+`FreeMobile`_ **Install**: ``composer require symfony/free-mobile-notifier`` \
+ **DSN**: ``freemobile://LOGIN:API_KEY@default?phone=PHONE`` \
+ **Webhook support**: No
+`GatewayApi`_ **Install**: ``composer require symfony/gateway-api-notifier`` \
+ **DSN**: ``gatewayapi://TOKEN@default?from=FROM`` \
+ **Webhook support**: No
+`GoIP`_ **Install**: ``composer require symfony/go-ip-notifier`` \
+ **DSN**: ``goip://USERNAME:PASSWORD@HOST:80?sim_slot=SIM_SLOT`` \
+ **Webhook support**: No
+`Infobip`_ **Install**: ``composer require symfony/infobip-notifier`` \
+ **DSN**: ``infobip://AUTH_TOKEN@HOST?from=FROM`` \
+ **Webhook support**: No
+`Iqsms`_ **Install**: ``composer require symfony/iqsms-notifier`` \
+ **DSN**: ``iqsms://LOGIN:PASSWORD@default?from=FROM`` \
+ **Webhook support**: No
+`iSendPro`_ **Install**: ``composer require symfony/isendpro-notifier`` \
+ **DSN**: ``isendpro://ACCOUNT_KEY_ID@default?from=FROM&no_stop=NO_STOP&sandbox=SANDBOX`` \
+ **Webhook support**: No
+`KazInfoTeh`_ **Install**: ``composer require symfony/kaz-info-teh-notifier`` \
+ **DSN**: ``kaz-info-teh://USERNAME:PASSWORD@default?sender=FROM`` \
+ **Webhook support**: No
+`LightSms`_ **Install**: ``composer require symfony/light-sms-notifier`` \
+ **DSN**: ``lightsms://LOGIN:TOKEN@default?from=PHONE`` \
+ **Webhook support**: No
+`LOX24`_ **Install**: ``composer require symfony/lox24-notifier`` \
+ **DSN**: ``lox24://USER:TOKEN@default?from=FROM`` \
+ **Webhook support**: No
+`Mailjet`_ **Install**: ``composer require symfony/mailjet-notifier`` \
+ **DSN**: ``mailjet://TOKEN@default?from=FROM`` \
+ **Webhook support**: No
+`MessageBird`_ **Install**: ``composer require symfony/message-bird-notifier`` \
+ **DSN**: ``messagebird://TOKEN@default?from=FROM`` \
+ **Webhook support**: No
+`MessageMedia`_ **Install**: ``composer require symfony/message-media-notifier`` \
+ **DSN**: ``messagemedia://API_KEY:API_SECRET@default?from=FROM`` \
+ **Webhook support**: No
+`Mobyt`_ **Install**: ``composer require symfony/mobyt-notifier`` \
+ **DSN**: ``mobyt://USER_KEY:ACCESS_TOKEN@default?from=FROM`` \
+ **Webhook support**: No
+`Nexmo`_ **Install**: ``composer require symfony/nexmo-notifier`` \
+ Abandoned in favor of Vonage (see below) \
+`Octopush`_ **Install**: ``composer require symfony/octopush-notifier`` \
+ **DSN**: ``octopush://USERLOGIN:APIKEY@default?from=FROM&type=TYPE`` \
+ **Webhook support**: No
+`OrangeSms`_ **Install**: ``composer require symfony/orange-sms-notifier`` \
+ **DSN**: ``orange-sms://CLIENT_ID:CLIENT_SECRET@default?from=FROM&sender_name=SENDER_NAME`` \
+ **Webhook support**: No
+`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
+`Primotexto`_ **Install**: ``composer require symfony/primotexto-notifier`` \
+ **DSN**: ``primotexto://API_KEY@default?from=FROM`` \
+ **Webhook support**: No
+`Redlink`_ **Install**: ``composer require symfony/redlink-notifier`` \
+ **DSN**: ``redlink://API_KEY:APP_KEY@default?from=SENDER_NAME&version=API_VERSION`` \
+ **Webhook support**: No
+`RingCentral`_ **Install**: ``composer require symfony/ring-central-notifier`` \
+ **DSN**: ``ringcentral://API_TOKEN@default?from=FROM`` \
+ **Webhook support**: No
+`Sendberry`_ **Install**: ``composer require symfony/sendberry-notifier`` \
+ **DSN**: ``sendberry://USERNAME:PASSWORD@default?auth_key=AUTH_KEY&from=FROM`` \
+ **Webhook support**: No
+`Sendinblue`_ **Install**: ``composer require symfony/sendinblue-notifier`` \
+ **DSN**: ``sendinblue://API_KEY@default?sender=PHONE`` \
+ **Webhook support**: No
+`Sms77`_ **Install**: ``composer require symfony/sms77-notifier`` \
+ **DSN**: ``sms77://API_KEY@default?from=FROM`` \
+ **Webhook support**: No
+`SimpleTextin`_ **Install**: ``composer require symfony/simple-textin-notifier`` \
+ **DSN**: ``simpletextin://API_KEY@default?from=FROM`` \
+ **Webhook support**: No
+`Sinch`_ **Install**: ``composer require symfony/sinch-notifier`` \
+ **DSN**: ``sinch://ACCOUNT_ID:AUTH_TOKEN@default?from=FROM`` \
+ **Webhook support**: No
+`Sipgate`_ **Install**: ``composer require symfony/sipgate-notifier`` \
+ **DSN**: ``sipgate://TOKEN_ID:TOKEN@default?senderId=SENDER_ID`` \
+ **Webhook support**: No
+`SmsSluzba`_ **Install**: ``composer require symfony/sms-sluzba-notifier`` \
+ **DSN**: ``sms-sluzba://USERNAME:PASSWORD@default`` \
+ **Webhook support**: No
+`Smsapi`_ **Install**: ``composer require symfony/smsapi-notifier`` \
+ **DSN**: ``smsapi://TOKEN@default?from=FROM`` \
+ **Webhook support**: No
+`Smsbox`_ **Install**: ``composer require symfony/smsbox-notifier`` \
+ **DSN**: ``smsbox://APIKEY@default?mode=MODE&strategy=STRATEGY&sender=SENDER`` \
+ **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
+`Smsc`_ **Install**: ``composer require symfony/smsc-notifier`` \
+ **DSN**: ``smsc://LOGIN:PASSWORD@default?from=FROM`` \
+ **Webhook support**: No
+`SMSense`_ **Install**: ``composer require smsense-notifier`` \
+ **DSN**: ``smsense://API_TOKEN@default?from=FROM`` \
+ **Webhook support**: No
+`SMSFactor`_ **Install**: ``composer require symfony/sms-factor-notifier`` \
+ **DSN**: ``sms-factor://TOKEN@default?sender=SENDER&push_type=PUSH_TYPE`` \
+ **Webhook support**: No
+`SpotHit`_ **Install**: ``composer require symfony/spot-hit-notifier`` \
+ **DSN**: ``spothit://TOKEN@default?from=FROM`` \
+ **Webhook support**: No
+`Sweego`_ **Install**: ``composer require symfony/sweego-notifier`` \
+ **DSN**: ``sweego://API_KEY@default?region=REGION&campaign_type=CAMPAIGN_TYPE`` \
+ **Webhook support**: Yes
+`Telnyx`_ **Install**: ``composer require symfony/telnyx-notifier`` \
+ **DSN**: ``telnyx://API_KEY@default?from=FROM&messaging_profile_id=MESSAGING_PROFILE_ID`` \
+ **Webhook support**: No
+`TurboSms`_ **Install**: ``composer require symfony/turbo-sms-notifier`` \
+ **DSN**: ``turbosms://AUTH_TOKEN@default?from=FROM`` \
+ **Webhook support**: No
+`Twilio`_ **Install**: ``composer require symfony/twilio-notifier`` \
+ **DSN**: ``twilio://SID:TOKEN@default?from=FROM`` \
+ **Webhook support**: Yes
+`Unifonic`_ **Install**: ``composer require symfony/unifonic-notifier`` \
+ **DSN**: ``unifonic://APP_SID@default?from=FROM`` \
+ **Webhook support**: No
+`Vonage`_ **Install**: ``composer require symfony/vonage-notifier`` \
+ **DSN**: ``vonage://KEY:SECRET@default?from=FROM`` \
+ **Webhook support**: Yes
+`Yunpian`_ **Install**: ``composer require symfony/yunpian-notifier`` \
+ **DSN**: ``yunpian://APIKEY@default`` \
+ **Webhook support**: No
+================== ====================================================================================================================================
+
+.. tip::
+
+ Use :doc:`Symfony configuration secrets ` to securely
+ store your API tokens.
+
+.. tip::
-================== ===================================== ===========================================================================
-Service Package DSN
-================== ===================================== ===========================================================================
-`46elks`_ ``symfony/forty-six-elks-notifier`` ``forty-six-elks://API_USERNAME:API_PASSWORD@default?from=FROM``
-`AllMySms`_ ``symfony/all-my-sms-notifier`` ``allmysms://LOGIN:APIKEY@default?from=FROM``
-`AmazonSns`_ ``symfony/amazon-sns-notifier`` ``sns://ACCESS_KEY:SECRET_KEY@default?region=REGION``
-`Bandwidth`_ ``symfony/bandwidth-notifier`` ``bandwidth://USERNAME:PASSWORD@default?from=FROM&account_id=ACCOUNT_ID&application_id=APPLICATION_ID&priority=PRIORITY``
-`Brevo`_ ``symfony/brevo-notifier`` ``brevo://API_KEY@default?sender=SENDER``
-`Clickatell`_ ``symfony/clickatell-notifier`` ``clickatell://ACCESS_TOKEN@default?from=FROM``
-`ContactEveryone`_ ``symfony/contact-everyone-notifier`` ``contact-everyone://TOKEN@default?&diffusionname=DIFFUSION_NAME&category=CATEGORY``
-`Esendex`_ ``symfony/esendex-notifier`` ``esendex://USER_NAME:PASSWORD@default?accountreference=ACCOUNT_REFERENCE&from=FROM``
-`FakeSms`_ ``symfony/fake-sms-notifier`` ``fakesms+email://MAILER_SERVICE_ID?to=TO&from=FROM`` or ``fakesms+logger://default``
-`FreeMobile`_ ``symfony/free-mobile-notifier`` ``freemobile://LOGIN:API_KEY@default?phone=PHONE``
-`GatewayApi`_ ``symfony/gateway-api-notifier`` ``gatewayapi://TOKEN@default?from=FROM``
-`GoIP`_ ``symfony/goip-notifier`` ``goip://USERNAME:PASSWORD@HOST:80?sim_slot=SIM_SLOT``
-`Infobip`_ ``symfony/infobip-notifier`` ``infobip://AUTH_TOKEN@HOST?from=FROM``
-`Iqsms`_ ``symfony/iqsms-notifier`` ``iqsms://LOGIN:PASSWORD@default?from=FROM``
-`iSendPro`_ ``symfony/isendpro-notifier`` ``isendpro://ACCOUNT_KEY_ID@default?from=FROM&no_stop=NO_STOP&sandbox=SANDBOX``
-`KazInfoTeh`_ ``symfony/kaz-info-teh-notifier`` ``kaz-info-teh://USERNAME:PASSWORD@default?sender=FROM``
-`LightSms`_ ``symfony/light-sms-notifier`` ``lightsms://LOGIN:TOKEN@default?from=PHONE``
-`Mailjet`_ ``symfony/mailjet-notifier`` ``mailjet://TOKEN@default?from=FROM``
-`MessageBird`_ ``symfony/message-bird-notifier`` ``messagebird://TOKEN@default?from=FROM``
-`MessageMedia`_ ``symfony/message-media-notifier`` ``messagemedia://API_KEY:API_SECRET@default?from=FROM``
-`Mobyt`_ ``symfony/mobyt-notifier`` ``mobyt://USER_KEY:ACCESS_TOKEN@default?from=FROM``
-`Nexmo`_ ``symfony/nexmo-notifier`` Abandoned in favor of Vonage (symfony/vonage-notifier).
-`Octopush`_ ``symfony/octopush-notifier`` ``octopush://USERLOGIN:APIKEY@default?from=FROM&type=TYPE``
-`OrangeSms`_ ``symfony/orange-sms-notifier`` ``orange-sms://CLIENT_ID:CLIENT_SECRET@default?from=FROM&sender_name=SENDER_NAME``
-`OvhCloud`_ ``symfony/ovh-cloud-notifier`` ``ovhcloud://APPLICATION_KEY:APPLICATION_SECRET@default?consumer_key=CONSUMER_KEY&service_name=SERVICE_NAME``
-`Plivo`_ ``symfony/plivo-notifier`` ``plivo://AUTH_ID:AUTH_TOKEN@default?from=FROM``
-`Redlink`_ ``symfony/redlink-notifier`` ``redlink://API_KEY:APP_KEY@default?from=SENDER_NAME&version=API_VERSION``
-`RingCentral`_ ``symfony/ring-central-notifier`` ``ringcentral://API_TOKEN@default?from=FROM``
-`Sendberry`_ ``symfony/sendberry-notifier`` ``sendberry://USERNAME:PASSWORD@default?auth_key=AUTH_KEY&from=FROM``
-`Sendinblue`_ ``symfony/sendinblue-notifier`` ``sendinblue://API_KEY@default?sender=PHONE``
-`Sms77`_ ``symfony/sms77-notifier`` ``sms77://API_KEY@default?from=FROM``
-`SimpleTextin`_ ``symfony/simple-textin-notifier`` ``simpletextin://API_KEY@default?from=FROM``
-`Sinch`_ ``symfony/sinch-notifier`` ``sinch://ACCOUNT_ID:AUTH_TOKEN@default?from=FROM``
-`Smsapi`_ ``symfony/smsapi-notifier`` ``smsapi://TOKEN@default?from=FROM``
-`SmsBiuras`_ ``symfony/sms-biuras-notifier`` ``smsbiuras://UID:API_KEY@default?from=FROM&test_mode=0``
-`Smsc`_ ``symfony/smsc-notifier`` ``smsc://LOGIN:PASSWORD@default?from=FROM``
-`SMSFactor`_ ``symfony/sms-factor-notifier`` ``sms-factor://TOKEN@default?sender=SENDER&push_type=PUSH_TYPE``
-`SpotHit`_ ``symfony/spot-hit-notifier`` ``spothit://TOKEN@default?from=FROM``
-`Telnyx`_ ``symfony/telnyx-notifier`` ``telnyx://API_KEY@default?from=FROM&messaging_profile_id=MESSAGING_PROFILE_ID``
-`TurboSms`_ ``symfony/turbo-sms-notifier`` ``turbosms://AUTH_TOKEN@default?from=FROM``
-`Twilio`_ ``symfony/twilio-notifier`` ``twilio://SID:TOKEN@default?from=FROM``
-`Vonage`_ ``symfony/vonage-notifier`` ``vonage://KEY:SECRET@default?from=FROM``
-`Yunpian`_ ``symfony/yunpian-notifier`` ``yunpian://APIKEY@default``
-================== ===================================== ===========================================================================
-
-.. versionadded:: 6.1
-
- The 46elks, OrangeSms, KazInfoTeh and Sendberry integrations were introduced in Symfony 6.1.
- The ``no_stop_clause`` option in ``OvhCloud`` DSN was introduced in Symfony 6.1.
- The ``test`` option in ``Smsapi`` DSN was introduced in Symfony 6.1.
-
-.. versionadded:: 6.2
-
- The ContactEveryone and SMSFactor integrations were introduced in Symfony 6.2.
-
-.. versionadded:: 6.3
-
- The Bandwith, iSendPro, Plivo, RingCentral, SimpleTextin and Termii integrations
- were introduced in Symfony 6.3.
- The ``from`` option in ``Smsapi`` DSN is optional since Symfony 6.3.
-
-.. versionadded:: 6.4
-
- The `Redlink`_ and `Brevo`_ integrations were introduced in Symfony 6.4.
-
-.. deprecated:: 6.4
-
- The `Sendinblue`_ integration is deprecated since
- Symfony 6.4, use the `Brevo`_ integration instead.
+ Some third party transports, when using the API, support status callbacks
+ via webhooks. See the :doc:`Webhook documentation ` for more
+ details.
+
+.. versionadded:: 7.1
+
+ The ``Smsbox``, ``SmsSluzba``, ``SMSense``, ``LOX24`` and ``Unifonic``
+ integrations were introduced in Symfony 7.1.
+
+.. versionadded:: 7.2
+
+ 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
+ Symfony 7.1, use the `Seven.io`_ integration instead.
To enable a texter, add the correct DSN in your ``.env`` file and
configure the ``texter_transports``:
@@ -189,7 +310,7 @@ send SMS messages::
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Notifier\Message\SmsMessage;
use Symfony\Component\Notifier\TexterInterface;
- use Symfony\Component\Routing\Annotation\Route;
+ use Symfony\Component\Routing\Attribute\Route;
class SecurityController
{
@@ -217,14 +338,6 @@ send SMS messages::
}
}
-.. versionadded:: 6.2
-
- The 3rd argument of ``SmsMessage`` (``$from``) was introduced in Symfony 6.2.
-
-.. versionadded:: 6.3
-
- The 4th argument of ``SmsMessage`` (``$options``) was introduced in Symfony 6.3.
-
The ``send()`` method returns a variable of type
:class:`Symfony\\Component\\Notifier\\Message\\SentMessage` which provides
information such as the message ID and the original message contents.
@@ -234,10 +347,10 @@ information such as the message ID and the original message contents.
Chat Channel
~~~~~~~~~~~~
-.. caution::
+.. warning::
If any of the DSN values contains any character considered special in a
- URI (such as ``+``, ``@``, ``$``, ``#``, ``/``, ``:``, ``*``, ``!``), you must
+ URI (such as ``: / ? # [ ] @ ! $ & ' ( ) * + , ; =``), you must
encode them. See `RFC 3986`_ for the full list of reserved characters or use the
:phpfunction:`urlencode` function to encode them.
@@ -245,37 +358,86 @@ The chat channel is used to send chat messages to users by using
:class:`Symfony\\Component\\Notifier\\Chatter` classes. Symfony provides
integration with these chat services:
-======================================= ==================================== =============================================================================
-Service Package DSN
-======================================= ==================================== =============================================================================
-`AmazonSns`_ ``symfony/amazon-sns-notifier`` ``sns://ACCESS_KEY:SECRET_KEY@default?region=REGION``
-`Chatwork`_ ``symfony/chatwork-notifier`` ``chatwork://API_TOKEN@default?room_id=ID``
-`Discord`_ ``symfony/discord-notifier`` ``discord://TOKEN@default?webhook_id=ID``
-`FakeChat`_ ``symfony/fake-chat-notifier`` ``fakechat+email://default?to=TO&from=FROM`` or ``fakechat+logger://default``
-`Firebase`_ ``symfony/firebase-notifier`` ``firebase://USERNAME:PASSWORD@default``
-`Gitter`_ ``symfony/gitter-notifier`` ``gitter://TOKEN@default?room_id=ROOM_ID``
-`GoogleChat`_ ``symfony/google-chat-notifier`` ``googlechat://ACCESS_KEY:ACCESS_TOKEN@default/SPACE?thread_key=THREAD_KEY``
-`LINE Notify`_ ``symfony/line-notify-notifier`` ``linenotify://TOKEN@default``
-`LinkedIn`_ ``symfony/linked-in-notifier`` ``linkedin://TOKEN:USER_ID@default``
-`Mastodon`_ ``symfony/mastodon-notifier`` ``mastodon://ACCESS_TOKEN@HOST``
-`Mattermost`_ ``symfony/mattermost-notifier`` ``mattermost://ACCESS_TOKEN@HOST/PATH?channel=CHANNEL``
-`Mercure`_ ``symfony/mercure-notifier`` ``mercure://HUB_ID?topic=TOPIC``
-`MicrosoftTeams`_ ``symfony/microsoft-teams-notifier`` ``microsoftteams://default/PATH``
-`RocketChat`_ ``symfony/rocket-chat-notifier`` ``rocketchat://TOKEN@ENDPOINT?channel=CHANNEL``
-`Slack`_ ``symfony/slack-notifier`` ``slack://TOKEN@default?channel=CHANNEL``
-`Telegram`_ ``symfony/telegram-notifier`` ``telegram://TOKEN@default?channel=CHAT_ID``
-`Twitter`_ ``symfony/twitter-notifier`` ``twitter://API_KEY:API_SECRET:ACCESS_TOKEN:ACCESS_SECRET@default``
-`Zendesk`_ ``symfony/zendesk-notifier`` ``zendesk://EMAIL:TOKEN@SUBDOMAIN``
-`Zulip`_ ``symfony/zulip-notifier`` ``zulip://EMAIL:TOKEN@HOST?channel=CHANNEL``
-====================================== ==================================== =============================================================================
-
-.. versionadded:: 6.2
-
- The Zendesk and Chatwork integration were introduced in Symfony 6.2.
-
-.. versionadded:: 6.3
-
- The LINE Notify, Mastodon and Twitter integrations were introduced in Symfony 6.3.
+====================================== =====================================================================================
+Service
+====================================== =====================================================================================
+`AmazonSns`_ **Install**: ``composer require symfony/amazon-sns-notifier`` \
+ **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`` \
+ **DSN**: ``discord://TOKEN@default?webhook_id=ID``
+`FakeChat`_ **Install**: ``composer require symfony/fake-chat-notifier`` \
+ **DSN**: ``fakechat+email://default?to=TO&from=FROM`` or ``fakechat+logger://default``
+`Firebase`_ **Install**: ``composer require symfony/firebase-notifier`` \
+ **DSN**: ``firebase://USERNAME:PASSWORD@default``
+`GoogleChat`_ **Install**: ``composer require symfony/google-chat-notifier`` \
+ **DSN**: ``googlechat://ACCESS_KEY:ACCESS_TOKEN@default/SPACE?thread_key=THREAD_KEY``
+`LINE Bot`_ **Install**: ``composer require symfony/line-bot-notifier`` \
+ **DSN**: ``linebot://TOKEN@default?receiver=RECEIVER``
+`LINE Notify`_ **Install**: ``composer require symfony/line-notify-notifier`` \
+ **DSN**: ``linenotify://TOKEN@default``
+`LinkedIn`_ **Install**: ``composer require symfony/linked-in-notifier`` \
+ **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`` \
+ **DSN**: ``mercure://HUB_ID?topic=TOPIC``
+`MicrosoftTeams`_ **Install**: ``composer require symfony/microsoft-teams-notifier`` \
+ **DSN**: ``microsoftteams://default/PATH``
+`RocketChat`_ **Install**: ``composer require symfony/rocket-chat-notifier`` \
+ **DSN**: ``rocketchat://TOKEN@ENDPOINT?channel=CHANNEL``
+`Slack`_ **Install**: ``composer require symfony/slack-notifier`` \
+ **DSN**: ``slack://TOKEN@default?channel=CHANNEL``
+`Telegram`_ **Install**: ``composer require symfony/telegram-notifier`` \
+ **DSN**: ``telegram://TOKEN@default?channel=CHAT_ID``
+`Twitter`_ **Install**: ``composer require symfony/twitter-notifier`` \
+ **DSN**: ``twitter://API_KEY:API_SECRET:ACCESS_TOKEN:ACCESS_SECRET@default``
+`Zendesk`_ **Install**: ``composer require symfony/zendesk-notifier`` \
+ **DSN**: ``zendesk://EMAIL:TOKEN@SUBDOMAIN``
+`Zulip`_ **Install**: ``composer require symfony/zulip-notifier`` \
+ **DSN**: ``zulip://EMAIL:TOKEN@HOST?channel=CHANNEL``
+====================================== =====================================================================================
+
+.. versionadded:: 7.1
+
+ The ``Bluesky`` integration was introduced in Symfony 7.1.
+
+.. versionadded:: 7.2
+
+ The ``LINE Bot`` integration was introduced in Symfony 7.2.
+
+.. deprecated:: 7.2
+
+ 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,
+ the notifications will be sent through the MessageBus. If you don't have a
+ message consumer running, messages will never be sent.
+
+ To change this behavior, add the following configuration to send messages
+ directly via the transport:
+
+ .. code-block:: yaml
+
+ # config/packages/notifier.yaml
+ framework:
+ notifier:
+ message_bus: false
Chatters are configured using the ``chatter_transports`` setting:
@@ -338,13 +500,11 @@ you to send messages to chat services::
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Notifier\ChatterInterface;
use Symfony\Component\Notifier\Message\ChatMessage;
- use Symfony\Component\Routing\Annotation\Route;
+ use Symfony\Component\Routing\Attribute\Route;
class CheckoutController extends AbstractController
{
- /**
- * @Route("/checkout/thankyou")
- */
+ #[Route('/checkout/thankyou')]
public function thankyou(ChatterInterface $chatter): Response
{
$message = (new ChatMessage('You got a new invoice for 15 EUR.'))
@@ -433,10 +593,10 @@ notification emails:
Push Channel
~~~~~~~~~~~~
-.. caution::
+.. warning::
If any of the DSN values contains any character considered special in a
- URI (such as ``+``, ``@``, ``$``, ``#``, ``/``, ``:``, ``*``, ``!``), you must
+ URI (such as ``: / ? # [ ] @ ! $ & ' ( ) * + , ; =``), you must
encode them. See `RFC 3986`_ for the full list of reserved characters or use the
:phpfunction:`urlencode` function to encode them.
@@ -444,33 +604,34 @@ The push channel is used to send notifications to users by using
:class:`Symfony\\Component\\Notifier\\Texter` classes. Symfony provides
integration with these push services:
-=============== ==================================== ==============================================================================
-Service Package DSN
-=============== ==================================== ==============================================================================
-`Engagespot`_ ``symfony/engagespot-notifier`` ``engagespot://API_KEY@default?campaign_name=CAMPAIGN_NAME``
-`Expo`_ ``symfony/expo-notifier`` ``expo://Token@default``
-`Novu`_ ``symfony/novu-notifier`` ``novu://API_KEY@default``
-`Ntfy`_ ``symfony/ntfy-notifier`` ``ntfy://default/TOPIC``
-`OneSignal`_ ``symfony/one-signal-notifier`` ``onesignal://APP_ID:API_KEY@default?defaultRecipientId=DEFAULT_RECIPIENT_ID``
-`PagerDuty`_ ``symfony/pager-duty-notifier`` ``pagerduty://TOKEN@SUBDOMAIN``
-`Pushover`_ ``symfony/pushover-notifier`` ``pushover://USER_KEY:APP_TOKEN@default``
-=============== ==================================== ==============================================================================
-
-.. versionadded:: 6.1
-
- The Engagespot integration was introduced in Symfony 6.1.
-
-.. versionadded:: 6.3
-
- The PagerDuty and Pushover integrations were introduced in Symfony 6.3.
-
-.. versionadded:: 6.4
-
- The Novu, Ntfy and GoIP integrations were introduced in Symfony 6.4.
+=============== =======================================================================================
+Service
+=============== =======================================================================================
+`Engagespot`_ **Install**: ``composer require symfony/engagespot-notifier`` \
+ **DSN**: ``engagespot://API_KEY@default?campaign_name=CAMPAIGN_NAME``
+`Expo`_ **Install**: ``composer require symfony/expo-notifier`` \
+ **DSN**: ``expo://TOKEN@default``
+`Novu`_ **Install**: ``composer require symfony/novu-notifier`` \
+ **DSN**: ``novu://API_KEY@default``
+`Ntfy`_ **Install**: ``composer require symfony/ntfy-notifier`` \
+ **DSN**: ``ntfy://default/TOPIC``
+`OneSignal`_ **Install**: ``composer require symfony/one-signal-notifier`` \
+ **DSN**: ``onesignal://APP_ID:API_KEY@default?defaultRecipientId=DEFAULT_RECIPIENT_ID``
+`PagerDuty`_ **Install**: ``composer require symfony/pager-duty-notifier`` \
+ **DSN**: ``pagerduty://TOKEN@SUBDOMAIN``
+`Pushover`_ **Install**: ``composer require symfony/pushover-notifier`` \
+ **DSN**: ``pushover://USER_KEY:APP_TOKEN@default``
+`Pushy`_ **Install**: ``composer require symfony/pushy-notifier`` \
+ **DSN**: ``pushy://API_KEY@default``
+=============== =======================================================================================
To enable a texter, add the correct DSN in your ``.env`` file and
configure the ``texter_transports``:
+.. versionadded:: 7.1
+
+ The `Pushy`_ integration was introduced in Symfony 7.1.
+
.. code-block:: bash
# .env
@@ -518,6 +679,124 @@ configure the ``texter_transports``:
;
};
+.. _notifier-desktop-channel:
+
+Desktop Channel
+~~~~~~~~~~~~~~~
+
+The desktop channel is used to display local desktop notifications on the same
+host machine using :class:`Symfony\\Component\\Notifier\\Texter` classes. Currently,
+Symfony is integrated with the following providers:
+
+=============== ================================================ ==============================================================================
+Provider Install DSN
+=============== ================================================ ==============================================================================
+`JoliNotif`_ ``composer require symfony/joli-notif-notifier`` ``jolinotif://default``
+=============== ================================================ ==============================================================================
+
+.. versionadded:: 7.2
+
+ The JoliNotif bridge was introduced in Symfony 7.2.
+
+If you are using :ref:`Symfony Flex `, installing that package will
+also create the necessary environment variable and configuration. Otherwise, you'll
+need to add the following manually:
+
+1) Add the correct DSN in your ``.env`` file:
+
+.. code-block:: bash
+
+ # .env
+ JOLINOTIF=jolinotif://default
+
+2) Update the Notifier configuration to add a new texter transport:
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/notifier.yaml
+ framework:
+ notifier:
+ texter_transports:
+ jolinotif: '%env(JOLINOTIF)%'
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+ %env(JOLINOTIF)%
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/notifier.php
+ use Symfony\Config\FrameworkConfig;
+
+ return static function (FrameworkConfig $framework): void {
+ $framework->notifier()
+ ->texterTransport('jolinotif', env('JOLINOTIF'))
+ ;
+ };
+
+Now you can send notifications to your desktop as follows::
+
+ // src/Notifier/SomeService.php
+ use Symfony\Component\Notifier\Message\DesktopMessage;
+ use Symfony\Component\Notifier\TexterInterface;
+ // ...
+
+ class SomeService
+ {
+ public function __construct(
+ private TexterInterface $texter,
+ ) {
+ }
+
+ public function notifyNewSubscriber(User $user): void
+ {
+ $message = new DesktopMessage(
+ 'New subscription! 🎉',
+ sprintf('%s is a new subscriber', $user->getFullName())
+ );
+
+ $this->texter->send($message);
+ }
+ }
+
+These notifications can be customized further, and depending on your operating system,
+they may support features like custom sounds, icons, and more::
+
+ use Symfony\Component\Notifier\Bridge\JoliNotif\JoliNotifOptions;
+ // ...
+
+ $options = (new JoliNotifOptions())
+ ->setIconPath('/path/to/icons/error.png')
+ ->setExtraOption('sound', 'sosumi')
+ ->setExtraOption('url', 'https://example.com/');
+
+ $message = new DesktopMessage('Production is down', <<send($message);
+
Configure to use Failover or Round-Robin Transports
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -804,7 +1083,7 @@ and its ``asChatMessage()`` method::
) {
}
- public function asChatMessage(RecipientInterface $recipient, string $transport = null): ?ChatMessage
+ public function asChatMessage(RecipientInterface $recipient, ?string $transport = null): ?ChatMessage
{
// Add a custom subject and emoji if the message is sent to Slack
if ('slack' === $transport) {
@@ -821,25 +1100,22 @@ and its ``asChatMessage()`` method::
The
:class:`Symfony\\Component\\Notifier\\Notification\\SmsNotificationInterface`,
-:class:`Symfony\\Component\\Notifier\\Notification\\EmailNotificationInterface`
-and
+:class:`Symfony\\Component\\Notifier\\Notification\\EmailNotificationInterface`,
:class:`Symfony\\Component\\Notifier\\Notification\\PushNotificationInterface`
+and
+:class:`Symfony\\Component\\Notifier\\Notification\\DesktopNotificationInterface`
also exists to modify messages sent to those channels.
Customize Browser Notifications (Flash Messages)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-.. versionadded:: 6.1
-
- Support for customizing importance levels was introduced in Symfony 6.1.
-
The default behavior for browser channel notifications is to add a
:ref:`flash message ` with ``notification`` as its key.
However, you might prefer to map the importance level of the notification to the
type of flash message, so you can tweak their style.
-you can do that by overriding the default ``notifier.flash_message_importance_mapper``
+You can do that by overriding the default ``notifier.flash_message_importance_mapper``
service with your own implementation of
:class:`Symfony\\Component\\Notifier\\FlashMessage\\FlashMessageImportanceMapperInterface`
where you can provide your own "importance" to "alert level" mapping.
@@ -892,11 +1168,6 @@ You can benefit from this class by using it directly or extending the
See :ref:`testing documentation ` for the list of available assertions.
-.. versionadded:: 6.2
-
- The :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\NotificationAssertionsTrait`
- was introduced in Symfony 6.2.
-
Disabling Delivery
------------------
@@ -923,8 +1194,8 @@ Using Events
The :class:`Symfony\\Component\\Notifier\\Transport` class of the Notifier component
allows you to optionally hook into the lifecycle via events.
-The ``MessageEvent::class`` Event
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The ``MessageEvent`` Event
+~~~~~~~~~~~~~~~~~~~~~~~~~~
**Typical Purposes**: Doing something before the message is sent (like logging
which message is going to be sent, or displaying something about the event
@@ -984,7 +1255,7 @@ is dispatched. Listeners receive a
$dispatcher->addListener(SentMessageEvent::class, function (SentMessageEvent $event): void {
// gets the message instance
- $message = $event->getOriginalMessage();
+ $message = $event->getMessage();
// log something
$this->logger(sprintf('The message has been successfully sent and has id: %s', $message->getMessageId()));
@@ -999,6 +1270,7 @@ is dispatched. Listeners receive a
.. _`AllMySms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/AllMySms/README.md
.. _`AmazonSns`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md
.. _`Bandwidth`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Bandwidth/README.md
+.. _`Bluesky`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Bluesky/README.md
.. _`Brevo`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Brevo/README.md
.. _`Chatwork`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Chatwork/README.md
.. _`Clickatell`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Clickatell/README.md
@@ -1012,18 +1284,21 @@ is dispatched. Listeners receive a
.. _`Firebase`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Firebase/README.md
.. _`FreeMobile`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/FreeMobile/README.md
.. _`GatewayApi`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/GatewayApi/README.md
-.. _`Gitter`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Gitter/README.md
.. _`GoIP`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/GoIP/README.md
.. _`GoogleChat`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/GoogleChat/README.md
.. _`Infobip`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Infobip/README.md
.. _`Iqsms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Iqsms/README.md
.. _`iSendPro`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Isendpro/README.md
+.. _`JoliNotif`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/JoliNotif/README.md
.. _`KazInfoTeh`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/KazInfoTeh/README.md
+.. _`LINE Bot`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/LineBot/README.md
.. _`LINE Notify`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/LineNotify/README.md
.. _`LightSms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/LightSms/README.md
.. _`LinkedIn`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/LinkedIn/README.md
+.. _`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
@@ -1039,7 +1314,9 @@ is dispatched. Listeners receive a
.. _`OvhCloud`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/OvhCloud/README.md
.. _`PagerDuty`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/PagerDuty/README.md
.. _`Plivo`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Plivo/README.md
+.. _`Primotexto`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Primotexto/README.md
.. _`Pushover`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Pushover/README.md
+.. _`Pushy`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Pushy/README.md
.. _`Redlink`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Redlink/README.md
.. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt
.. _`RingCentral`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/RingCentral/README.md
@@ -1047,19 +1324,26 @@ is dispatched. Listeners receive a
.. _`SMSFactor`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SmsFactor/README.md
.. _`Sendberry`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sendberry/README.md
.. _`Sendinblue`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sendinblue/README.md
+.. _`Seven.io`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sevenio/README.md
.. _`SimpleTextin`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SimpleTextin/README.md
.. _`Sinch`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sinch/README.md
+.. _`Sipgate`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sipgate/README.md
.. _`Slack`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Slack/README.md
.. _`Sms77`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sms77/README.md
.. _`SmsBiuras`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SmsBiuras/README.md
+.. _`Smsbox`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Smsbox/README.md
.. _`Smsapi`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Smsapi/README.md
.. _`Smsc`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Smsc/README.md
+.. _`SMSense`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SMSense/README.md
+.. _`SmsSluzba`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SmsSluzba/README.md
.. _`SpotHit`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SpotHit/README.md
+.. _`Sweego`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sweego/README.md
.. _`Telegram`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Telegram/README.md
.. _`Telnyx`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Telnyx/README.md
.. _`TurboSms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/TurboSms/README.md
.. _`Twilio`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Twilio/README.md
.. _`Twitter`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Twitter/README.md
+.. _`Unifonic`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Unifonic/README.md
.. _`Vonage`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Vonage/README.md
.. _`Yunpian`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Yunpian/README.md
.. _`Zendesk`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Zendesk/README.md
diff --git a/object_mapper.rst b/object_mapper.rst
new file mode 100644
index 00000000000..fe7db2365cf
--- /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 form 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/page_creation.rst b/page_creation.rst
index fa280ea600b..f8b2fdaf251 100644
--- a/page_creation.rst
+++ b/page_creation.rst
@@ -69,7 +69,7 @@ metadata to code):
// src/Controller/LuckyController.php
// ...
- + use Symfony\Component\Routing\Annotation\Route;
+ + use Symfony\Component\Routing\Attribute\Route;
class LuckyController
{
@@ -281,6 +281,7 @@ OK, time to finish mastering the fundamentals by reading these articles:
* :doc:`/routing`
* :doc:`/controller`
* :doc:`/templates`
+* :doc:`/frontend`
* :doc:`/configuration`
Then, learn about other important topics like the
diff --git a/performance.rst b/performance.rst
index fac10407759..828333f338b 100644
--- a/performance.rst
+++ b/performance.rst
@@ -98,7 +98,7 @@ Use the OPcache Byte Code Cache
OPcache stores the compiled PHP files to avoid having to recompile them for
every request. There are some `byte code caches`_ available, but as of PHP
5.5, PHP comes with `OPcache`_ built-in. For older versions, the most widely
-used byte code cache is `APC`_.
+used byte code cache is APC.
.. _performance-use-preloading:
@@ -263,10 +263,6 @@ in performance, you can stop generating the file as follows:
// ...
$container->parameters()->set('debug.container.dump', false);
-.. versionadded:: 6.3
-
- The ``debug.container.dump`` option was introduced in Symfony 6.3.
-
.. _profiling-applications:
Profiling Symfony Applications
@@ -366,6 +362,13 @@ method does, which stops an event and then restarts it immediately::
// Lap information is stored as "periods" within the event:
// $event->getPeriods();
+ // Gets the last event period:
+ // $event->getLastPeriod();
+
+.. versionadded:: 7.2
+
+ The ``getLastPeriod()`` method was introduced in Symfony 7.2.
+
Profiling Sections
..................
@@ -386,10 +389,16 @@ All events that don't belong to any named section are added to the special secti
called ``__root__``. This way you can get all stopwatch events, even if you don't
know their names, as follows::
- foreach($this->stopwatch->getSectionEvents('__root__') as $event) {
+ use Symfony\Component\Stopwatch\Stopwatch;
+
+ foreach($this->stopwatch->getSectionEvents(Stopwatch::ROOT) as $event) {
echo (string) $event;
}
+.. versionadded:: 7.2
+
+ The ``Stopwatch::ROOT`` constant as a shortcut for ``__root__`` was introduced in Symfony 7.2.
+
Learn more
----------
@@ -398,7 +407,6 @@ Learn more
.. _`byte code caches`: https://en.wikipedia.org/wiki/List_of_PHP_accelerators
.. _`OPcache`: https://www.php.net/manual/en/book.opcache.php
.. _`Composer's autoloader optimization`: https://getcomposer.org/doc/articles/autoloader-optimization.md
-.. _`APC`: https://www.php.net/manual/en/book.apc.php
.. _`APCu Polyfill component`: https://github.com/symfony/polyfill-apcu
.. _`APCu PHP functions`: https://www.php.net/manual/en/ref.apcu.php
.. _`cachetool`: https://github.com/gordalina/cachetool
diff --git a/profiler.rst b/profiler.rst
index b21132a5816..7fc97c8ee33 100644
--- a/profiler.rst
+++ b/profiler.rst
@@ -4,7 +4,7 @@ Profiler
The profiler is a powerful **development tool** that gives detailed information
about the execution of any request.
-.. caution::
+.. danger::
**Never** enable the profiler in production environments
as it will lead to major security vulnerabilities in your project.
@@ -35,10 +35,6 @@ Symfony Profiler, which will look like this:
in the ``X-Debug-Token-Link`` HTTP response header. Browse the ``/_profiler``
URL to see all profiles.
-.. versionadded:: 6.3
-
- Profile garbage collection was introduced in Symfony 6.3.
-
.. note::
To limit the storage used by profiles on disk, they are probabilistically
@@ -58,6 +54,12 @@ method to access to its associated profile::
// ... $profiler is the 'profiler' service
$profile = $profiler->loadProfileFromResponse($response);
+.. note::
+
+ The ``profiler`` service will be :doc:`autowired `
+ automatically when type-hinting any service argument with the
+ :class:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler` class.
+
When the profiler stores data about a request, it also associates a token with it;
this token is available in the ``X-Debug-Token`` HTTP header of the response.
Using this token, you can access the profile of any past response thanks to the
@@ -91,10 +93,6 @@ look for tokens based on some criteria::
// gets the latest 10 tokens for requests that happened between 2 and 4 days ago
$tokens = $profiler->find('', '', 10, '', '4 days ago', '2 days ago');
-.. versionadded:: 6.4
-
- Prefixing the URL filter with a ``!`` symbol to negate the query was introduced in Symfony 6.4.
-
Data Collectors
---------------
@@ -219,46 +217,74 @@ 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);
+ };
- $response->headers->set('Symfony-Debug-Toolbar-Replace', 1);
+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');
Ideally this header should only be set during development and not for
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);
+ $response->headers->set('Symfony-Debug-Toolbar-Replace', '1');
}
}
@@ -290,7 +316,7 @@ request::
class RequestCollector extends AbstractDataCollector
{
- public function collect(Request $request, Response $response, \Throwable $exception = null): void
+ public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
$this->data = [
'method' => $request->getMethod(),
@@ -306,13 +332,13 @@ These are the method that you can define in the data collector class:
from ``AbstractDataCollector``). If you need some services to collect the
data, inject those services in the data collector constructor.
- .. caution::
+ .. warning::
The ``collect()`` method is only called once. It is not used to "gather"
data but is there to "pick up" the data that has been stored by your
service.
- .. caution::
+ .. warning::
As the profiler serializes data collector instances, you should not
store objects that cannot be serialized (like PDO objects) or you need
diff --git a/quick_tour/flex_recipes.rst b/quick_tour/flex_recipes.rst
index a71961d78af..856b4271205 100644
--- a/quick_tour/flex_recipes.rst
+++ b/quick_tour/flex_recipes.rst
@@ -79,8 +79,8 @@ Thanks to Flex, after one command, you can start using Twig immediately:
// src/Controller/DefaultController.php
namespace App\Controller;
- use Symfony\Component\Routing\Annotation\Route;
- - use Symfony\Component\HttpFoundation\Response;
+ use Symfony\Component\Routing\Attribute\Route;
+ use Symfony\Component\HttpFoundation\Response;
+ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
- class DefaultController
@@ -157,7 +157,7 @@ Are you building an API? You can already return JSON from any controller::
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
- use Symfony\Component\Routing\Annotation\Route;
+ use Symfony\Component\Routing\Attribute\Route;
class DefaultController extends AbstractController
{
diff --git a/quick_tour/the_architecture.rst b/quick_tour/the_architecture.rst
index a4154d822b1..3b66570b3d3 100644
--- a/quick_tour/the_architecture.rst
+++ b/quick_tour/the_architecture.rst
@@ -27,7 +27,7 @@ use the logger in a controller, add a new argument type-hinted with ``LoggerInte
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
- use Symfony\Component\Routing\Annotation\Route;
+ use Symfony\Component\Routing\Attribute\Route;
class DefaultController extends AbstractController
{
@@ -108,7 +108,7 @@ Great! You can use it immediately in your controller::
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
- use Symfony\Component\Routing\Annotation\Route;
+ use Symfony\Component\Routing\Attribute\Route;
class DefaultController extends AbstractController
{
@@ -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/quick_tour/the_big_picture.rst b/quick_tour/the_big_picture.rst
index e78cdf4e698..b069cb4f716 100644
--- a/quick_tour/the_big_picture.rst
+++ b/quick_tour/the_big_picture.rst
@@ -39,7 +39,7 @@ Symfony application:
├─ var/
└─ vendor/
-Can we already load the project in a browser? Yes! You can setup
+Can we already load the project in a browser? Yes! You can set up
:doc:`Nginx or Apache ` and configure their
document root to be the ``public/`` directory. But, for development, it's better
to :doc:`install the Symfony local web server ` and run
@@ -63,20 +63,6 @@ web app, or a microservice. Symfony starts small, but scales with you.
But before we go too far, let's dig into the fundamentals by building our first page.
-Start in ``config/routes.yaml``: this is where *we* can define the URL to our new
-page. Uncomment the example that already lives in the file:
-
-.. code-block:: yaml
-
- # config/routes.yaml
- index:
- path: /
- controller: 'App\Controller\DefaultController::index'
-
-This is called a *route*: it defines the URL to your page (``/``) and the "controller":
-the *function* that will be called whenever anyone goes to this URL. That function
-doesn't exist yet, so let's create it!
-
In ``src/Controller``, create a new ``DefaultController`` class and an ``index``
method inside::
@@ -84,9 +70,11 @@ method inside::
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
+ use Symfony\Component\Routing\Attribute\Route;
class DefaultController
{
+ #[Route('/', name: 'index')]
public function index(): Response
{
return new Response('Hello!');
@@ -104,11 +92,21 @@ But the routing system is *much* more powerful. So let's make the route more int
.. code-block:: diff
- # config/routes.yaml
- index:
- - path: /
- + path: /hello/{name}
- controller: 'App\Controller\DefaultController::index'
+ // src/Controller/DefaultController.php
+ namespace App\Controller;
+
+ use Symfony\Component\HttpFoundation\Response;
+ use Symfony\Component\Routing\Attribute\Route;
+
+ class DefaultController
+ {
+ - #[Route('/', name: 'index')]
+ + #[Route('/hello/{name}', name: 'index')]
+ public function index(): Response
+ {
+ return new Response('Hello!');
+ }
+ }
The URL to this page has changed: it is *now* ``/hello/*``: the ``{name}`` acts
like a wildcard that matches anything. And it gets better! Update the controller too:
@@ -120,9 +118,11 @@ like a wildcard that matches anything. And it gets better! Update the controller
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
+ use Symfony\Component\Routing\Attribute\Route;
class DefaultController
{
+ #[Route('/hello/{name}', name: 'index')]
- public function index()
+ public function index(string $name): Response
{
@@ -135,45 +135,14 @@ Try the page out by going to ``http://localhost:8000/hello/Symfony``. You should
see: Hello Symfony! The value of the ``{name}`` in the URL is available as a ``$name``
argument in your controller.
-But this can be even simpler! Comment-out the YAML route by adding the
-``#`` character:
-
-.. code-block:: yaml
-
- # config/routes.yaml
- # index:
- # path: /hello/{name}
- # controller: 'App\Controller\DefaultController::index'
-
-Instead, add the route *right above* the controller method:
-
-.. code-block:: diff
-
- ` or
+ :doc:`locator `.
+
+ .. versionadded:: 7.1
+
+ The automatic addition of the ``rate_limiter`` tag was introduced
+ in Symfony 7.1.
+
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
@@ -230,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.)
@@ -279,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);
@@ -320,11 +338,6 @@ processes by reserving unused tokens.
$limit->wait();
} while (!$limit->isAccepted());
-.. versionadded:: 6.4
-
- The support for the ``reserve()`` method for the ``SlidingWindow`` strategy
- was introduced in Symfony 6.4.
-
Exposing the Rate Limiter Status
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -343,17 +356,17 @@ 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();
$headers = [
'X-RateLimit-Remaining' => $limit->getRemainingTokens(),
- 'X-RateLimit-Retry-After' => $limit->calculateTimeForTokens(1, 1),
+ 'X-RateLimit-Retry-After' => $limit->getRetryAfter()->getTimestamp() - time(),
'X-RateLimit-Limit' => $limit->getLimit(),
];
@@ -370,19 +383,6 @@ the :class:`Symfony\\Component\\RateLimiter\\Reservation` object returned by the
}
}
-.. versionadded:: 6.4
-
- The :method:`Symfony\\Component\\RateLimiter\\Policy\\SlidingWindow::calculateTimeForTokens`
- method was introduced in Symfony 6.4.
-
-.. deprecated:: 6.4
-
- The :method:`Symfony\\Component\\RateLimiter\\Policy\\SlidingWindow::getRetryAfter`
- method is deprecated since Symfony 6.4. Prior to this version, the
- ``getRetryAfter()`` method must be used instead of the
- :method:`Symfony\\Component\\RateLimiter\\Policy\\SlidingWindow::calculateTimeForTokens`
- method.
-
.. _rate-limiter-storage:
Storing Rate Limiter State
@@ -467,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::
@@ -540,9 +541,133 @@ 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/
+.. _`Caddy HTTP rate limit module`: https://github.com/mholt/caddy-ratelimit
.. _`token bucket algorithm`: https://en.wikipedia.org/wiki/Token_bucket
.. _`PHP date relative formats`: https://www.php.net/manual/en/datetime.formats.php#datetime.formats.relative
.. _`Race conditions`: https://en.wikipedia.org/wiki/Race_condition
diff --git a/reference/attributes.rst b/reference/attributes.rst
index 841bd56e2bb..eb09f4aa6bc 100644
--- a/reference/attributes.rst
+++ b/reference/attributes.rst
@@ -5,11 +5,6 @@ Attributes are the successor of annotations since PHP 8. Attributes are native
to the language and Symfony takes full advantage of them across the framework
and its different components.
-.. deprecated:: 6.4
-
- Annotations across the framework are deprecated since Symfony 6.4, you must
- only use attributes instead.
-
Doctrine Bridge
~~~~~~~~~~~~~~~
@@ -38,14 +33,23 @@ Dependency Injection
* :ref:`Autowire `
* :ref:`AutowireCallable `
* :doc:`AutowireDecorated `
-* :doc:`AutowireIterator `
+* :ref:`AutowireIterator `
* :ref:`AutowireLocator `
+* :ref:`AutowireMethodOf `
* :ref:`AutowireServiceClosure `
* :ref:`Exclude `
+* :ref:`Lazy `
* :ref:`TaggedIterator `
* :ref:`TaggedLocator `
* :ref:`Target `
* :ref:`When `
+* :ref:`WhenNot `
+
+.. deprecated:: 7.1
+
+ The :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedIterator`
+ and :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedLocator`
+ attributes were deprecated in Symfony 7.1.
EventDispatcher
~~~~~~~~~~~~~~~
@@ -67,6 +71,7 @@ HttpKernel
* :ref:`MapQueryParameter `
* :ref:`MapQueryString `
* :ref:`MapRequestPayload `
+* :ref:`MapUploadedFile `
* :ref:`ValueResolver `
* :ref:`WithHttpStatus `
* :ref:`WithLogLevel `
@@ -74,34 +79,53 @@ HttpKernel
Messenger
~~~~~~~~~
+* :ref:`AsMessage `
* :ref:`AsMessageHandler `
+RemoteEvent
+~~~~~~~~~~~
+
+* :ref:`AsRemoteEventConsumer `
+
Routing
~~~~~~~
* :doc:`Route `
+Scheduler
+~~~~~~~~~
+
+* :ref:`AsCronTask `
+* :ref:`AsPeriodicTask `
+* :ref:`AsSchedule `
+
Security
~~~~~~~~
* :ref:`CurrentUser `
+* :ref:`IsCsrfTokenValid `
* :ref:`IsGranted `
+.. _reference-attributes-serializer:
+
Serializer
~~~~~~~~~~
-* :ref:`Context `
+* :ref:`Context `
* :ref:`DiscriminatorMap `
-* :ref:`Groups `
+* :ref:`Groups `
* :ref:`Ignore `
* :ref:`MaxDepth `
-* :ref:`SerializedName `
-* :ref:`SerializedPath `
+* :ref:`SerializedName `
+* :ref:`SerializedPath `
Twig
~~~~
* :ref:`Template `
+* :ref:`AsTwigFilter `
+* :ref:`AsTwigFunction `
+* ``AsTwigTest``
Symfony UX
~~~~~~~~~~
diff --git a/reference/configuration/debug.rst b/reference/configuration/debug.rst
index 482396d2ae2..6ca05b49bd7 100644
--- a/reference/configuration/debug.rst
+++ b/reference/configuration/debug.rst
@@ -8,18 +8,14 @@ key in your application configuration.
.. code-block:: terminal
# displays the default config values defined by Symfony
- $ php bin/console config:dump-reference framework
+ $ php bin/console config:dump-reference debug
# displays the actual config values used by your application
- $ php bin/console debug:config framework
+ $ php bin/console debug:config debug
# displays the config values used by your application and replaces the
# environment variables with their actual values
- $ php bin/console debug:config --resolve-env framework
-
-.. versionadded:: 6.2
-
- The ``--resolve-env`` option was introduced in Symfony 6.2.
+ $ php bin/console debug:config --resolve-env debug
.. note::
@@ -27,35 +23,6 @@ key in your application configuration.
namespace and the related XSD schema is available at:
``https://symfony.com/schema/dic/debug/debug-1.0.xsd``
-Configuration
--------------
-
-max_items
-~~~~~~~~~
-
-**type**: ``integer`` **default**: ``2500``
-
-This is the maximum number of items to dump. Setting this option to ``-1``
-disables the limit.
-
-min_depth
-~~~~~~~~~
-
-**type**: ``integer`` **default**: ``1``
-
-Configures the minimum tree depth until which all items are guaranteed to
-be cloned. After this depth is reached, only ``max_items`` items will be
-cloned. The default value is ``1``, which is consistent with older Symfony
-versions.
-
-max_string_length
-~~~~~~~~~~~~~~~~~
-
-**type**: ``integer`` **default**: ``-1``
-
-This option configures the maximum string length before truncating the
-string. The default value (``-1``) means that strings are never truncated.
-
.. _configuration-debug-dump_destination:
dump_destination
@@ -95,8 +62,39 @@ Typically, you would set this to ``php://stderr``:
.. code-block:: php
// config/packages/debug.php
- $container->loadFromExtension('debug', [
- 'dump_destination' => 'php://stderr',
- ]);
+ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
+
+ return static function (ContainerConfigurator $container): void {
+ $container->extension('debug', [
+ 'dump_destination' => 'php://stderr',
+ ]);
+ };
+
Configure it to ``"tcp://%env(VAR_DUMPER_SERVER)%"`` in order to use the :ref:`ServerDumper feature