From c5ae3dd4c440b25fe150d9599799d4c5cbf2ebe3 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Thu, 12 Sep 2013 17:42:39 -0500 Subject: [PATCH 1/3] [#2956] Refactoring the scopes entry entirely to not talk about the request anymore, since it has a simpler solution See sha: b84f52c28d969b5143521261d818105824eaea5e for more information. --- cookbook/service_container/scopes.rst | 201 ++++++++++++++------------ 1 file changed, 107 insertions(+), 94 deletions(-) diff --git a/cookbook/service_container/scopes.rst b/cookbook/service_container/scopes.rst index 78ccb76126f..32884424732 100644 --- a/cookbook/service_container/scopes.rst +++ b/cookbook/service_container/scopes.rst @@ -12,7 +12,9 @@ This entry is all about scopes, a somewhat advanced topic related to the If you are trying to inject the ``request`` service, the simple solution is to inject the ``request_stack`` service instead and access the current - Request by calling the ``getCurrentRequest()`` method. + Request by calling the ``getCurrentRequest()`` method. The rest of this + entry talks about scopes in a theoretical and more advanced way. If you're + dealing with scopes for the ``request`` service, simply inject ``request_stack``. Understanding Scopes -------------------- @@ -32,10 +34,22 @@ also defines a third scope: ``request``. This scope is tied to the request, meaning a new instance is created for each subrequest and is unavailable outside the request (for instance in the CLI). +The Example: client Scope +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Other than the ``request`` service (which has a simple solution, see the +above note), no services in the default Symfony2 container belong to any +scope other than ``container`` and ``prototype``. But for the purposes of +this entry, imagine there is another scope ``client`` and a service ``client_configuration`` +that belongs to it. This is not a common situation, but the idea is that +you may enter and exit multiple ``client`` scopes during a request, and each +has its own ``client_configuration`` service. + Scopes add a constraint on the dependencies of a service: a service cannot depend on services from a narrower scope. For example, if you create a generic -``my_foo`` service, but try to inject the ``request`` service, you will receive -a :class:`Symfony\\Component\\DependencyInjection\\Exception\\ScopeWideningInjectionException` +``my_foo`` service, but try to inject the ``client_configuration`` service, +you will receive a +:class:`Symfony\\Component\\DependencyInjection\\Exception\\ScopeWideningInjectionException` when compiling the container. Read the sidebar below for more details. .. sidebar:: Scopes and Dependencies @@ -45,28 +59,29 @@ when compiling the container. Read the sidebar below for more details. every time you ask the container for the ``my_mailer`` service, you get the same object back. This is usually how you want your services to work. - Imagine, however, that you need the ``request`` service in your ``my_mailer`` - service, maybe because you're reading the URL of the current request. - So, you add it as a constructor argument. Let's look at why this presents - a problem: + Imagine, however, that you need the ``client_configuration`` service + in your ``my_mailer`` service, maybe because you're reading some details + from it, such as what the "sender" address should be. You add it as a + constructor argument. Let's look at why this presents a problem: * When requesting ``my_mailer``, an instance of ``my_mailer`` (let's call - it *MailerA*) is created and the ``request`` service (let's call it - *RequestA*) is passed to it. Life is good! + it *MailerA*) is created and the ``client_configuration`` service (let's + call it *ConfigurationA*) is passed to it. Life is good! - * You've now made a subrequest in Symfony, which is a fancy way of saying - that you've called, for example, the ``{{ render(...) }}`` Twig function, - which executes another controller. Internally, the old ``request`` service - (*RequestA*) is actually replaced by a new request instance (*RequestB*). - This happens in the background, and it's totally normal. + * Your application now needs to do something with another client, and + you've architected your application in such a way that you handle this + by entering a new ``client_configuration`` scope and setting a new + ``client_configuration`` service into the container. Let's call this + *ConfigurationB*. - * In your embedded controller, you once again ask for the ``my_mailer`` + * Somewhere in your application, you once again ask for the ``my_mailer`` service. Since your service is in the ``container`` scope, the same instance (*MailerA*) is just re-used. But here's the problem: the - *MailerA* instance still contains the old *RequestA* object, which - is now **not** the correct request object to have (*RequestB* is now - the current ``request`` service). This is subtle, but the mis-match could - cause major problems, which is why it's not allowed. + *MailerA* instance still contains the old *ConfigurationA* object, which + is now **not** the correct configuration object to have (*ConfigurationB* + is now the current ``client_configuration`` service). This is subtle, + but the mis-match could cause major problems, which is why it's not + allowed. So, that's the reason *why* scopes exist, and how they can cause problems. Keep reading to find out the common solutions. @@ -79,19 +94,14 @@ when compiling the container. Read the sidebar below for more details. Using a Service from a narrower Scope ------------------------------------- -The most common problem with "scope" is when your service has a dependency -on the ``request`` service. The *easiest* way to solve this is to instead -inject the ``request_stack`` service and access the current Request by calling -the ``getCurrentRequest()`` method. - -This solution is great, but there are also others: +There are several solutions to the scope problem: * Use setter injection if the dependency is "synchronized"; (see :ref:`using-synchronized-service`). * Put your service in the same scope as the dependency (or a narrower one). If - you depend on the ``request`` service, this means putting your new service - in the ``request`` scope (see :ref:`changing-service-scope`); + you depend on the ``client_configuration`` service, this means putting your + new service in the ``client`` scope (see :ref:`changing-service-scope`); * Pass the entire container to your service and retrieve your dependency from the container each time you need it to be sure you have the right instance @@ -109,42 +119,81 @@ Using a synchronized Service Synchronized services are new in Symfony 2.3. Injecting the container or setting your service to a narrower scope have -drawbacks. For synchronized services (like the ``request``), using setter -injection is a nice option as it has no drawbacks and everything works -without any special code in your service or in your definition:: +drawbacks. Assume first that the ``client_configuration`` service has been +marked as "synchronized": + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + services: + client_configuration: + class: Acme\HelloBundle\Client\ClientConfiguration + scope: client + synchronized: true + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // app/config/config.php + use Symfony\Component\DependencyInjection\Definition; + + $defn = new Definition( + 'Acme\HelloBundle\Client\ClientConfiguration', + array() + ); + $defn->setScope('client'); + $defn->setSynchronized(true); + $container->setDefinition('client_configuration', $defn); + +Now, if you inject this service using setter injection, there are no drawbacks +and everything works without any special code in your service or in your definition:: // src/Acme/HelloBundle/Mail/Mailer.php namespace Acme\HelloBundle\Mail; - use Symfony\Component\HttpFoundation\Request; + use Acme\HelloBundle\Client\ClientConfiguration; class Mailer { - protected $request; + protected $clientConfiguration; - public function setRequest(Request $request = null) + public function setClientConfiguration(ClientConfiguration $clientConfiguration = null) { - $this->request = $request; + $this->clientConfiguration = $clientConfiguration; } public function sendEmail() { - if (null === $this->request) { + if (null === $this->clientConfiguration) { // throw an error? } - // ... do something using the request here + // ... do something using the client configuration here } } -Whenever the ``request`` scope is entered or left, the service container will -automatically call the ``setRequest()`` method with the current ``request`` -instance. +Whenever the ``client`` scope is entered or left, the service container will +automatically call the ``setClientConfiguration()`` method with the current +``client_configuration`` instance. -You might have noticed that the ``setRequest()`` method accepts ``null`` as a -valid value for the ``request`` argument. That's because when leaving the -``request`` scope, the ``request`` instance can be ``null`` (for the master -request for instance). Of course, you should take care of this possibility in +You might have noticed that the ``setClientConfiguration()`` method accepts +``null`` as a valid value for the ``client_configuration`` argument. That's +because when leaving the ``client`` scope, the ``client_configuration`` instance +can be ``null``. Of course, you should take care of this possibility in your code. This should also be taken into account when declaring your service: .. configuration-block:: @@ -156,7 +205,7 @@ your code. This should also be taken into account when declaring your service: greeting_card_manager: class: Acme\HelloBundle\Mail\GreetingCardManager calls: - - [setRequest, ['@?request=']] + - [setClientConfiguration, ['@?client_configuration=']] .. code-block:: xml @@ -165,8 +214,8 @@ your code. This should also be taken into account when declaring your service: - - + + @@ -181,46 +230,10 @@ your code. This should also be taken into account when declaring your service: 'greeting_card_manager', new Definition('Acme\HelloBundle\Mail\GreetingCardManager') ) - ->addMethodCall('setRequest', array( - new Reference('request', ContainerInterface::NULL_ON_INVALID_REFERENCE, false) + ->addMethodCall('setClientConfiguration', array( + new Reference('client_configuration', ContainerInterface::NULL_ON_INVALID_REFERENCE, false) )); -.. tip:: - - You can declare your own ``synchronized`` services very easily; here is - the declaration of the ``request`` service for reference: - - .. configuration-block:: - - .. code-block:: yaml - - services: - request: - scope: request - synthetic: true - synchronized: true - - .. code-block:: xml - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\ContainerInterface; - - $definition = $container->setDefinition('request') - ->setScope('request') - ->setSynthetic(true) - ->setSynchronized(true); - -.. caution:: - - The service using the synchronized service will need to be public in order - to have its setter called when the scope changes. - .. _changing-service-scope: Changing the Scope of your Service @@ -236,8 +249,8 @@ Changing the scope of a service should be done in its definition: services: greeting_card_manager: class: Acme\HelloBundle\Mail\GreetingCardManager - scope: request - arguments: [@request] + scope: client + arguments: [@client_configuration] .. code-block:: xml @@ -245,9 +258,9 @@ Changing the scope of a service should be done in its definition: - + .. code-block:: php @@ -259,9 +272,9 @@ Changing the scope of a service should be done in its definition: 'greeting_card_manager', new Definition( 'Acme\HelloBundle\Mail\GreetingCardManager', - array(new Reference('request'), + array(new Reference('client_configuration'), )) - )->setScope('request'); + )->setScope('client'); .. _passing-container: @@ -289,15 +302,15 @@ into your service:: public function sendEmail() { - $request = $this->container->get('request'); - // ... do something using the request here + $request = $this->container->get('client_configuration'); + // ... do something using the client configuration here } } .. caution:: - Take care not to store the request in a property of the object for a - future call of the service as it would cause the same issue described + Take care not to store the client configuration in a property of the object + for a future call of the service as it would cause the same issue described in the first section (except that Symfony cannot detect that you are wrong). From d417f7b4363011dacd6f91a99e94e602fe35f97d Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Mon, 11 Nov 2013 21:04:18 -0500 Subject: [PATCH 2/3] [#2972] Many small tweaks - thanks very much to @WouterJ and @xabbuh --- book/service_container.rst | 2 + cookbook/service_container/scopes.rst | 102 +++++++++++++++----------- 2 files changed, 63 insertions(+), 41 deletions(-) diff --git a/book/service_container.rst b/book/service_container.rst index 2d3b65ab0c5..a5c7b3d0748 100644 --- a/book/service_container.rst +++ b/book/service_container.rst @@ -754,6 +754,8 @@ Injecting the dependency by the setter method just needs a change of syntax: and "setter injection". The Symfony2 service container also supports "property injection". +.. _book-container-request-stack: + Injecting the Request ~~~~~~~~~~~~~~~~~~~~~ diff --git a/cookbook/service_container/scopes.rst b/cookbook/service_container/scopes.rst index 32884424732..03d94e1a426 100644 --- a/cookbook/service_container/scopes.rst +++ b/cookbook/service_container/scopes.rst @@ -12,9 +12,10 @@ This entry is all about scopes, a somewhat advanced topic related to the If you are trying to inject the ``request`` service, the simple solution is to inject the ``request_stack`` service instead and access the current - Request by calling the ``getCurrentRequest()`` method. The rest of this - entry talks about scopes in a theoretical and more advanced way. If you're - dealing with scopes for the ``request`` service, simply inject ``request_stack``. + Request by calling the ``getCurrentRequest()`` method (see :ref:`book-container-request-stack`). + The rest of this entry talks about scopes in a theoretical and more advanced + way. If you're dealing with scopes for the ``request`` service, simply + inject ``request_stack``. Understanding Scopes -------------------- @@ -34,8 +35,8 @@ also defines a third scope: ``request``. This scope is tied to the request, meaning a new instance is created for each subrequest and is unavailable outside the request (for instance in the CLI). -The Example: client Scope -~~~~~~~~~~~~~~~~~~~~~~~~~ +An Example: client Scope +~~~~~~~~~~~~~~~~~~~~~~~~ Other than the ``request`` service (which has a simple solution, see the above note), no services in the default Symfony2 container belong to any @@ -64,14 +65,14 @@ when compiling the container. Read the sidebar below for more details. from it, such as what the "sender" address should be. You add it as a constructor argument. Let's look at why this presents a problem: - * When requesting ``my_mailer``, an instance of ``my_mailer`` (let's call - it *MailerA*) is created and the ``client_configuration`` service (let's - call it *ConfigurationA*) is passed to it. Life is good! + * When requesting ``my_mailer``, an instance of ``my_mailer`` (called + *MailerA* here) is created and the ``client_configuration`` service ( + called *ConfigurationA* here) is passed to it. Life is good! * Your application now needs to do something with another client, and you've architected your application in such a way that you handle this by entering a new ``client_configuration`` scope and setting a new - ``client_configuration`` service into the container. Let's call this + ``client_configuration`` service into the container. Call this *ConfigurationB*. * Somewhere in your application, you once again ask for the ``my_mailer`` @@ -96,14 +97,14 @@ Using a Service from a narrower Scope There are several solutions to the scope problem: -* Use setter injection if the dependency is "synchronized"; (see +* A) Use setter injection if the dependency is "synchronized"; (see :ref:`using-synchronized-service`). -* Put your service in the same scope as the dependency (or a narrower one). If +* B) Put your service in the same scope as the dependency (or a narrower one). If you depend on the ``client_configuration`` service, this means putting your new service in the ``client`` scope (see :ref:`changing-service-scope`); -* Pass the entire container to your service and retrieve your dependency from +* C) Pass the entire container to your service and retrieve your dependency from the container each time you need it to be sure you have the right instance -- your service can live in the default ``container`` scope (see :ref:`passing-container`); @@ -112,15 +113,15 @@ Each scenario is detailed in the following sections. .. _using-synchronized-service: -Using a synchronized Service -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A) Using a synchronized Service +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. versionadded:: 2.3 Synchronized services are new in Symfony 2.3. -Injecting the container or setting your service to a narrower scope have +Both injecting the container and setting your service to a narrower scope have drawbacks. Assume first that the ``client_configuration`` service has been -marked as "synchronized": +marked as ``synchronized``: .. configuration-block:: @@ -132,6 +133,7 @@ marked as "synchronized": class: Acme\HelloBundle\Client\ClientConfiguration scope: client synchronized: true + # ... .. code-block:: xml @@ -139,10 +141,17 @@ marked as "synchronized": + xsi:schemaLocation="http://symfony.com/schema/dic/services + http://symfony.com/schema/dic/services/services-1.0.xsd" + > - + @@ -151,13 +160,13 @@ marked as "synchronized": // app/config/config.php use Symfony\Component\DependencyInjection\Definition; - $defn = new Definition( + $definition = new Definition( 'Acme\HelloBundle\Client\ClientConfiguration', array() ); - $defn->setScope('client'); - $defn->setSynchronized(true); - $container->setDefinition('client_configuration', $defn); + $definition->setScope('client'); + $definition->setSynchronized(true); + $container->setDefinition('client_configuration', $definition); Now, if you inject this service using setter injection, there are no drawbacks and everything works without any special code in your service or in your definition:: @@ -202,8 +211,8 @@ your code. This should also be taken into account when declaring your service: # src/Acme/HelloBundle/Resources/config/services.yml services: - greeting_card_manager: - class: Acme\HelloBundle\Mail\GreetingCardManager + my_mailer: + class: Acme\HelloBundle\Mail\Mailer calls: - [setClientConfiguration, ['@?client_configuration=']] @@ -211,11 +220,16 @@ your code. This should also be taken into account when declaring your service: - - + @@ -227,19 +241,25 @@ your code. This should also be taken into account when declaring your service: use Symfony\Component\DependencyInjection\ContainerInterface; $definition = $container->setDefinition( - 'greeting_card_manager', - new Definition('Acme\HelloBundle\Mail\GreetingCardManager') + 'my_mailer', + new Definition('Acme\HelloBundle\Mail\Mailer') ) ->addMethodCall('setClientConfiguration', array( - new Reference('client_configuration', ContainerInterface::NULL_ON_INVALID_REFERENCE, false) + new Reference( + 'client_configuration', + ContainerInterface::NULL_ON_INVALID_REFERENCE, + false + ) )); .. _changing-service-scope: -Changing the Scope of your Service -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +B) Changing the Scope of your Service +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Changing the scope of a service should be done in its definition: +Changing the scope of a service should be done in its definition. This example +assumes that the ``Mailer`` class has a ``__construct`` function whose first +argument is the ``ClientConfiguration`` object: .. configuration-block:: @@ -247,8 +267,8 @@ Changing the scope of a service should be done in its definition: # src/Acme/HelloBundle/Resources/config/services.yml services: - greeting_card_manager: - class: Acme\HelloBundle\Mail\GreetingCardManager + my_mailer: + class: Acme\HelloBundle\Mail\Mailer scope: client arguments: [@client_configuration] @@ -256,8 +276,8 @@ Changing the scope of a service should be done in its definition: - @@ -269,17 +289,17 @@ Changing the scope of a service should be done in its definition: use Symfony\Component\DependencyInjection\Definition; $definition = $container->setDefinition( - 'greeting_card_manager', + 'my_mailer', new Definition( - 'Acme\HelloBundle\Mail\GreetingCardManager', + 'Acme\HelloBundle\Mail\Mailer', array(new Reference('client_configuration'), )) )->setScope('client'); .. _passing-container: -Passing the Container as a Dependency of your Service -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +C) Passing the Container as a Dependency of your Service +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Setting the scope to a narrower one is not always possible (for instance, a twig extension must be in the ``container`` scope as the Twig environment From 3dcbbc25772b7e87388bf04b79d855b2a2911306 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Mon, 11 Nov 2013 21:12:38 -0500 Subject: [PATCH 3/3] [#2972] Clarifying how to use the request_stack with an example --- book/service_container.rst | 93 ++++++++++++++++++++++++++++++++------ 1 file changed, 79 insertions(+), 14 deletions(-) diff --git a/book/service_container.rst b/book/service_container.rst index a5c7b3d0748..8644d93685a 100644 --- a/book/service_container.rst +++ b/book/service_container.rst @@ -762,22 +762,87 @@ Injecting the Request .. versionadded:: 2.4 The ``request_stack`` service was introduced in version 2.4. -Almost all Symfony2 built-in services behave in the same way: a single -instance is created by the container which it returns whenever you get it or -when it is injected into another service. There is one exception in a standard -Symfony2 application: the ``request`` service. - -If you try to inject the ``request`` into a service, you will probably receive -a -:class:`Symfony\\Component\\DependencyInjection\\Exception\\ScopeWideningInjectionException` -exception. That's because the ``request`` can **change** during the life-time -of a container (when a sub-request is created for instance). - As of Symfony 2.4, instead of injecting the ``request`` service, you should inject the ``request_stack`` service instead and access the Request by calling -the ``getCurrentRequest()`` method. For earlier versions, or if you want to -understand this problem better, refer to the cookbook article -:doc:`/cookbook/service_container/scopes`. +the ``getCurrentRequest()`` method: + + namespace Acme\HelloBundle\Newsletter; + + use Symfony\Component\HttpFoundation\RequestStack; + + class NewsletterManager + { + protected $requestStack; + + public function __construct(RequestStack $requestStack) + { + $this->requestStack = $requestStack; + } + + public function anyMethod() + { + $request = $this->requestStack->getCurrentRequest(); + // ... do something with the request + } + + // ... + } + +Now, just inject the ``request_stack``, which behaves like any normal service: + +.. configuration-block:: + + .. code-block:: yaml + + # src/Acme/HelloBundle/Resources/config/services.yml + services: + newsletter_manager: + class: "Acme\HelloBundle\Newsletter\NewsletterManager" + arguments: ["@request_stack"] + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // src/Acme/HelloBundle/Resources/config/services.php + use Symfony\Component\DependencyInjection\Definition; + use Symfony\Component\DependencyInjection\Reference; + + // ... + $container->setDefinition('newsletter_manager', new Definition( + 'Acme\HelloBundle\Newsletter\NewsletterManager', + array(new Reference('request_stack')) + )); + +.. sidebar: Why not Inject the request Service? + + Almost all Symfony2 built-in services behave in the same way: a single + instance is created by the container which it returns whenever you get it or + when it is injected into another service. There is one exception in a standard + Symfony2 application: the ``request`` service. + + If you try to inject the ``request`` into a service, you will probably receive + a + :class:`Symfony\\Component\\DependencyInjection\\Exception\\ScopeWideningInjectionException` + exception. That's because the ``request`` can **change** during the life-time + of a container (when a sub-request is created for instance). + .. tip::